mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 21:34:00 +01:00
chat message optimsiation, fixed copy code during generation
fixes 17, fixes 53
This commit is contained in:
parent
e5c35eb1a3
commit
443d4d3454
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,8 @@ 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> }) => {
|
||||||
}: {
|
|
||||||
saveRef: React.RefObject<HTMLDivElement>;
|
|
||||||
}) => {
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -107,6 +104,7 @@ const DownloadChat = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default DownloadChat;
|
export default DownloadChat;
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
66
src/components/Chat/ChatContent/Message/CodeBlock.tsx
Normal file
66
src/components/Chat/ChatContent/Message/CodeBlock.tsx
Normal 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;
|
|
@ -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} />
|
||||||
|
|
|
@ -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 (
|
return (
|
||||||
<MessageButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} />
|
<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;
|
||||||
|
|
|
@ -4,15 +4,16 @@ 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,
|
role,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
sticky,
|
sticky,
|
||||||
}: {
|
}: {
|
||||||
role: Role;
|
role: Role;
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const setInputRole = useStore((state) => state.setInputRole);
|
const setInputRole = useStore((state) => state.setInputRole);
|
||||||
const setChats = useStore((state) => state.setChats);
|
const setChats = useStore((state) => state.setChats);
|
||||||
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
||||||
|
@ -64,5 +65,6 @@ const RoleSelector = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
export default RoleSelector;
|
export default RoleSelector;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
||||||
|
];
|
||||||
|
|
57
yarn.lock
57
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue