chat message optimsiation, fixed copy code during generation

fixes 17, fixes 53
This commit is contained in:
Jing Hua 2023-03-11 19:18:00 +08:00
parent e5c35eb1a3
commit 443d4d3454
11 changed files with 437 additions and 270 deletions

View file

@ -9,15 +9,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"dompurify": "^3.0.1",
"highlight.js": "^11.7.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"react-scroll-to-bottom": "^4.2.0", "react-scroll-to-bottom": "^4.2.0",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.2", "rehype-katex": "^6.0.2",
"rehype-sanitize": "^5.0.1",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"zustand": "^4.3.6" "zustand": "^4.3.6"

View file

@ -58,14 +58,14 @@ const ChatContent = () => {
<ChatTitle /> <ChatTitle />
{messages?.length === 0 && <NewMessageButton messageIndex={-1} />} {messages?.length === 0 && <NewMessageButton messageIndex={-1} />}
{messages?.map((message, index) => ( {messages?.map((message, index) => (
<> <React.Fragment key={index}>
<Message <Message
role={message.role} role={message.role}
content={message.content} content={message.content}
messageIndex={index} messageIndex={index}
/> />
<NewMessageButton messageIndex={index} /> <NewMessageButton messageIndex={index} />
</> </React.Fragment>
))} ))}
</div> </div>

View file

@ -12,101 +12,99 @@ import ImageIcon from '@icon/ImageIcon';
import PdfIcon from '@icon/PdfIcon'; import PdfIcon from '@icon/PdfIcon';
import MarkdownIcon from '@icon/MarkdownIcon'; import MarkdownIcon from '@icon/MarkdownIcon';
const DownloadChat = ({ const DownloadChat = React.memo(
saveRef, ({ saveRef }: { saveRef: React.RefObject<HTMLDivElement> }) => {
}: { const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
saveRef: React.RefObject<HTMLDivElement>; return (
}) => { <>
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); <button
return ( className='btn btn-neutral'
<> onClick={() => {
<button setIsModalOpen(true);
className='btn btn-neutral' }}
onClick={() => {
setIsModalOpen(true);
}}
>
Download Chat
</button>
{isModalOpen && (
<PopupModal
setIsModalOpen={setIsModalOpen}
title='Download Chat'
cancelButton={false}
> >
<div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'> Download Chat
<button </button>
className='btn btn-neutral gap-2' {isModalOpen && (
onClick={async () => { <PopupModal
if (saveRef && saveRef.current) { setIsModalOpen={setIsModalOpen}
const imgData = await htmlToImg(saveRef.current); title='Download Chat'
downloadImg( cancelButton={false}
imgData, >
`${ <div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'>
useStore <button
.getState() className='btn btn-neutral gap-2'
.chats?.[ onClick={async () => {
useStore.getState().currentChatIndex if (saveRef && saveRef.current) {
].title.trim() ?? 'download' const imgData = await htmlToImg(saveRef.current);
}.png` downloadImg(
); imgData,
}
}}
>
<ImageIcon />
Image
</button>
<button
className='btn btn-neutral gap-2'
onClick={async () => {
if (saveRef && saveRef.current) {
const imgData = await htmlToImg(saveRef.current);
downloadPDF(
imgData,
useStore.getState().theme,
`${
useStore
.getState()
.chats?.[
useStore.getState().currentChatIndex
].title.trim() ?? 'download'
}.pdf`
);
}
}}
>
<PdfIcon />
PDF
</button>
<button
className='btn btn-neutral gap-2'
onClick={async () => {
if (saveRef && saveRef.current) {
const chats = useStore.getState().chats;
if (chats) {
const markdown = chatToMarkdown(
chats[useStore.getState().currentChatIndex]
);
downloadMarkdown(
markdown,
`${ `${
chats[ useStore
useStore.getState().currentChatIndex .getState()
].title.trim() ?? 'download' .chats?.[
}.md` useStore.getState().currentChatIndex
].title.trim() ?? 'download'
}.png`
); );
} }
} }}
}} >
> <ImageIcon />
<MarkdownIcon /> Image
Markdown </button>
</button> <button
</div> className='btn btn-neutral gap-2'
</PopupModal> onClick={async () => {
)} if (saveRef && saveRef.current) {
</> const imgData = await htmlToImg(saveRef.current);
); downloadPDF(
}; imgData,
useStore.getState().theme,
`${
useStore
.getState()
.chats?.[
useStore.getState().currentChatIndex
].title.trim() ?? 'download'
}.pdf`
);
}
}}
>
<PdfIcon />
PDF
</button>
<button
className='btn btn-neutral gap-2'
onClick={async () => {
if (saveRef && saveRef.current) {
const chats = useStore.getState().chats;
if (chats) {
const markdown = chatToMarkdown(
chats[useStore.getState().currentChatIndex]
);
downloadMarkdown(
markdown,
`${
chats[
useStore.getState().currentChatIndex
].title.trim() ?? 'download'
}.md`
);
}
}
}}
>
<MarkdownIcon />
Markdown
</button>
</div>
</PopupModal>
)}
</>
);
}
);
export default DownloadChat; export default DownloadChat;

