refactor: message views

This commit is contained in:
Jing Hua 2023-04-21 17:42:27 +08:00
parent ece4778f88
commit 4f23af860e
10 changed files with 577 additions and 512 deletions

View file

@ -1,35 +1,8 @@
import React, { import React, { useState } from '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 useStore from '@store/store'; import useStore from '@store/store';
import EditIcon2 from '@icon/EditIcon2'; import ContentView from './View/ContentView';
import DeleteIcon from '@icon/DeleteIcon'; import EditView from './View/EditView';
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';
const MessageContent = ({ const MessageContent = ({
role, role,
@ -67,486 +40,4 @@ const MessageContent = ({
); );
}; };
const ContentView = React.memo(
({
role,
content,
setIsEdit,
messageIndex,
}: {
role: string;
content: string;
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
messageIndex: number;
}) => {
const { handleSubmit } = useSubmit();
const [isDelete, setIsDelete] = useState<boolean>(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 (
<>
<div className='markdown prose w-full md:max-w-full break-words dark:prose-invert dark share-gpt-message'>
<ReactMarkdown
remarkPlugins={[
remarkGfm,
[remarkMath, { singleDollarTextMath: inlineLatex }],
]}
rehypePlugins={[
rehypeKatex,
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: codeLanguageSubset,
},
],
]}
linkTarget='_new'
components={{
code,
p,
}}
>
{content}
</ReactMarkdown>
</div>
<div className='flex justify-end gap-2 w-full mt-2'>
{isDelete || (
<>
{!useStore.getState().generating &&
role === 'assistant' &&
messageIndex === lastMessageIndex && (
<RefreshButton onClick={handleRefresh} />
)}
{messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}
{messageIndex !== lastMessageIndex && (
<DownButton onClick={handleMoveDown} />
)}
<CopyButton onClick={handleCopy} />
<EditButton setIsEdit={setIsEdit} />
<DeleteButton setIsDelete={setIsDelete} />
</>
)}
{isDelete && (
<>
<button
className='p-1 hover:text-white'
onClick={() => setIsDelete(false)}
>
<CrossIcon />
</button>
<button className='p-1 hover:text-white' onClick={handleDelete}>
<TickIcon />
</button>
</>
)}
</div>
</>
);
}
);
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,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
icon: React.ReactElement;
}) => {
return (
<div className='text-gray-400 flex self-end lg:self-center justify-center gap-3 md:gap-4 visible'>
<button
className='p-1 rounded-md hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible'
onClick={onClick}
>
{icon}
</button>
</div>
);
};
const EditButton = React.memo(
({
setIsEdit,
}: {
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
return (
<MessageButton icon={<EditIcon2 />} onClick={() => setIsEdit(true)} />
);
}
);
const DeleteButton = React.memo(
({
setIsDelete,
}: {
setIsDelete: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
return (
<MessageButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} />
);
}
);
const DownButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return <MessageButton icon={<DownChevronArrow />} onClick={onClick} />;
};
const UpButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return (
<MessageButton
icon={<DownChevronArrow className='rotate-180' />}
onClick={onClick}
/>
);
};
const RefreshButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return <MessageButton icon={<RefreshIcon />} onClick={onClick} />;
};
const CopyButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
return (
<MessageButton
icon={isCopied ? <TickIcon /> : <CopyIcon />}
onClick={(e) => {
onClick(e);
setIsCopied(true);
window.setTimeout(() => {
setIsCopied(false);
}, 3000);
}}
/>
);
};
const EditView = ({
content,
setIsEdit,
messageIndex,
sticky,
}: {
content: string;
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
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<string>(content);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const textareaRef = React.createRef<HTMLTextAreaElement>();
const { t } = useTranslation();
const resetTextAreaHeight = () => {
if (textareaRef.current) textareaRef.current.style.height = 'auto';
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
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 (
<>
<div
className={`w-full ${
sticky
? 'py-2 md:py-3 px-2 md:px-4 border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]'
: ''
}`}
>
<textarea
ref={textareaRef}
className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden focus:ring-0 focus-visible:ring-0 leading-7 w-full placeholder:text-gray-500/40'
onChange={(e) => {
_setContent(e.target.value);
}}
value={_content}
placeholder={t('submitPlaceholder') as string}
onKeyDown={handleKeyDown}
rows={1}
></textarea>
</div>
<EditViewButtons
sticky={sticky}
handleSaveAndSubmit={handleSaveAndSubmit}
handleSave={handleSave}
setIsModalOpen={setIsModalOpen}
setIsEdit={setIsEdit}
_setContent={_setContent}
/>
{isModalOpen && (
<PopupModal
setIsModalOpen={setIsModalOpen}
title={t('warning') as string}
message={t('clearMessageWarning') as string}
handleConfirm={handleSaveAndSubmit}
/>
)}
</>
);
};
const EditViewButtons = React.memo(
({
sticky = false,
handleSaveAndSubmit,
handleSave,
setIsModalOpen,
setIsEdit,
_setContent,
}: {
sticky?: boolean;
handleSaveAndSubmit: () => void;
handleSave: () => void;
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
_setContent: React.Dispatch<React.SetStateAction<string>>;
}) => {
const { t } = useTranslation();
const generating = useStore.getState().generating;
const advancedMode = useStore((state) => state.advancedMode);
return (
<div className='flex'>
<div className='flex-1 text-center mt-2 flex justify-center'>
{sticky && (
<button
className={`btn relative mr-2 btn-primary ${
generating ? 'cursor-not-allowed opacity-40' : ''
}`}
onClick={handleSaveAndSubmit}
>
<div className='flex items-center justify-center gap-2'>
{t('saveAndSubmit')}
</div>
</button>
)}
<button
className={`btn relative mr-2 ${
sticky
? `btn-neutral ${
generating ? 'cursor-not-allowed opacity-40' : ''
}`
: 'btn-primary'
}`}
onClick={handleSave}
>
<div className='flex items-center justify-center gap-2'>
{t('save')}
</div>
</button>
{sticky || (
<button
className='btn relative mr-2 btn-neutral'
onClick={() => {
!generating && setIsModalOpen(true);
}}
>
<div className='flex items-center justify-center gap-2'>
{t('saveAndSubmit')}
</div>
</button>
)}
{sticky || (
<button
className='btn relative btn-neutral'
onClick={() => setIsEdit(false)}
>
<div className='flex items-center justify-center gap-2'>
{t('cancel')}
</div>
</button>
)}
</div>
{sticky && advancedMode && <TokenCount />}
<CommandPrompt _setContent={_setContent} />
</div>
);
}
);
export default MessageContent; export default MessageContent;

View file

@ -0,0 +1,22 @@
import React from 'react';
const BaseButton = ({
onClick,
icon,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
icon: React.ReactElement;
}) => {
return (
<div className='text-gray-400 flex self-end lg:self-center justify-center gap-3 md:gap-4 visible'>
<button
className='p-1 rounded-md hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible'
onClick={onClick}
>
{icon}
</button>
</div>
);
};
export default BaseButton;

View file

@ -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<HTMLButtonElement>;
}) => {
const [isCopied, setIsCopied] = useState<boolean>(false);
return (
<BaseButton
icon={isCopied ? <TickIcon /> : <CopyIcon />}
onClick={(e) => {
onClick(e);
setIsCopied(true);
window.setTimeout(() => {
setIsCopied(false);
}, 3000);
}}
/>
);
};
export default CopyButton;

