mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 22:53:59 +01:00
refactor: message views
This commit is contained in:
parent
ece4778f88
commit
4f23af860e
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
193
src/components/Chat/ChatContent/Message/View/ContentView.tsx
Normal file
193
src/components/Chat/ChatContent/Message/View/ContentView.tsx
Normal 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;
|
244
src/components/Chat/ChatContent/Message/View/EditView.tsx
Normal file
244
src/components/Chat/ChatContent/Message/View/EditView.tsx
Normal 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;
|
Loading…
Reference in a new issue