View file

@ -3,7 +3,7 @@ import { Role } from '@type/chat';
import SettingIcon from '@icon/SettingIcon'; import SettingIcon from '@icon/SettingIcon';
import PersonIcon from '@icon/PersonIcon'; import PersonIcon from '@icon/PersonIcon';
const Avatar = ({ role }: { role: Role }) => { const Avatar = React.memo(({ role }: { role: Role }) => {
return ( return (
<div className='w-[30px] flex flex-col relative items-end'> <div className='w-[30px] flex flex-col relative items-end'>
{role === 'user' && <UserAvatar />} {role === 'user' && <UserAvatar />}
@ -11,7 +11,7 @@ const Avatar = ({ role }: { role: Role }) => {
{role === 'system' && <SystemAvatar />} {role === 'system' && <SystemAvatar />}
</div> </div>
); );
}; });
const UserAvatar = () => { const UserAvatar = () => {
return ( return (

View file

@ -0,0 +1,66 @@
import React, { useRef, useState } from 'react';
import CopyIcon from '@icon/CopyIcon';
import TickIcon from '@icon/TickIcon';
const CodeBlock = ({
lang,
codeChildren,
}: {
lang: string;
codeChildren: React.ReactNode & React.ReactNode[];
}) => {
const codeRef = useRef<HTMLElement>(null);
return (
<div className='bg-black rounded-md'>
<CodeBar lang={lang} codeRef={codeRef} />
<div className='p-4 overflow-y-auto'>
<code ref={codeRef} className={`!whitespace-pre hljs language-${lang}`}>
{codeChildren}
</code>
</div>
</div>
);
};
const CodeBar = React.memo(
({
lang,
codeRef,
}: {
lang: string;
codeRef: React.RefObject<HTMLElement>;
}) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
return (
<div className='flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans'>
<span className=''>{lang}</span>
<button
className='flex ml-auto gap-2'
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString)
navigator.clipboard.writeText(codeString).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 3000);
});
}}
>
{isCopied ? (
<>
<TickIcon />
Copied!
</>
) : (
<>
<CopyIcon />
Copy code
</>
)}
</button>
</div>
);
}
);
export default CodeBlock;

View file