View file

@ -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<React.SetStateAction<boolean>>;
}) => {
return (
<BaseButton icon={<DeleteIcon />} onClick={() => setIsDelete(true)} />
);
}
);
export default DeleteButton;

View file

@ -0,0 +1,15 @@
import React from 'react';
import DownChevronArrow from '@icon/DownChevronArrow';
import BaseButton from './BaseButton';
const DownButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return <BaseButton icon={<DownChevronArrow />} onClick={onClick} />;
};
export default DownButton;

View file

@ -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<React.SetStateAction<boolean>>;
}) => {
return <BaseButton icon={<EditIcon2 />} onClick={() => setIsEdit(true)} />;
}
);
export default EditButton;

View file

@ -0,0 +1,15 @@
import React from 'react';
import RefreshIcon from '@icon/RefreshIcon';
import BaseButton from './BaseButton';
const RefreshButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return <BaseButton icon={<RefreshIcon />} onClick={onClick} />;
};
export default RefreshButton;

View file

@ -0,0 +1,20 @@
import React from 'react';
import DownChevronArrow from '@icon/DownChevronArrow';
import BaseButton from './BaseButton';
const UpButton = ({
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}) => {
return (
<BaseButton
icon={<DownChevronArrow className='rotate-180' />}
onClick={onClick}
/>
);
};
export default UpButton;

View file

