diff --git a/src/components/Chat/ChatContent/Message/MessageContent.tsx b/src/components/Chat/ChatContent/Message/MessageContent.tsx index 4844450..93fe2de 100644 --- a/src/components/Chat/ChatContent/Message/MessageContent.tsx +++ b/src/components/Chat/ChatContent/Message/MessageContent.tsx @@ -1,35 +1,8 @@ -import React, { - DetailedHTMLProps, - HTMLAttributes, - useEffect, - useState, -} from 'react'; -import { useTranslation } from 'react-i18next'; -import ReactMarkdown from 'react-markdown'; -import { CodeProps, ReactMarkdownProps } from 'react-markdown/lib/ast-to-react'; -import rehypeKatex from 'rehype-katex'; -import rehypeHighlight from 'rehype-highlight'; -import remarkMath from 'remark-math'; -import remarkGfm from 'remark-gfm'; +import React, { useState } from 'react'; import useStore from '@store/store'; -import EditIcon2 from '@icon/EditIcon2'; -import DeleteIcon from '@icon/DeleteIcon'; -import TickIcon from '@icon/TickIcon'; -import CrossIcon from '@icon/CrossIcon'; -import RefreshIcon from '@icon/RefreshIcon'; -import DownChevronArrow from '@icon/DownChevronArrow'; -import CopyIcon from '@icon/CopyIcon'; - -import useSubmit from '@hooks/useSubmit'; - -import { ChatInterface } from '@type/chat'; - -import PopupModal from '@components/PopupModal'; -import TokenCount from '@components/TokenCount'; -import CommandPrompt from './CommandPrompt'; -import CodeBlock from './CodeBlock'; -import { codeLanguageSubset } from '@constants/chat'; +import ContentView from './View/ContentView'; +import EditView from './View/EditView'; const MessageContent = ({ role, @@ -67,486 +40,4 @@ const MessageContent = ({ ); }; -const ContentView = React.memo( - ({ - role, - content, - setIsEdit, - messageIndex, - }: { - role: string; - content: string; - setIsEdit: React.Dispatch>; - messageIndex: number; - }) => { - const { handleSubmit } = useSubmit(); - const [isDelete, setIsDelete] = useState(false); - const currentChatIndex = useStore((state) => state.currentChatIndex); - const setChats = useStore((state) => state.setChats); - const lastMessageIndex = useStore((state) => - state.chats ? state.chats[state.currentChatIndex].messages.length - 1 : 0 - ); - const inlineLatex = useStore((state) => state.inlineLatex); - - const handleDelete = () => { - const updatedChats: ChatInterface[] = JSON.parse( - JSON.stringify(useStore.getState().chats) - ); - updatedChats[currentChatIndex].messages.splice(messageIndex, 1); - setChats(updatedChats); - }; - - const handleMove = (direction: 'up' | 'down') => { - const updatedChats: ChatInterface[] = JSON.parse( - JSON.stringify(useStore.getState().chats) - ); - const updatedMessages = updatedChats[currentChatIndex].messages; - const temp = updatedMessages[messageIndex]; - if (direction === 'up') { - updatedMessages[messageIndex] = updatedMessages[messageIndex - 1]; - updatedMessages[messageIndex - 1] = temp; - } else { - updatedMessages[messageIndex] = updatedMessages[messageIndex + 1]; - updatedMessages[messageIndex + 1] = temp; - } - setChats(updatedChats); - }; - - const handleMoveUp = () => { - handleMove('up'); - }; - - const handleMoveDown = () => { - handleMove('down'); - }; - - const handleRefresh = () => { - const updatedChats: ChatInterface[] = JSON.parse( - JSON.stringify(useStore.getState().chats) - ); - const updatedMessages = updatedChats[currentChatIndex].messages; - updatedMessages.splice(updatedMessages.length - 1, 1); - setChats(updatedChats); - handleSubmit(); - }; - - const handleCopy = () => { - navigator.clipboard.writeText(content); - }; - - return ( - <> -
- - {content} - -
-
- {isDelete || ( - <> - {!useStore.getState().generating && - role === 'assistant' && - messageIndex === lastMessageIndex && ( - - )} - {messageIndex !== 0 && } - {messageIndex !== lastMessageIndex && ( - - )} - - - - - - )} - {isDelete && ( - <> - - - - )} -
- - ); - } -); - -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 {children}; - } else { - return ; - } -}); - -const p = React.memo( - ( - props?: Omit< - DetailedHTMLProps< - HTMLAttributes, - HTMLParagraphElement - >, - 'ref' - > & - ReactMarkdownProps - ) => { - return

{props?.children}

; - } -); - -const MessageButton = ({ - onClick, - icon, -}: { - onClick: React.MouseEventHandler; - icon: React.ReactElement; -}) => { - return ( -
- -
- ); -}; - -const EditButton = React.memo( - ({ - setIsEdit, - }: { - setIsEdit: React.Dispatch>; - }) => { - return ( - } onClick={() => setIsEdit(true)} /> - ); - } -); - -const DeleteButton = React.memo( - ({ - setIsDelete, - }: { - setIsDelete: React.Dispatch>; - }) => { - return ( - } onClick={() => setIsDelete(true)} /> - ); - } -); - -const DownButton = ({ - onClick, -}: { - onClick: React.MouseEventHandler; -}) => { - return } onClick={onClick} />; -}; -const UpButton = ({ - onClick, -}: { - onClick: React.MouseEventHandler; -}) => { - return ( - } - onClick={onClick} - /> - ); -}; - -const RefreshButton = ({ - onClick, -}: { - onClick: React.MouseEventHandler; -}) => { - return } onClick={onClick} />; -}; - -const CopyButton = ({ - onClick, -}: { - onClick: React.MouseEventHandler; -}) => { - const [isCopied, setIsCopied] = useState(false); - - return ( - : } - onClick={(e) => { - onClick(e); - setIsCopied(true); - window.setTimeout(() => { - setIsCopied(false); - }, 3000); - }} - /> - ); -}; - -const EditView = ({ - content, - setIsEdit, - messageIndex, - sticky, -}: { - content: string; - setIsEdit: React.Dispatch>; - messageIndex: number; - sticky?: boolean; -}) => { - const inputRole = useStore((state) => state.inputRole); - const setChats = useStore((state) => state.setChats); - const currentChatIndex = useStore((state) => state.currentChatIndex); - - const [_content, _setContent] = useState(content); - const [isModalOpen, setIsModalOpen] = useState(false); - const textareaRef = React.createRef(); - - const { t } = useTranslation(); - - const resetTextAreaHeight = () => { - if (textareaRef.current) textareaRef.current.style.height = 'auto'; - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - const isMobile = - /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|playbook|silk/i.test( - navigator.userAgent - ); - - if (e.key === 'Enter' && !isMobile && !e.nativeEvent.isComposing) { - const enterToSubmit = useStore.getState().enterToSubmit; - if (sticky) { - if ( - (enterToSubmit && !e.shiftKey) || - (!enterToSubmit && (e.ctrlKey || e.shiftKey)) - ) { - e.preventDefault(); - handleSaveAndSubmit(); - resetTextAreaHeight(); - } - } else { - if (e.ctrlKey && e.shiftKey) { - e.preventDefault(); - handleSaveAndSubmit(); - resetTextAreaHeight(); - } else if (e.ctrlKey || e.shiftKey) handleSave(); - } - } - }; - - const handleSave = () => { - if (sticky && (_content === '' || useStore.getState().generating)) return; - const updatedChats: ChatInterface[] = JSON.parse( - JSON.stringify(useStore.getState().chats) - ); - const updatedMessages = updatedChats[currentChatIndex].messages; - if (sticky) { - updatedMessages.push({ role: inputRole, content: _content }); - _setContent(''); - resetTextAreaHeight(); - } else { - updatedMessages[messageIndex].content = _content; - setIsEdit(false); - } - setChats(updatedChats); - }; - - const { handleSubmit } = useSubmit(); - const handleSaveAndSubmit = () => { - if (useStore.getState().generating) return; - const updatedChats: ChatInterface[] = JSON.parse( - JSON.stringify(useStore.getState().chats) - ); - const updatedMessages = updatedChats[currentChatIndex].messages; - if (sticky) { - if (_content !== '') { - updatedMessages.push({ role: inputRole, content: _content }); - } - _setContent(''); - resetTextAreaHeight(); - } else { - updatedMessages[messageIndex].content = _content; - updatedChats[currentChatIndex].messages = updatedMessages.slice( - 0, - messageIndex + 1 - ); - setIsEdit(false); - } - setChats(updatedChats); - handleSubmit(); - }; - - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; - } - }, [_content]); - - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; - } - }, []); - - return ( - <> -
- -
- - {isModalOpen && ( - - )} - - ); -}; - -const EditViewButtons = React.memo( - ({ - sticky = false, - handleSaveAndSubmit, - handleSave, - setIsModalOpen, - setIsEdit, - _setContent, - }: { - sticky?: boolean; - handleSaveAndSubmit: () => void; - handleSave: () => void; - setIsModalOpen: React.Dispatch>; - setIsEdit: React.Dispatch>; - _setContent: React.Dispatch>; - }) => { - const { t } = useTranslation(); - const generating = useStore.getState().generating; - const advancedMode = useStore((state) => state.advancedMode); - - return ( -
-
- {sticky && ( - - )} - - - - {sticky || ( - - )} - - {sticky || ( - - )} -
- {sticky && advancedMode && } - -
- ); - } -); - export default MessageContent; diff --git a/src/components/Chat/ChatContent/Message/View/Button/BaseButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/BaseButton.tsx new file mode 100644 index 0000000..59afd4e --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/BaseButton.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const BaseButton = ({ + onClick, + icon, +}: { + onClick: React.MouseEventHandler; + icon: React.ReactElement; +}) => { + return ( +
+ +
+ ); +}; + +export default BaseButton; diff --git a/src/components/Chat/ChatContent/Message/View/Button/CopyButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/CopyButton.tsx new file mode 100644 index 0000000..76ae6c6 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/CopyButton.tsx @@ -0,0 +1,29 @@ +import React, { useState } from 'react'; + +import TickIcon from '@icon/TickIcon'; +import CopyIcon from '@icon/CopyIcon'; + +import BaseButton from './BaseButton'; + +const CopyButton = ({ + onClick, +}: { + onClick: React.MouseEventHandler; +}) => { + const [isCopied, setIsCopied] = useState(false); + + return ( + : } + onClick={(e) => { + onClick(e); + setIsCopied(true); + window.setTimeout(() => { + setIsCopied(false); + }, 3000); + }} + /> + ); +}; + +export default CopyButton; diff --git a/src/components/Chat/ChatContent/Message/View/Button/DeleteButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/DeleteButton.tsx new file mode 100644 index 0000000..4853fd8 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/DeleteButton.tsx @@ -0,0 +1,19 @@ +import React, { memo } from 'react'; + +import DeleteIcon from '@icon/DeleteIcon'; + +import BaseButton from './BaseButton'; + +const DeleteButton = memo( + ({ + setIsDelete, + }: { + setIsDelete: React.Dispatch>; + }) => { + return ( + } onClick={() => setIsDelete(true)} /> + ); + } +); + +export default DeleteButton; diff --git a/src/components/Chat/ChatContent/Message/View/Button/DownButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/DownButton.tsx new file mode 100644 index 0000000..8310be5 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/DownButton.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import DownChevronArrow from '@icon/DownChevronArrow'; + +import BaseButton from './BaseButton'; + +const DownButton = ({ + onClick, +}: { + onClick: React.MouseEventHandler; +}) => { + return } onClick={onClick} />; +}; + +export default DownButton; diff --git a/src/components/Chat/ChatContent/Message/View/Button/EditButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/EditButton.tsx new file mode 100644 index 0000000..449b0d4 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/EditButton.tsx @@ -0,0 +1,17 @@ +import React, { memo } from 'react'; + +import EditIcon2 from '@icon/EditIcon2'; + +import BaseButton from './BaseButton'; + +const EditButton = memo( + ({ + setIsEdit, + }: { + setIsEdit: React.Dispatch>; + }) => { + return } onClick={() => setIsEdit(true)} />; + } +); + +export default EditButton; diff --git a/src/components/Chat/ChatContent/Message/View/Button/RefreshButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/RefreshButton.tsx new file mode 100644 index 0000000..b294478 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/RefreshButton.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import RefreshIcon from '@icon/RefreshIcon'; + +import BaseButton from './BaseButton'; + +const RefreshButton = ({ + onClick, +}: { + onClick: React.MouseEventHandler; +}) => { + return } onClick={onClick} />; +}; + +export default RefreshButton; diff --git a/src/components/Chat/ChatContent/Message/View/Button/UpButton.tsx b/src/components/Chat/ChatContent/Message/View/Button/UpButton.tsx new file mode 100644 index 0000000..bc40941 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/Button/UpButton.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import DownChevronArrow from '@icon/DownChevronArrow'; + +import BaseButton from './BaseButton'; + +const UpButton = ({ + onClick, +}: { + onClick: React.MouseEventHandler; +}) => { + return ( + } + onClick={onClick} + /> + ); +}; + +export default UpButton; diff --git a/src/components/Chat/ChatContent/Message/View/ContentView.tsx b/src/components/Chat/ChatContent/Message/View/ContentView.tsx new file mode 100644 index 0000000..997fb1c --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/ContentView.tsx @@ -0,0 +1,193 @@ +import React, { + DetailedHTMLProps, + HTMLAttributes, + memo, + useState, +} from 'react'; + +import ReactMarkdown from 'react-markdown'; +import { CodeProps, ReactMarkdownProps } from 'react-markdown/lib/ast-to-react'; + +import rehypeKatex from 'rehype-katex'; +import rehypeHighlight from 'rehype-highlight'; +import remarkMath from 'remark-math'; +import remarkGfm from 'remark-gfm'; +import useStore from '@store/store'; + +import TickIcon from '@icon/TickIcon'; +import CrossIcon from '@icon/CrossIcon'; + +import useSubmit from '@hooks/useSubmit'; + +import { ChatInterface } from '@type/chat'; + +import { codeLanguageSubset } from '@constants/chat'; + +import RefreshButton from './Button/RefreshButton'; +import UpButton from './Button/UpButton'; +import DownButton from './Button/DownButton'; +import CopyButton from './Button/CopyButton'; +import EditButton from './Button/EditButton'; +import DeleteButton from './Button/DeleteButton'; +import CodeBlock from '../CodeBlock'; + +const ContentView = memo( + ({ + role, + content, + setIsEdit, + messageIndex, + }: { + role: string; + content: string; + setIsEdit: React.Dispatch>; + messageIndex: number; + }) => { + const { handleSubmit } = useSubmit(); + const [isDelete, setIsDelete] = useState(false); + const currentChatIndex = useStore((state) => state.currentChatIndex); + const setChats = useStore((state) => state.setChats); + const lastMessageIndex = useStore((state) => + state.chats ? state.chats[state.currentChatIndex].messages.length - 1 : 0 + ); + const inlineLatex = useStore((state) => state.inlineLatex); + + const handleDelete = () => { + const updatedChats: ChatInterface[] = JSON.parse( + JSON.stringify(useStore.getState().chats) + ); + updatedChats[currentChatIndex].messages.splice(messageIndex, 1); + setChats(updatedChats); + }; + + const handleMove = (direction: 'up' | 'down') => { + const updatedChats: ChatInterface[] = JSON.parse( + JSON.stringify(useStore.getState().chats) + ); + const updatedMessages = updatedChats[currentChatIndex].messages; + const temp = updatedMessages[messageIndex]; + if (direction === 'up') { + updatedMessages[messageIndex] = updatedMessages[messageIndex - 1]; + updatedMessages[messageIndex - 1] = temp; + } else { + updatedMessages[messageIndex] = updatedMessages[messageIndex + 1]; + updatedMessages[messageIndex + 1] = temp; + } + setChats(updatedChats); + }; + + const handleMoveUp = () => { + handleMove('up'); + }; + + const handleMoveDown = () => { + handleMove('down'); + }; + + const handleRefresh = () => { + const updatedChats: ChatInterface[] = JSON.parse( + JSON.stringify(useStore.getState().chats) + ); + const updatedMessages = updatedChats[currentChatIndex].messages; + updatedMessages.splice(updatedMessages.length - 1, 1); + setChats(updatedChats); + handleSubmit(); + }; + + const handleCopy = () => { + navigator.clipboard.writeText(content); + }; + + return ( + <> +
+ + {content} + +
+
+ {isDelete || ( + <> + {!useStore.getState().generating && + role === 'assistant' && + messageIndex === lastMessageIndex && ( + + )} + {messageIndex !== 0 && } + {messageIndex !== lastMessageIndex && ( + + )} + + + + + + )} + {isDelete && ( + <> + + + + )} +
+ + ); + } +); + +const code = memo((props: CodeProps) => { + const { inline, className, children } = props; + const match = /language-(\w+)/.exec(className || ''); + const lang = match && match[1]; + + if (inline) { + return {children}; + } else { + return ; + } +}); + +const p = memo( + ( + props?: Omit< + DetailedHTMLProps< + HTMLAttributes, + HTMLParagraphElement + >, + 'ref' + > & + ReactMarkdownProps + ) => { + return

{props?.children}

; + } +); + +export default ContentView; diff --git a/src/components/Chat/ChatContent/Message/View/EditView.tsx b/src/components/Chat/ChatContent/Message/View/EditView.tsx new file mode 100644 index 0000000..3427923 --- /dev/null +++ b/src/components/Chat/ChatContent/Message/View/EditView.tsx @@ -0,0 +1,244 @@ +import React, { memo, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import useStore from '@store/store'; + +import useSubmit from '@hooks/useSubmit'; + +import { ChatInterface } from '@type/chat'; + +import PopupModal from '@components/PopupModal'; +import TokenCount from '@components/TokenCount'; +import CommandPrompt from '../CommandPrompt'; + +const EditView = ({ + content, + setIsEdit, + messageIndex, + sticky, +}: { + content: string; + setIsEdit: React.Dispatch>; + messageIndex: number; + sticky?: boolean; +}) => { + const inputRole = useStore((state) => state.inputRole); + const setChats = useStore((state) => state.setChats); + const currentChatIndex = useStore((state) => state.currentChatIndex); + + const [_content, _setContent] = useState(content); + const [isModalOpen, setIsModalOpen] = useState(false); + const textareaRef = React.createRef(); + + const { t } = useTranslation(); + + const resetTextAreaHeight = () => { + if (textareaRef.current) textareaRef.current.style.height = 'auto'; + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const isMobile = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|playbook|silk/i.test( + navigator.userAgent + ); + + if (e.key === 'Enter' && !isMobile && !e.nativeEvent.isComposing) { + const enterToSubmit = useStore.getState().enterToSubmit; + if (sticky) { + if ( + (enterToSubmit && !e.shiftKey) || + (!enterToSubmit && (e.ctrlKey || e.shiftKey)) + ) { + e.preventDefault(); + handleSaveAndSubmit(); + resetTextAreaHeight(); + } + } else { + if (e.ctrlKey && e.shiftKey) { + e.preventDefault(); + handleSaveAndSubmit(); + resetTextAreaHeight(); + } else if (e.ctrlKey || e.shiftKey) handleSave(); + } + } + }; + + const handleSave = () => { + if (sticky && (_content === '' || useStore.getState().generating)) return; + const updatedChats: ChatInterface[] = JSON.parse( + JSON.stringify(useStore.getState().chats) + ); + const updatedMessages = updatedChats[currentChatIndex].messages; + if (sticky) { + updatedMessages.push({ role: inputRole, content: _content }); + _setContent(''); + resetTextAreaHeight(); + } else { + updatedMessages[messageIndex].content = _content; + setIsEdit(false); + } + setChats(updatedChats); + }; + + const { handleSubmit } = useSubmit(); + const handleSaveAndSubmit = () => { + if (useStore.getState().generating) return; + const updatedChats: ChatInterface[] = JSON.parse( + JSON.stringify(useStore.getState().chats) + ); + const updatedMessages = updatedChats[currentChatIndex].messages; + if (sticky) { + if (_content !== '') { + updatedMessages.push({ role: inputRole, content: _content }); + } + _setContent(''); + resetTextAreaHeight(); + } else { + updatedMessages[messageIndex].content = _content; + updatedChats[currentChatIndex].messages = updatedMessages.slice( + 0, + messageIndex + 1 + ); + setIsEdit(false); + } + setChats(updatedChats); + handleSubmit(); + }; + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + } + }, [_content]); + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + } + }, []); + + return ( + <> +
+ +
+ + {isModalOpen && ( + + )} + + ); +}; + +const EditViewButtons = memo( + ({ + sticky = false, + handleSaveAndSubmit, + handleSave, + setIsModalOpen, + setIsEdit, + _setContent, + }: { + sticky?: boolean; + handleSaveAndSubmit: () => void; + handleSave: () => void; + setIsModalOpen: React.Dispatch>; + setIsEdit: React.Dispatch>; + _setContent: React.Dispatch>; + }) => { + const { t } = useTranslation(); + const generating = useStore.getState().generating; + const advancedMode = useStore((state) => state.advancedMode); + + return ( +
+
+ {sticky && ( + + )} + + + + {sticky || ( + + )} + + {sticky || ( + + )} +
+ {sticky && advancedMode && } + +
+ ); + } +); + +export default EditView;