@ -30,11 +30,6 @@ const Message = React.memo(
className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${ className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${
backgroundStyle[messageIndex % 2] backgroundStyle[messageIndex % 2]
}`} }`}
key={
messageIndex !== -1
? `${messageIndex}-${content}`
: 'sticky-message-text-area'
}
> >
<div className='text-base gap-4 md:gap-6 m-auto md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0'> <div className='text-base gap-4 md:gap-6 m-auto md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0'>
<Avatar role={role} /> <Avatar role={role} />

View file

@ -1,13 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, {
DetailedHTMLProps,
HTMLAttributes,
useEffect,
useState,
} from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { CodeProps, ReactMarkdownProps } from 'react-markdown/lib/ast-to-react';
import rehypeKatex from 'rehype-katex'; import rehypeKatex from 'rehype-katex';
import rehypeSanitize from 'rehype-sanitize';
import rehypeHighlight from 'rehype-highlight';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import hljs from 'highlight.js';
import DOMPurify from 'dompurify';
import useStore from '@store/store'; import useStore from '@store/store';
import CopyIcon from '@icon/CopyIcon';
import EditIcon2 from '@icon/EditIcon2'; import EditIcon2 from '@icon/EditIcon2';
import DeleteIcon from '@icon/DeleteIcon'; import DeleteIcon from '@icon/DeleteIcon';
import TickIcon from '@icon/TickIcon'; import TickIcon from '@icon/TickIcon';
@ -20,6 +25,8 @@ import useSubmit from '@hooks/useSubmit';
import { ChatInterface } from '@type/chat'; import { ChatInterface } from '@type/chat';
import PopupModal from '@components/PopupModal'; import PopupModal from '@components/PopupModal';
import CodeBlock from './CodeBlock';
import { codeLanguageSubset } from '@constants/chat';
const MessageContent = ({ const MessageContent = ({
role, role,
@ -100,6 +107,14 @@ const ContentView = React.memo(
setChats(updatedChats); setChats(updatedChats);
}; };
const handleMoveUp = () => {
handleMove('up');
};
const handleMoveDown = () => {
handleMove('down');
};
const handleRefresh = () => { const handleRefresh = () => {
const updatedChats: ChatInterface[] = JSON.parse( const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats) JSON.stringify(useStore.getState().chats)
@ -114,70 +129,26 @@ const ContentView = React.memo(
<> <>
<div className='markdown prose w-full break-words dark:prose-invert dark'> <div className='markdown prose w-full break-words dark:prose-invert dark'>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]} remarkPlugins={[
rehypePlugins={[[rehypeKatex, { output: 'mathml' }]]} remarkGfm,
[remarkMath, { singleDollarTextMath: false }],
]}
rehypePlugins={[
rehypeSanitize,
[rehypeKatex, { output: 'mathml' }],
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: codeLanguageSubset,
},
],
]}
linkTarget='_new'
components={{ components={{
code({ node, inline, className, children, ...props }) { code,
if (inline) return <code>{children}</code>; p,
const [copied, setCopied] = useState<boolean>(false);
let highlight;
const match = /language-(\w+)/.exec(className || '');
const lang = match && match[1];
const isMatch = lang && hljs.getLanguage(lang);
if (isMatch)
highlight = hljs.highlight(children.toString(), {
language: lang,
});
else highlight = hljs.highlightAuto(children.toString());
return (
<div className='bg-black rounded-md'>
<div className='flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans'>
<span className=''>{highlight.language}</span>
<button
className='flex ml-auto gap-2'
onClick={() => {
navigator.clipboard
.writeText(children.toString())
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 3000);
});
}}
>
{copied ? (
<>
<TickIcon />
Copied!
</>
) : (
<>
<CopyIcon />
Copy code
</>
)}
</button>
</div>
<div className='p-4 overflow-y-auto'>
<code
className={`!whitespace-pre hljs language-${highlight.language}`}
>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(highlight.value, {
USE_PROFILES: { html: true },
}),
}}
/>
</code>
</div>
</div>
);
},
p({ className, children, ...props }) {
return <p className='whitespace-pre-wrap'>{children}</p>;
},
}} }}
> >
{content} {content}
@ -189,11 +160,9 @@ const ContentView = React.memo(
{role === 'assistant' && messageIndex === lastMessageIndex && ( {role === 'assistant' && messageIndex === lastMessageIndex && (
<RefreshButton onClick={handleRefresh} /> <RefreshButton onClick={handleRefresh} />
)} )}
{messageIndex !== 0 && ( {messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}
<UpButton onClick={() => handleMove('up')} />
)}
{messageIndex !== lastMessageIndex && ( {messageIndex !== lastMessageIndex && (
<DownButton onClick={() => handleMove('down')} /> <DownButton onClick={handleMoveDown} />
)} )}
<EditButton setIsEdit={setIsEdit} /> <EditButton setIsEdit={setIsEdit} />
@ -219,6 +188,33 @@ const ContentView = React.memo(
} }
); );
const code = React.memo((props: CodeProps) => {
const { inline, className, children } = props;
const match = /language-(\w+)/.exec(className || '');
const lang = match && match[1];
if (inline) {
return <code className={className}>{children}</code>;
} else {
return <CodeBlock lang={lang || 'text'} codeChildren={children} />;
}
});
const p = React.memo(
(
props?: Omit<
DetailedHTMLProps<
HTMLAttributes<HTMLParagraphElement>,
HTMLParagraphElement
>,
'ref'
> &
ReactMarkdownProps
) => {
return <p className='whitespace-pre-wrap'>{props?.children}</p>;
}
);
const MessageButton = ({ const MessageButton = ({
onClick, onClick,
icon, icon,
@ -238,23 +234,29 @@ const MessageButton = ({
); );
}; };
const EditButton = ({ const EditButton = React.memo(
setIsEdit, ({
}: { setIsEdit,
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>; }: {
}) => { setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
return <MessageButton icon={<EditIcon2 />} onClick={() => setIsEdit(true)} />; }) => {
}; return (
<MessageButton icon={<EditIcon2 />} onClick={() => setIsEdit(true)} />
);
}
);
const DeleteButton = ({ const DeleteButton = React.memo(
setIsDelete, ({
}: { setIsDelete,
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>; }: {
}) => { setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
return ( }) => {
<MessageButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} /> return (
); <MessageButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} />
}; );
}
);
const DownButton = ({ const DownButton = ({
onClick, onClick,
@ -263,7 +265,6 @@ const DownButton = ({
}) => { }) => {
return <MessageButton icon={<DownChevronArrow />} onClick={onClick} />; return <MessageButton icon={<DownChevronArrow />} onClick={onClick} />;
}; };
const UpButton = ({ const UpButton = ({
onClick, onClick,
}: { }: {
@ -276,7 +277,6 @@ const UpButton = ({
/> />
); );
}; };
const RefreshButton = ({ const RefreshButton = ({
onClick, onClick,
}: { }: {
@ -284,7 +284,6 @@ const RefreshButton = ({
}) => { }) => {
return <MessageButton icon={<RefreshIcon />} onClick={onClick} />; return <MessageButton icon={<RefreshIcon />} onClick={onClick} />;
}; };
const EditView = ({ const EditView = ({
content, content,
setIsEdit, setIsEdit,
@ -394,6 +393,40 @@ const EditView = ({
rows={1} rows={1}
></textarea> ></textarea>
</div> </div>
<EditViewButtons
sticky={sticky}
handleSaveAndSubmit={handleSaveAndSubmit}
handleSave={handleSave}
setIsModalOpen={setIsModalOpen}
setIsEdit={setIsEdit}
/>
{isModalOpen && (
<PopupModal
setIsModalOpen={setIsModalOpen}
title='Warning'
message='Please be advised that by submitting this message, all subsequent messages will be deleted!'
handleConfirm={handleSaveAndSubmit}
/>
)}
</>
);
};
const EditViewButtons = React.memo(
({
sticky = false,
handleSaveAndSubmit,
handleSave,
setIsModalOpen,
setIsEdit,
}: {
sticky?: boolean;
handleSaveAndSubmit: () => void;
handleSave: () => void;
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
return (
<div className='text-center mt-2 flex justify-center'> <div className='text-center mt-2 flex justify-center'>
{sticky && ( {sticky && (
<button <button
@ -437,16 +470,8 @@ const EditView = ({
</button> </button>
)} )}
</div> </div>
{isModalOpen && ( );
<PopupModal }
setIsModalOpen={setIsModalOpen} );
title='Warning'
message='Please be advised that by submitting this message, all subsequent messages will be deleted!'
handleConfirm={handleSaveAndSubmit}
/>
)}
</>
);
};
export default MessageContent; export default MessageContent;

View file

@ -4,65 +4,67 @@ import useStore from '@store/store';
import DownChevronArrow from '@icon/DownChevronArrow'; import DownChevronArrow from '@icon/DownChevronArrow';
import { ChatInterface, Role, roles } from '@type/chat'; import { ChatInterface, Role, roles } from '@type/chat';
const RoleSelector = ({ const RoleSelector = React.memo(
role, ({
messageIndex, role,
sticky, messageIndex,
}: { sticky,
role: Role; }: {
messageIndex: number; role: Role;
sticky?: boolean; messageIndex: number;
}) => { sticky?: boolean;
const setInputRole = useStore((state) => state.setInputRole); }) => {
const setChats = useStore((state) => state.setChats); const setInputRole = useStore((state) => state.setInputRole);
const currentChatIndex = useStore((state) => state.currentChatIndex); const setChats = useStore((state) => state.setChats);
const currentChatIndex = useStore((state) => state.currentChatIndex);
const [dropDown, setDropDown] = useState<boolean>(false); const [dropDown, setDropDown] = useState<boolean>(false);
return ( return (
<div className='prose dark:prose-invert relative'> <div className='prose dark:prose-invert relative'>
<button <button
className='btn btn-neutral btn-small flex gap-1' className='btn btn-neutral btn-small flex gap-1'
type='button' type='button'
onClick={() => setDropDown((prev) => !prev)} onClick={() => setDropDown((prev) => !prev)}
>
{role.charAt(0).toUpperCase() + role.slice(1)}
<DownChevronArrow />
</button>
<div
id='dropdown'
className={`${
dropDown ? '' : 'hidden'
} absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90`}
>
<ul
className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'
aria-labelledby='dropdownDefaultButton'
> >
{roles.map((r) => ( {role.charAt(0).toUpperCase() + role.slice(1)}
<li <DownChevronArrow />
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer' </button>
onClick={() => { <div
if (!sticky) { id='dropdown'
const updatedChats: ChatInterface[] = JSON.parse( className={`${
JSON.stringify(useStore.getState().chats) dropDown ? '' : 'hidden'
); } absolute top-100 bottom-100 z-10 bg-white rounded-lg shadow-xl border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800 opacity-90`}
updatedChats[currentChatIndex].messages[messageIndex].role = >
r; <ul
setChats(updatedChats); className='text-sm text-gray-700 dark:text-gray-200 p-0 m-0'
} else { aria-labelledby='dropdownDefaultButton'
setInputRole(r); >
} {roles.map((r) => (
setDropDown(false); <li
}} className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'
key={r} onClick={() => {
> if (!sticky) {
{r.charAt(0).toUpperCase() + r.slice(1)} const updatedChats: ChatInterface[] = JSON.parse(
</li> JSON.stringify(useStore.getState().chats)
))} );
</ul> updatedChats[currentChatIndex].messages[messageIndex].role =
r;
setChats(updatedChats);
} else {
setInputRole(r);
}
setDropDown(false);
}}
key={r}
>
{r.charAt(0).toUpperCase() + r.slice(1)}
</li>
))}
</ul>
</div>
</div> </div>
</div> );
); }
}; );
export default RoleSelector; export default RoleSelector;

View file

@ -3,7 +3,7 @@ import { useAtBottom, useScrollToBottom } from 'react-scroll-to-bottom';
import DownArrow from '@icon/DownArrow'; import DownArrow from '@icon/DownArrow';
const ScrollToBottomButton = () => { const ScrollToBottomButton = React.memo(() => {
const scrollToBottom = useScrollToBottom(); const scrollToBottom = useScrollToBottom();
const [atBottom] = useAtBottom(); const [atBottom] = useAtBottom();
@ -17,6 +17,6 @@ const ScrollToBottomButton = () => {
<DownArrow /> <DownArrow />
</button> </button>
); );
}; });
export default ScrollToBottomButton; export default ScrollToBottomButton;

View file

@ -24,3 +24,41 @@ export const generateDefaultChat = (title?: string): ChatInterface => ({
config: { ...defaultChatConfig }, config: { ...defaultChatConfig },
titleSet: false, titleSet: false,
}); });
export const codeLanguageSubset = [
'python',
'javascript',
'java',
'go',
'bash',
'c',
'cpp',
'csharp',
'css',
'diff',
'graphql',
'json',
'kotlin',
'less',
'lua',
'makefile',
'markdown',
'objectivec',
'perl',
'php',
'php-template',
'plaintext',
'python-repl',
'r',
'ruby',
'rust',
'scss',
'shell',
'sql',
'swift',
'typescript',
'vbnet',
'wasm',
'xml',
'yaml',
];

View file

@ -742,11 +742,6 @@ dompurify@^2.2.0:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87"
integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA== integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==
dompurify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.1.tgz#a0933f38931b3238934dd632043b727e53004289"
integrity sha512-60tsgvPKwItxZZdfLmamp0MTcecCta3avOhsLgPZ0qcWt96OasFfhkeIRbJ6br5i0fQawT1/RBGB5L58/Jpwuw==
electron-to-chromium@^1.4.284: electron-to-chromium@^1.4.284:
version "1.4.317" version "1.4.317"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.317.tgz#9a3d38a1a37f26a417d3d95dafe198ff11ed072b" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.317.tgz#9a3d38a1a37f26a417d3d95dafe198ff11ed072b"
@ -830,6 +825,13 @@ fastq@^1.6.0:
dependencies: dependencies:
reusify "^1.0.4" reusify "^1.0.4"
fault@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c"
integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==
dependencies:
format "^0.2.0"
fflate@^0.4.8: fflate@^0.4.8:
version "0.4.8" version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
@ -847,6 +849,11 @@ find-root@^1.1.0:
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
format@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
fraction.js@^4.2.0: fraction.js@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -916,7 +923,14 @@ hast-util-parse-selector@^3.0.0:
dependencies: dependencies:
"@types/hast" "^2.0.0" "@types/hast" "^2.0.0"
hast-util-to-text@^3.1.0: hast-util-sanitize@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-4.1.0.tgz#d90f8521f5083547095c5c63a7e03150303e0286"
integrity sha512-Hd9tU0ltknMGRDv+d6Ro/4XKzBqQnP/EZrpiTbpFYfXv/uOhWeKc+2uajcbEvAEH98VZd7eII2PiXm13RihnLw==
dependencies:
"@types/hast" "^2.0.0"
hast-util-to-text@^3.0.0, hast-util-to-text@^3.1.0:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz#ecf30c47141f41e91a5d32d0b1e1859fd2ac04f2" resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz#ecf30c47141f41e91a5d32d0b1e1859fd2ac04f2"
integrity sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw== integrity sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==
@ -942,7 +956,7 @@ hastscript@^7.0.0:
property-information "^6.0.0" property-information "^6.0.0"
space-separated-tokens "^2.0.0" space-separated-tokens "^2.0.0"
highlight.js@^11.7.0: highlight.js@~11.7.0:
version "11.7.0" version "11.7.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ== integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
@ -1095,6 +1109,15 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
lowlight@^2.0.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-2.8.1.tgz#5f54016ebd1b2f66b3d0b94d10ef6dd5df4f2e42"
integrity sha512-HCaGL61RKc1MYzEYn3rFoGkK0yslzCVDFJEanR19rc2L0mb8i58XM55jSRbzp9jcQrFzschPlwooC0vuNitk8Q==
dependencies:
"@types/hast" "^2.0.0"
fault "^2.0.0"
highlight.js "~11.7.0"
markdown-table@^3.0.0: markdown-table@^3.0.0:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd"
@ -1829,6 +1852,17 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.7:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
rehype-highlight@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/rehype-highlight/-/rehype-highlight-6.0.0.tgz#8097219d8813b51f4c2b6d92db27dac6cbc9a641"
integrity sha512-q7UtlFicLhetp7K48ZgZiJgchYscMma7XjzX7t23bqEJF8m6/s+viXQEe4oHjrATTIZpX7RG8CKD7BlNZoh9gw==
dependencies:
"@types/hast" "^2.0.0"
hast-util-to-text "^3.0.0"
lowlight "^2.0.0"
unified "^10.0.0"
unist-util-visit "^4.0.0"
rehype-katex@^6.0.2: rehype-katex@^6.0.2:
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-6.0.2.tgz#20197bbc10bdf79f6b999bffa6689d7f17226c35" resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-6.0.2.tgz#20197bbc10bdf79f6b999bffa6689d7f17226c35"
@ -1853,6 +1887,15 @@ rehype-parse@^8.0.0:
parse5 "^6.0.0" parse5 "^6.0.0"
unified "^10.0.0" unified "^10.0.0"
rehype-sanitize@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-5.0.1.tgz#dac01a7417bdd329260c74c74449697b4be5eb56"
integrity sha512-da/jIOjq8eYt/1r9GN6GwxIR3gde7OZ+WV8pheu1tL8K0D9KxM2AyMh+UEfke+FfdM3PvGHeYJU0Td5OWa7L5A==
dependencies:
"@types/hast" "^2.0.0"
hast-util-sanitize "^4.0.0"
unified "^10.0.0"
remark-gfm@^3.0.1: remark-gfm@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f"