@ -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<React.SetStateAction<boolean>>;
messageIndex: number;
}) => {
const { handleSubmit } = useSubmit();
const [isDelete, setIsDelete] = useState<boolean>(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 (
<>
<div className='markdown prose w-full md:max-w-full break-words dark:prose-invert dark share-gpt-message'>
<ReactMarkdown
remarkPlugins={[
remarkGfm,
[remarkMath, { singleDollarTextMath: inlineLatex }],
]}
rehypePlugins={[
rehypeKatex,
[
rehypeHighlight,
{
detect: true,
ignoreMissing: true,
subset: codeLanguageSubset,
},
],
]}
linkTarget='_new'
components={{
code,
p,
}}
>
{content}
</ReactMarkdown>
</div>
<div className='flex justify-end gap-2 w-full mt-2'>
{isDelete || (
<>
{!useStore.getState().generating &&
role === 'assistant' &&
messageIndex === lastMessageIndex && (
<RefreshButton onClick={handleRefresh} />
)}
{messageIndex !== 0 && <UpButton onClick={handleMoveUp} />}
{messageIndex !== lastMessageIndex && (
<DownButton onClick={handleMoveDown} />
)}
<CopyButton onClick={handleCopy} />
<EditButton setIsEdit={setIsEdit} />
<DeleteButton setIsDelete={setIsDelete} />
</>
)}
{isDelete && (
<>
<button
className='p-1 hover:text-white'
onClick={() => setIsDelete(false)}
>
<CrossIcon />
</button>
<button className='p-1 hover:text-white' onClick={handleDelete}>
<TickIcon />
</button>
</>
)}
</div>
</>
);
}
);
const code = 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 = memo(
(
props?: Omit<
DetailedHTMLProps<
HTMLAttributes<HTMLParagraphElement>,
HTMLParagraphElement
>,
'ref'
> &
ReactMarkdownProps
) => {
return <p className='whitespace-pre-wrap'>{props?.children}</p>;
}
);
export default ContentView;

View file

@ -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<React.SetStateAction<boolean>>;
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<string>(content);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const textareaRef = React.createRef<HTMLTextAreaElement>();
const { t } = useTranslation();
const resetTextAreaHeight = () => {
if (textareaRef.current) textareaRef.current.style.height = 'auto';
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
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 (
<>
<div
className={`w-full ${
sticky
? 'py-2 md:py-3 px-2 md:px-4 border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-gray-700 rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]'
: ''
}`}
>
<textarea
ref={textareaRef}
className='m-0 resize-none rounded-lg bg-transparent overflow-y-hidden focus:ring-0 focus-visible:ring-0 leading-7 w-full placeholder:text-gray-500/40'
onChange={(e) => {
_setContent(e.target.value);
}}
value={_content}
placeholder={t('submitPlaceholder') as string}
onKeyDown={handleKeyDown}
rows={1}
></textarea>
</div>
<EditViewButtons
sticky={sticky}
handleSaveAndSubmit={handleSaveAndSubmit}
handleSave={handleSave}
setIsModalOpen={setIsModalOpen}
setIsEdit={setIsEdit}
_setContent={_setContent}
/>
{isModalOpen && (
<PopupModal
setIsModalOpen={setIsModalOpen}
title={t('warning') as string}
message={t('clearMessageWarning') as string}
handleConfirm={handleSaveAndSubmit}
/>
)}
</>
);
};
const EditViewButtons = memo(
({
sticky = false,
handleSaveAndSubmit,
handleSave,
setIsModalOpen,
setIsEdit,
_setContent,
}: {
sticky?: boolean;
handleSaveAndSubmit: () => void;
handleSave: () => void;
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
_setContent: React.Dispatch<React.SetStateAction<string>>;
}) => {
const { t } = useTranslation();
const generating = useStore.getState().generating;
const advancedMode = useStore((state) => state.advancedMode);
return (
<div className='flex'>
<div className='flex-1 text-center mt-2 flex justify-center'>
{sticky && (
<button
className={`btn relative mr-2 btn-primary ${
generating ? 'cursor-not-allowed opacity-40' : ''
}`}
onClick={handleSaveAndSubmit}
>
<div className='flex items-center justify-center gap-2'>
{t('saveAndSubmit')}
</div>
</button>
)}
<button
className={`btn relative mr-2 ${
sticky
? `btn-neutral ${
generating ? 'cursor-not-allowed opacity-40' : ''
}`
: 'btn-primary'
}`}
onClick={handleSave}
>
<div className='flex items-center justify-center gap-2'>
{t('save')}
</div>
</button>
{sticky || (
<button
className='btn relative mr-2 btn-neutral'
onClick={() => {
!generating && setIsModalOpen(true);
}}
>
<div className='flex items-center justify-center gap-2'>
{t('saveAndSubmit')}
</div>
</button>
)}
{sticky || (
<button
className='btn relative btn-neutral'
onClick={() => setIsEdit(false)}
>
<div className='flex items-center justify-center gap-2'>
{t('cancel')}
</div>
</button>
)}
</div>
{sticky && advancedMode && <TokenCount />}
<CommandPrompt _setContent={_setContent} />
</div>
);
}
);
export default EditView;