mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 19:33:59 +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"
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^3.0.1",
|
||||
"highlight.js": "^11.7.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-scroll-to-bottom": "^4.2.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-katex": "^6.0.2",
|
||||
"rehype-sanitize": "^5.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"zustand": "^4.3.6"
|
||||
|
|
|
@ -58,14 +58,14 @@ const ChatContent = () => {
|
|||
<ChatTitle />
|
||||
{messages?.length === 0 && <NewMessageButton messageIndex={-1} />}
|
||||
{messages?.map((message, index) => (
|
||||
<>
|
||||
<React.Fragment key={index}>
|
||||
<Message
|
||||
role={message.role}
|
||||
content={message.content}
|
||||
messageIndex={index}
|
||||
/>
|
||||
<NewMessageButton messageIndex={index} />
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,101 +12,99 @@ import ImageIcon from '@icon/ImageIcon';
|
|||
import PdfIcon from '@icon/PdfIcon';
|
||||
import MarkdownIcon from '@icon/MarkdownIcon';
|
||||
|
||||
const DownloadChat = ({
|
||||
saveRef,
|
||||
}: {
|
||||
saveRef: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className='btn btn-neutral'
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Download Chat
|
||||
</button>
|
||||
{isModalOpen && (
|
||||
<PopupModal
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
title='Download Chat'
|
||||
cancelButton={false}
|
||||
const DownloadChat = React.memo(
|
||||
({ saveRef }: { saveRef: React.RefObject<HTMLDivElement> }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className='btn btn-neutral'
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className='p-6 border-b border-gray-200 dark:border-gray-600 flex gap-4'>
|
||||
<button
|
||||
className='btn btn-neutral gap-2'
|
||||
onClick={async () => {
|
||||
if (saveRef && saveRef.current) {
|
||||
const imgData = await htmlToImg(saveRef.current);
|
||||
downloadImg(
|
||||
imgData,
|
||||
`${
|
||||
useStore
|
||||
.getState()
|
||||
.chats?.[
|
||||
useStore.getState().currentChatIndex
|
||||
].title.trim() ?? 'download'
|
||||
}.png`
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
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'>
|
||||
<button
|
||||
className='btn btn-neutral gap-2'
|
||||
onClick={async () => {
|
||||
if (saveRef && saveRef.current) {
|
||||
const imgData = await htmlToImg(saveRef.current);
|
||||
downloadImg(
|
||||
imgData,
|
||||
`${
|
||||
chats[
|
||||
useStore.getState().currentChatIndex
|
||||
].title.trim() ?? 'download'
|
||||
}.md`
|
||||
useStore
|
||||
.getState()
|
||||
.chats?.[
|
||||
useStore.getState().currentChatIndex
|
||||
].title.trim() ?? 'download'
|
||||
}.png`
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MarkdownIcon />
|
||||
Markdown
|
||||
</button>
|
||||
</div>
|
||||
</PopupModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}}
|
||||
>
|
||||
<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.getState().currentChatIndex
|
||||
].title.trim() ?? 'download'
|
||||
}.md`
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MarkdownIcon />
|
||||
Markdown
|
||||
</button>
|
||||
</div>
|
||||
</PopupModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default DownloadChat;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Role } from '@type/chat';
|
|||
import SettingIcon from '@icon/SettingIcon';
|
||||
import PersonIcon from '@icon/PersonIcon';
|
||||
|
||||
const Avatar = ({ role }: { role: Role }) => {
|
||||
const Avatar = React.memo(({ role }: { role: Role }) => {
|
||||
return (
|
||||
<div className='w-[30px] flex flex-col relative items-end'>
|
||||
{role === 'user' && <UserAvatar />}
|
||||
|
@ -11,7 +11,7 @@ const Avatar = ({ role }: { role: Role }) => {
|
|||
{role === 'system' && <SystemAvatar />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const UserAvatar = () => {
|
||||
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 ${
|
||||
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'>
|
||||
<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 { CodeProps, ReactMarkdownProps } from 'react-markdown/lib/ast-to-react';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeSanitize from 'rehype-sanitize';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import hljs from 'highlight.js';
|
||||
import DOMPurify from 'dompurify';
|
||||
import useStore from '@store/store';
|
||||
|
||||
import CopyIcon from '@icon/CopyIcon';
|
||||
import EditIcon2 from '@icon/EditIcon2';
|
||||
import DeleteIcon from '@icon/DeleteIcon';
|
||||
import TickIcon from '@icon/TickIcon';
|
||||
|
@ -20,6 +25,8 @@ import useSubmit from '@hooks/useSubmit';
|
|||
import { ChatInterface } from '@type/chat';
|
||||
|
||||
import PopupModal from '@components/PopupModal';
|
||||
import CodeBlock from './CodeBlock';
|
||||
import { codeLanguageSubset } from '@constants/chat';
|
||||
|
||||
const MessageContent = ({
|
||||
role,
|
||||
|
@ -100,6 +107,14 @@ const ContentView = React.memo(
|
|||
setChats(updatedChats);
|
||||
};
|
||||
|
||||
const handleMoveUp = () => {
|
||||
handleMove('up');
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
handleMove('down');
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
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'>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath, remarkGfm]}
|
||||
rehypePlugins={[[rehypeKatex, { output: 'mathml' }]]}
|
||||
remarkPlugins={[
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: false }],
|
||||
]}
|
||||
rehypePlugins={[
|
||||
rehypeSanitize,
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
detect: true,
|
||||
ignoreMissing: true,
|
||||
subset: codeLanguageSubset,
|
||||
},
|
||||
],
|
||||
]}
|
||||
linkTarget='_new'
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
if (inline) return <code>{children}</code>;
|
||||
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>;
|
||||
},
|
||||
code,
|
||||
p,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
|
@ -189,11 +160,9 @@ const ContentView = React.memo(
|
|||
{role === 'assistant' && messageIndex === lastMessageIndex && (
|
||||
<RefreshButton onClick={handleRefresh} />
|
||||
)}
|
||||
{messageIndex !== 0 && (
|
||||
<UpButton onClick={() => handleMove('up')} />
|
||||
)}
|
||||
{messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}
|
||||
{messageIndex !== lastMessageIndex && (
|
||||
<DownButton onClick={() => handleMove('down')} />
|
||||
<DownButton onClick={handleMoveDown} />
|
||||
)}
|
||||
|
||||
<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 = ({
|
||||
onClick,
|
||||
icon,
|
||||
|
@ -238,23 +234,29 @@ const MessageButton = ({
|
|||
);
|
||||
};
|
||||
|
||||
const EditButton = ({
|
||||
setIsEdit,
|
||||
}: {
|
||||
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return <MessageButton icon={<EditIcon2 />} onClick={() => setIsEdit(true)} />;
|
||||
};
|
||||
const EditButton = React.memo(
|
||||
({
|
||||
setIsEdit,
|
||||
}: {
|
||||
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return (
|
||||
<MessageButton icon={<EditIcon2 />} onClick={() => setIsEdit(true)} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const DeleteButton = ({
|
||||
setIsDelete,
|
||||
}: {
|
||||
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return (
|
||||
<MessageButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} />
|
||||
);
|
||||
};
|
||||
const DeleteButton = React.memo(
|
||||
({
|
||||
setIsDelete,
|
||||
}: {
|
||||
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}) => {
|
||||
return (
|
||||
<MessageButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const DownButton = ({
|
||||
onClick,
|
||||
|
@ -263,7 +265,6 @@ const DownButton = ({
|
|||
}) => {
|
||||
return <MessageButton icon={<DownChevronArrow />} onClick={onClick} />;
|
||||
};
|
||||
|
||||
const UpButton = ({
|
||||
onClick,
|
||||
}: {
|
||||
|
@ -276,7 +277,6 @@ const UpButton = ({
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RefreshButton = ({
|
||||
onClick,
|
||||
}: {
|
||||
|
@ -284,7 +284,6 @@ const RefreshButton = ({
|
|||
}) => {
|
||||
return <MessageButton icon={<RefreshIcon />} onClick={onClick} />;
|
||||
};
|
||||
|
||||
const EditView = ({
|
||||
content,
|
||||
setIsEdit,
|
||||
|
@ -394,6 +393,40 @@ const EditView = ({
|
|||
rows={1}
|
||||
></textarea>
|
||||
</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'>
|
||||
{sticky && (
|
||||
<button
|
||||
|
@ -437,16 +470,8 @@ const EditView = ({
|
|||
</button>
|
||||
)}
|
||||
</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;
|
||||
|
|
|
@ -4,65 +4,67 @@ import useStore from '@store/store';
|
|||
import DownChevronArrow from '@icon/DownChevronArrow';
|
||||
import { ChatInterface, Role, roles } from '@type/chat';
|
||||
|
||||
const RoleSelector = ({
|
||||
role,
|
||||
messageIndex,
|
||||
sticky,
|
||||
}: {
|
||||
role: Role;
|
||||
messageIndex: number;
|
||||
sticky?: boolean;
|
||||
}) => {
|
||||
const setInputRole = useStore((state) => state.setInputRole);
|
||||
const setChats = useStore((state) => state.setChats);
|
||||
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
||||
const RoleSelector = React.memo(
|
||||
({
|
||||
role,
|
||||
messageIndex,
|
||||
sticky,
|
||||
}: {
|
||||
role: Role;
|
||||
messageIndex: number;
|
||||
sticky?: boolean;
|
||||
}) => {
|
||||
const setInputRole = useStore((state) => state.setInputRole);
|
||||
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 (
|
||||
<div className='prose dark:prose-invert relative'>
|
||||
<button
|
||||
className='btn btn-neutral btn-small flex gap-1'
|
||||
type='button'
|
||||
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'
|
||||
return (
|
||||
<div className='prose dark:prose-invert relative'>
|
||||
<button
|
||||
className='btn btn-neutral btn-small flex gap-1'
|
||||
type='button'
|
||||
onClick={() => setDropDown((prev) => !prev)}
|
||||
>
|
||||
{roles.map((r) => (
|
||||
<li
|
||||
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'
|
||||
onClick={() => {
|
||||
if (!sticky) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(useStore.getState().chats)
|
||||
);
|
||||
updatedChats[currentChatIndex].messages[messageIndex].role =
|
||||
r;
|
||||
setChats(updatedChats);
|
||||
} else {
|
||||
setInputRole(r);
|
||||
}
|
||||
setDropDown(false);
|
||||
}}
|
||||
key={r}
|
||||
>
|
||||
{r.charAt(0).toUpperCase() + r.slice(1)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{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) => (
|
||||
<li
|
||||
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'
|
||||
onClick={() => {
|
||||
if (!sticky) {
|
||||
const updatedChats: ChatInterface[] = JSON.parse(
|
||||
JSON.stringify(useStore.getState().chats)
|
||||
);
|
||||
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>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
export default RoleSelector;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useAtBottom, useScrollToBottom } from 'react-scroll-to-bottom';
|
|||
|
||||
import DownArrow from '@icon/DownArrow';
|
||||
|
||||
const ScrollToBottomButton = () => {
|
||||
const ScrollToBottomButton = React.memo(() => {
|
||||
const scrollToBottom = useScrollToBottom();
|
||||
const [atBottom] = useAtBottom();
|
||||
|
||||
|
@ -17,6 +17,6 @@ const ScrollToBottomButton = () => {
|
|||
<DownArrow />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default ScrollToBottomButton;
|
||||
|
|
|
@ -24,3 +24,41 @@ export const generateDefaultChat = (title?: string): ChatInterface => ({
|
|||
config: { ...defaultChatConfig },
|
||||
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"
|
||||
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:
|
||||
version "1.4.317"
|
||||
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:
|
||||
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:
|
||||
version "0.4.8"
|
||||
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"
|
||||
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:
|
||||
version "4.2.0"
|
||||
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:
|
||||
"@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"
|
||||
resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz#ecf30c47141f41e91a5d32d0b1e1859fd2ac04f2"
|
||||
integrity sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==
|
||||
|
@ -942,7 +956,7 @@ hastscript@^7.0.0:
|
|||
property-information "^6.0.0"
|
||||
space-separated-tokens "^2.0.0"
|
||||
|
||||
highlight.js@^11.7.0:
|
||||
highlight.js@~11.7.0:
|
||||
version "11.7.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
|
||||
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
|
||||
|
@ -1095,6 +1109,15 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.0.3"
|
||||
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"
|
||||
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:
|
||||
version "6.0.2"
|
||||
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"
|
||||
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:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f"
|
||||
|
|
Loading…
Reference in a new issue