feat: performance optimisation

fixes partially #9 and #16
This commit is contained in:
Jing Hua 2023-03-05 22:59:31 +08:00
parent 59466e987a
commit 4fa5c14734
17 changed files with 350 additions and 410 deletions

View file

@ -6,38 +6,30 @@ import Menu from './components/Menu';
import ConfigMenu from './components/ConfigMenu'; import ConfigMenu from './components/ConfigMenu';
import useSaveToLocalStorage from '@hooks/useSaveToLocalStorage'; import useSaveToLocalStorage from '@hooks/useSaveToLocalStorage';
import useUpdateCharts from '@hooks/useUpdateChats';
import useInitialiseNewChat from '@hooks/useInitialiseNewChat'; import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
import { ChatInterface } from '@type/chat'; import { ChatInterface } from '@type/chat';
function App() { function App() {
useSaveToLocalStorage(); useSaveToLocalStorage();
useUpdateCharts();
const initialiseNewChat = useInitialiseNewChat(); const initialiseNewChat = useInitialiseNewChat();
const [setChats, setMessages, setCurrentChatIndex] = useStore((state) => [ const setChats = useStore((state) => state.setChats);
state.setChats, const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
state.setMessages,
state.setCurrentChatIndex,
]);
useEffect(() => { useEffect(() => {
// localStorage.removeItem('chats');
const storedChats = localStorage.getItem('chats'); const storedChats = localStorage.getItem('chats');
if (storedChats) { if (storedChats) {
try { try {
const chats: ChatInterface[] = JSON.parse(storedChats); const chats: ChatInterface[] = JSON.parse(storedChats);
if (chats.length > 0) { if (chats.length > 0) {
setChats(chats); setChats(chats);
setMessages(chats[0].messages);
setCurrentChatIndex(0); setCurrentChatIndex(0);
} else { } else {
initialiseNewChat(); initialiseNewChat();
} }
} catch (e: unknown) { } catch (e: unknown) {
setChats([]); setChats([]);
setMessages([]);
setCurrentChatIndex(-1); setCurrentChatIndex(-1);
console.log(e); console.log(e);
} }

View file

@ -1,4 +1,4 @@
import React, { createRef, useState } from 'react'; import React from 'react';
import ScrollToBottom from 'react-scroll-to-bottom'; import ScrollToBottom from 'react-scroll-to-bottom';
import useStore from '@store/store'; import useStore from '@store/store';
@ -11,11 +11,15 @@ import CrossIcon from '@icon/CrossIcon';
import useSubmit from '@hooks/useSubmit'; import useSubmit from '@hooks/useSubmit';
const ChatContent = () => { const ChatContent = () => {
const [messages, inputRole, setError] = useStore((state) => [ const inputRole = useStore((state) => state.inputRole);
state.messages, const setError = useStore((state) => state.setError);
state.inputRole, const messages = useStore((state) =>
state.setError, state.chats ? state.chats[state.currentChatIndex].messages : []
]); );
const stickyIndex = useStore((state) =>
state.chats ? state.chats[state.currentChatIndex].messages.length : 0
);
const { handleSubmit, error } = useSubmit(); const { handleSubmit, error } = useSubmit();
return ( return (
@ -38,7 +42,12 @@ const ChatContent = () => {
<NewMessageButton messageIndex={index} /> <NewMessageButton messageIndex={index} />
</> </>
))} ))}
<Message role={inputRole} content='' messageIndex={-1} sticky /> <Message
role={inputRole}
content=''
messageIndex={stickyIndex}
sticky
/>
{error !== '' && ( {error !== '' && (
<div className='relative bg-red-600/50 p-2 rounded-sm w-3/5 mt-3'> <div className='relative bg-red-600/50 p-2 rounded-sm w-3/5 mt-3'>

View file

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store';
import Avatar from './Avatar'; import Avatar from './Avatar';
import MessageContent from './MessageContent'; import MessageContent from './MessageContent';
@ -14,50 +13,48 @@ import RoleSelector from './RoleSelector';
// }; // };
const backgroundStyle = ['dark:bg-gray-800', 'bg-gray-50 dark:bg-[#444654]']; const backgroundStyle = ['dark:bg-gray-800', 'bg-gray-50 dark:bg-[#444654]'];
const Message = ({ const Message = React.memo(
role, ({
content, role,
messageIndex, content,
sticky = false, messageIndex,
}: { sticky = false,
role: Role; }: {
content: string; role: Role;
messageIndex: number; content: string;
sticky?: boolean; messageIndex: number;
}) => { sticky?: boolean;
const stickyIndex = useStore((state) => state.messages.length); }) => {
return (
return ( <div
<div className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${
className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${ backgroundStyle[messageIndex % 2]
sticky }`}
? backgroundStyle[stickyIndex % 2] key={
: backgroundStyle[messageIndex % 2] messageIndex !== -1
}`} ? `${messageIndex}-${content}`
key={ : 'sticky-message-text-area'
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} />
> <div className='w-[calc(100%-50px)] '>
<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'> <RoleSelector
<Avatar role={role} /> role={role}
<div className='w-[calc(100%-50px)] '> messageIndex={messageIndex}
<RoleSelector sticky={sticky}
role={role} />
messageIndex={messageIndex} <MessageContent
sticky={sticky} role={role}
/> content={content}
<MessageContent messageIndex={messageIndex}
role={role} sticky={sticky}
content={content} />
messageIndex={messageIndex} </div>
sticky={sticky}
/>
</div> </div>
</div> </div>
</div> );
); }
}; );
export default Message; export default Message;

View file

@ -17,7 +17,7 @@ import DownChevronArrow from '@icon/DownChevronArrow';
import useSubmit from '@hooks/useSubmit'; import useSubmit from '@hooks/useSubmit';
import { MessageInterface } from '@type/chat'; import { ChatInterface } from '@type/chat';
import PopupModal from '@components/PopupModal'; import PopupModal from '@components/PopupModal';
@ -56,158 +56,168 @@ const MessageContent = ({
); );
}; };
const ContentView = ({ const ContentView = React.memo(
role, ({
content, role,
setIsEdit, content,
messageIndex, setIsEdit,
}: { messageIndex,
role: string; }: {
content: string; role: string;
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>; content: string;
messageIndex: number; setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
}) => { messageIndex: number;
const { handleSubmit, error } = useSubmit(); }) => {
const { handleSubmit } = useSubmit();
const [isDelete, setIsDelete] = useState<boolean>(false); const [isDelete, setIsDelete] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false); const [copied, setCopied] = 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 [messages, setMessages] = useStore((state) => [ const handleDelete = () => {
state.messages, const updatedChats: ChatInterface[] = JSON.parse(
state.setMessages, JSON.stringify(useStore.getState().chats)
]); );
updatedChats[currentChatIndex].messages.splice(messageIndex, 1);
setChats(updatedChats);
};
const handleDelete = () => { const handleMove = (direction: 'up' | 'down') => {
const updatedMessages = JSON.parse(JSON.stringify(messages)); const updatedChats: ChatInterface[] = JSON.parse(
updatedMessages.splice(messageIndex, 1); JSON.stringify(useStore.getState().chats)
setMessages(updatedMessages); );
}; 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 handleMove = (direction: 'up' | 'down') => { const handleRefresh = () => {
const updatedMessages = JSON.parse(JSON.stringify(messages)); const updatedChats: ChatInterface[] = JSON.parse(
const temp = updatedMessages[messageIndex]; JSON.stringify(useStore.getState().chats)
if (direction === 'up') { );
updatedMessages[messageIndex] = updatedMessages[messageIndex - 1]; const updatedMessages = updatedChats[currentChatIndex].messages;
updatedMessages[messageIndex - 1] = temp; updatedMessages.splice(updatedMessages.length - 1, 1);
} else { setChats(updatedChats);
updatedMessages[messageIndex] = updatedMessages[messageIndex + 1]; handleSubmit();
updatedMessages[messageIndex + 1] = temp; };
}
setMessages(updatedMessages);
};
const handleRefresh = () => { return (
const updatedMessages = JSON.parse(JSON.stringify(messages)); <>
updatedMessages.splice(updatedMessages.length - 1, 1); <div className='markdown prose w-full break-words dark:prose-invert dark'>
setMessages(updatedMessages); <ReactMarkdown
handleSubmit(); remarkPlugins={[remarkMath, remarkGfm]}
}; rehypePlugins={[[rehypeKatex, { output: 'mathml' }]]}
components={{
code({ node, inline, className, children, ...props }) {
if (inline) return <code>{children}</code>;
let highlight;
return ( const match = /language-(\w+)/.exec(className || '');
<> const lang = match && match[1];
<div className='markdown prose w-full break-words dark:prose-invert dark'> if (lang)
<ReactMarkdown highlight = hljs.highlight(children.toString(), {
remarkPlugins={[remarkMath, remarkGfm]} language: lang,
rehypePlugins={[[rehypeKatex, { output: 'mathml' }]]} });
components={{ else highlight = hljs.highlightAuto(children.toString());
code({ node, inline, className, children, ...props }) {
if (inline) return <code>{children}</code>;
let highlight;
const match = /language-(\w+)/.exec(className || ''); return (
const lang = match && match[1]; <div className='bg-black rounded-md'>
if (lang) <div className='flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans'>
highlight = hljs.highlight(children.toString(), { <span className=''>{highlight.language}</span>
language: lang, <button
}); className='flex ml-auto gap-2'
else highlight = hljs.highlightAuto(children.toString()); onClick={() => {
navigator.clipboard
return ( .writeText(children.toString())
<div className='bg-black rounded-md'> .then(() => {
<div className='flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans'> setCopied(true);
<span className=''>{highlight.language}</span> setTimeout(() => setCopied(false), 3000);
<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> {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> </div>
</div> );
); },
}, p({ className, children, ...props }) {
p({ className, children, ...props }) { return <p className='whitespace-pre-wrap'>{children}</p>;
return <p className='whitespace-pre-wrap'>{children}</p>; },
}, }}
}} >
> {content}
{content} </ReactMarkdown>
</ReactMarkdown> </div>
</div> <div className='flex justify-end gap-2 w-full mt-2'>
<div className='flex justify-end gap-2 w-full mt-2'> {isDelete || (
{isDelete || ( <>
<> {role === 'assistant' && messageIndex === lastMessageIndex && (
{role === 'assistant' && messageIndex === messages?.length - 1 && ( <RefreshButton onClick={handleRefresh} />
<RefreshButton onClick={handleRefresh} /> )}
)} {messageIndex !== 0 && (
{messageIndex !== 0 && ( <UpButton onClick={() => handleMove('up')} />
<UpButton onClick={() => handleMove('up')} /> )}
)} {messageIndex !== lastMessageIndex && (
{messageIndex !== messages?.length - 1 && ( <DownButton onClick={() => handleMove('down')} />
<DownButton onClick={() => handleMove('down')} /> )}
)}
<EditButton setIsEdit={setIsEdit} /> <EditButton setIsEdit={setIsEdit} />
<DeleteButton setIsDelete={setIsDelete} /> <DeleteButton setIsDelete={setIsDelete} />
</> </>
)} )}
{isDelete && ( {isDelete && (
<> <>
<button <button
className='p-1 hover:text-white' className='p-1 hover:text-white'
onClick={() => setIsDelete(false)} onClick={() => setIsDelete(false)}
> >
<CrossIcon /> <CrossIcon />
</button> </button>
<button className='p-1 hover:text-white' onClick={handleDelete}> <button className='p-1 hover:text-white' onClick={handleDelete}>
<TickIcon /> <TickIcon />
</button> </button>
</> </>
)} )}
</div> </div>
</> </>
); );
}; }
);
const MessageButton = ({ const MessageButton = ({
onClick, onClick,
@ -286,11 +296,9 @@ const EditView = ({
messageIndex: number; messageIndex: number;
sticky?: boolean; sticky?: boolean;
}) => { }) => {
const [messages, setMessages, inputRole] = useStore((state) => [ const inputRole = useStore((state) => state.inputRole);
state.messages, const setChats = useStore((state) => state.setChats);
state.setMessages, const currentChatIndex = useStore((state) => state.currentChatIndex);
state.inputRole,
]);
const [_content, _setContent] = useState<string>(content); const [_content, _setContent] = useState<string>(content);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
@ -319,9 +327,10 @@ const EditView = ({
const handleSave = () => { const handleSave = () => {
if (_content === '') return; if (_content === '') return;
const updatedMessages: MessageInterface[] = JSON.parse( const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(messages) JSON.stringify(useStore.getState().chats)
); );
const updatedMessages = updatedChats[currentChatIndex].messages;
if (sticky) { if (sticky) {
updatedMessages.push({ role: inputRole, content: _content }); updatedMessages.push({ role: inputRole, content: _content });
_setContent(''); _setContent('');
@ -330,26 +339,29 @@ const EditView = ({
updatedMessages[messageIndex].content = _content; updatedMessages[messageIndex].content = _content;
setIsEdit(false); setIsEdit(false);
} }
setMessages(updatedMessages); setChats(updatedChats);
}; };
const { handleSubmit } = useSubmit(); const { handleSubmit } = useSubmit();
const handleSaveAndSubmit = () => { const handleSaveAndSubmit = () => {
if (_content == '') return; if (_content == '') return;
const updatedMessages: MessageInterface[] = JSON.parse( const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(messages) JSON.stringify(useStore.getState().chats)
); );
const updatedMessages = updatedChats[currentChatIndex].messages;
if (sticky) { if (sticky) {
updatedMessages.push({ role: inputRole, content: _content }); updatedMessages.push({ role: inputRole, content: _content });
_setContent(''); _setContent('');
setMessages(updatedMessages);
resetTextAreaHeight(); resetTextAreaHeight();
} else { } else {
updatedMessages[messageIndex].content = _content; updatedMessages[messageIndex].content = _content;
const _updatedMessages = updatedMessages.slice(0, messageIndex + 1); updatedChats[currentChatIndex].messages = updatedMessages.slice(
setMessages(_updatedMessages); 0,
messageIndex + 1
);
setIsEdit(false); setIsEdit(false);
} }
setChats(updatedChats);
handleSubmit(); handleSubmit();
}; };

View file

@ -3,72 +3,62 @@ import useStore from '@store/store';
import PlusIcon from '@icon/PlusIcon'; import PlusIcon from '@icon/PlusIcon';
import { ChatInterface, MessageInterface } from '@type/chat'; import { ChatInterface } from '@type/chat';
import { defaultSystemMessage } from '@constants/chat'; import { defaultSystemMessage } from '@constants/chat';
const NewMessageButton = ({ messageIndex }: { messageIndex: number }) => { const NewMessageButton = React.memo(
const [ ({ messageIndex }: { messageIndex: number }) => {
messages, const setChats = useStore((state) => state.setChats);
chats, const currentChatIndex = useStore((state) => state.currentChatIndex);
setMessages, const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
currentChatIndex,
setChats,
setCurrentChatIndex,
] = useStore((state) => [
state.messages,
state.chats,
state.setMessages,
state.currentChatIndex,
state.setChats,
state.setCurrentChatIndex,
]);
const addChat = () => { const addChat = () => {
if (chats) { const chats = useStore.getState().chats;
const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats)); if (chats) {
let titleIndex = 1; const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));
let title = `New Chat ${titleIndex}`; let titleIndex = 1;
let title = `New Chat ${titleIndex}`;
while (chats.some((chat) => chat.title === title)) { while (chats.some((chat) => chat.title === title)) {
titleIndex += 1; titleIndex += 1;
title = `New Chat ${titleIndex}`; title = `New Chat ${titleIndex}`;
}
updatedChats.unshift({
title,
messages: [{ role: 'system', content: defaultSystemMessage }],
});
setChats(updatedChats);
setCurrentChatIndex(0);
} }
};
updatedChats.unshift({ const addMessage = () => {
title, if (currentChatIndex === -1) {
messages: [{ role: 'system', content: defaultSystemMessage }], addChat();
}); } else {
setChats(updatedChats); const updatedChats: ChatInterface[] = JSON.parse(
setMessages(updatedChats[0].messages); JSON.stringify(useStore.getState().chats)
setCurrentChatIndex(0); );
} updatedChats[currentChatIndex].messages.splice(messageIndex + 1, 0, {
}; content: '',
role: 'user',
});
setChats(updatedChats);
}
};
const addMessage = () => { return (
if (currentChatIndex === -1) { <div className='h-0 w-0 relative' key={messageIndex}>
addChat(); <div
} else { className='absolute top-0 right-0 translate-x-1/2 translate-y-[-50%] text-gray-600 dark:text-white cursor-pointer bg-gray-200 dark:bg-gray-600/80 rounded-full p-1 text-sm hover:bg-gray-300 dark:hover:bg-gray-800/80 transition-bg duration-200'
const updatedMessages: MessageInterface[] = JSON.parse( onClick={addMessage}
JSON.stringify(messages) >
); <PlusIcon />
updatedMessages.splice(messageIndex + 1, 0, { </div>
content: '',
role: 'user',
});
setMessages(updatedMessages);
}
};
return (
<div className='h-0 w-0 relative' key={messageIndex}>
<div
className='absolute top-0 right-0 translate-x-1/2 translate-y-[-50%] text-gray-600 dark:text-white cursor-pointer bg-gray-200 dark:bg-gray-600/80 rounded-full p-1 text-sm hover:bg-gray-300 dark:hover:bg-gray-800/80 transition-bg duration-200'
onClick={addMessage}
>
<PlusIcon />
</div> </div>
</div> );
); }
}; );
export default NewMessageButton; export default NewMessageButton;

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import useStore from '@store/store'; import useStore from '@store/store';
import DownChevronArrow from '@icon/DownChevronArrow'; import DownChevronArrow from '@icon/DownChevronArrow';
import { MessageInterface, Role, roles } from '@type/chat'; import { ChatInterface, Role, roles } from '@type/chat';
const RoleSelector = ({ const RoleSelector = ({
role, role,
@ -13,11 +13,9 @@ const RoleSelector = ({
messageIndex: number; messageIndex: number;
sticky?: boolean; sticky?: boolean;
}) => { }) => {
const [messages, setMessages, setInputRole] = useStore((state) => [ const setInputRole = useStore((state) => state.setInputRole);
state.messages, const setChats = useStore((state) => state.setChats);
state.setMessages, const currentChatIndex = useStore((state) => state.currentChatIndex);
state.setInputRole,
]);
const [dropDown, setDropDown] = useState<boolean>(false); const [dropDown, setDropDown] = useState<boolean>(false);
@ -46,11 +44,12 @@ const RoleSelector = ({
className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer' className='px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white cursor-pointer'
onClick={() => { onClick={() => {
if (!sticky) { if (!sticky) {
const updatedMessages: MessageInterface[] = JSON.parse( const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(messages) JSON.stringify(useStore.getState().chats)
); );
updatedMessages[messageIndex].role = r; updatedChats[currentChatIndex].messages[messageIndex].role =
setMessages(updatedMessages); r;
setChats(updatedChats);
} else { } else {
setInputRole(r); setInputRole(r);
} }
@ -66,5 +65,4 @@ const RoleSelector = ({
</div> </div>
); );
}; };
export default RoleSelector; export default RoleSelector;

View file

@ -5,15 +5,12 @@ import CrossIcon2 from '@icon/CrossIcon2';
import { validateApiKey } from '@api/customApi'; import { validateApiKey } from '@api/customApi';
const ConfigMenu = () => { const ConfigMenu = () => {
const [apiKey, setApiKey, apiFree, setApiFree, openConfig, setOpenConfig] = const apiKey = useStore((state) => state.apiKey);
useStore((state) => [ const setApiKey = useStore((state) => state.setApiKey);
state.apiKey, const apiFree = useStore((state) => state.apiFree);
state.setApiKey, const setApiFree = useStore((state) => state.setApiFree);
state.apiFree, const openConfig = useStore((state) => state.openConfig);
state.setApiFree, const setOpenConfig = useStore((state) => state.setOpenConfig);
state.openConfig,
state.setOpenConfig,
]);
const [_apiKey, _setApiKey] = useState<string>(apiKey || ''); const [_apiKey, _setApiKey] = useState<string>(apiKey || '');
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);

View file

@ -10,11 +10,8 @@ import CrossIcon from '@icon/CrossIcon';
import useInitialiseNewChat from '@hooks/useInitialiseNewChat'; import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
const ChatHistoryList = () => { const ChatHistoryList = () => {
const [chats, setCurrentChatIndex, setMessages] = useStore((state) => [ const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
state.chats, const chats = useStore((state) => state.chats);
state.setCurrentChatIndex,
state.setMessages,
]);
return ( return (
<div className='flex-col flex-1 overflow-y-auto border-b border-white/20'> <div className='flex-col flex-1 overflow-y-auto border-b border-white/20'>
@ -27,7 +24,6 @@ const ChatHistoryList = () => {
chatIndex={index} chatIndex={index}
onClick={() => { onClick={() => {
setCurrentChatIndex(index); setCurrentChatIndex(index);
setMessages(chats[index].messages);
}} }}
/> />
))} ))}
@ -66,14 +62,9 @@ const ChatHistory = ({
onClick?: React.MouseEventHandler<HTMLElement>; onClick?: React.MouseEventHandler<HTMLElement>;
}) => { }) => {
const initialiseNewChat = useInitialiseNewChat(); const initialiseNewChat = useInitialiseNewChat();
const [chats, setChats, currentChatIndex, setMessages, setCurrentChatIndex] = const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
useStore((state) => [ const setChats = useStore((state) => state.setChats);
state.chats, const currentChatIndex = useStore((state) => state.currentChatIndex);
state.setChats,
state.currentChatIndex,
state.setMessages,
state.setCurrentChatIndex,
]);
const [isDelete, setIsDelete] = useState<boolean>(false); const [isDelete, setIsDelete] = useState<boolean>(false);
const [isEdit, setIsEdit] = useState<boolean>(false); const [isEdit, setIsEdit] = useState<boolean>(false);
@ -83,7 +74,7 @@ const ChatHistory = ({
const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => { const handleTick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
const updatedChats = JSON.parse(JSON.stringify(chats)); const updatedChats = JSON.parse(JSON.stringify(useStore.getState().chats));
if (isEdit) { if (isEdit) {
updatedChats[chatIndex].title = _title; updatedChats[chatIndex].title = _title;
setChats(updatedChats); setChats(updatedChats);
@ -92,7 +83,6 @@ const ChatHistory = ({
updatedChats.splice(chatIndex, 1); updatedChats.splice(chatIndex, 1);
if (updatedChats.length > 0) { if (updatedChats.length > 0) {
setCurrentChatIndex(0); setCurrentChatIndex(0);
setMessages(updatedChats[0].messages);
setChats(updatedChats); setChats(updatedChats);
} else { } else {
initialiseNewChat(); initialiseNewChat();

View file

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store';
import NewChat from './NewChat'; import NewChat from './NewChat';
import ChatHistoryList from './ChatHistoryList'; import ChatHistoryList from './ChatHistoryList';

View file

@ -4,10 +4,8 @@ import useStore from '@store/store';
import PersonIcon from '@icon/PersonIcon'; import PersonIcon from '@icon/PersonIcon';
const Config = () => { const Config = () => {
const [apiFree, setOpenConfig] = useStore((state) => [ const apiFree = useStore((state) => state.apiFree);
state.apiFree, const setOpenConfig = useStore((state) => state.setOpenConfig);
state.setOpenConfig,
]);
return ( return (
<a <a

View file

@ -1,20 +1,10 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store';
import PlusIcon from '@icon/PlusIcon'; import PlusIcon from '@icon/PlusIcon';
import useAddChat from '@hooks/useAddChat'; import useAddChat from '@hooks/useAddChat';
const NewChat = () => { const NewChat = () => {
const [chats, setChats, setCurrentChatIndex, setMessages] = useStore(
(state) => [
state.chats,
state.setChats,
state.setCurrentChatIndex,
state.setMessages,
]
);
const addChat = useAddChat(); const addChat = useAddChat();
return ( return (

View file

@ -5,10 +5,12 @@ import PlusIcon from '@icon/PlusIcon';
import useAddChat from '@hooks/useAddChat'; import useAddChat from '@hooks/useAddChat';
const MobileBar = () => { const MobileBar = () => {
const [chats, currentChatIndex] = useStore((state) => [ const chatTitle = useStore((state) =>
state.chats, state.chats && state.chats.length > 0
state.currentChatIndex, ? state.chats[state.currentChatIndex].title
]); : 'New Chat'
);
const addChat = useAddChat(); const addChat = useAddChat();
return ( return (
@ -42,11 +44,7 @@ const MobileBar = () => {
<line x1='3' y1='18' x2='21' y2='18'></line> <line x1='3' y1='18' x2='21' y2='18'></line>
</svg> </svg>
</button> </button>
<h1 className='flex-1 text-center text-base font-normal'> <h1 className='flex-1 text-center text-base font-normal'>{chatTitle}</h1>
{chats && chats.length > 0
? chats[currentChatIndex]?.title
: 'New Chat'}
</h1>
<button type='button' className='px-3 text-gray-400' onClick={addChat}> <button type='button' className='px-3 text-gray-400' onClick={addChat}>
<PlusIcon className='h-6 w-6' /> <PlusIcon className='h-6 w-6' />
</button> </button>

View file

@ -4,16 +4,11 @@ import { defaultSystemMessage } from '@constants/chat';
import { ChatInterface } from '@type/chat'; import { ChatInterface } from '@type/chat';
const useAddChat = () => { const useAddChat = () => {
const [chats, setChats, setCurrentChatIndex, setMessages] = useStore( const setChats = useStore((state) => state.setChats);
(state) => [ const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
state.chats,
state.setChats,
state.setCurrentChatIndex,
state.setMessages,
]
);
const addChat = () => { const addChat = () => {
const chats = useStore.getState().chats;
if (chats) { if (chats) {
const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats)); const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));
let titleIndex = 1; let titleIndex = 1;
@ -29,7 +24,6 @@ const useAddChat = () => {
messages: [{ role: 'system', content: defaultSystemMessage }], messages: [{ role: 'system', content: defaultSystemMessage }],
}); });
setChats(updatedChats); setChats(updatedChats);
setMessages(updatedChats[0].messages);
setCurrentChatIndex(0); setCurrentChatIndex(0);
} }
}; };

View file

@ -4,11 +4,8 @@ import { MessageInterface } from '@type/chat';
import { defaultSystemMessage } from '@constants/chat'; import { defaultSystemMessage } from '@constants/chat';
const useInitialiseNewChat = () => { const useInitialiseNewChat = () => {
const [setChats, setMessages, setCurrentChatIndex] = useStore((state) => [ const setChats = useStore((state) => state.setChats);
state.setChats, const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
state.setMessages,
state.setCurrentChatIndex,
]);
const initialiseNewChat = () => { const initialiseNewChat = () => {
const message: MessageInterface = { const message: MessageInterface = {
@ -21,7 +18,6 @@ const useInitialiseNewChat = () => {
messages: [message], messages: [message],
}, },
]); ]);
setMessages([message]);
setCurrentChatIndex(0); setCurrentChatIndex(0);
}; };

View file

@ -1,12 +1,19 @@
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import useStore from '@store/store'; import useStore from '@store/store';
const useSaveToLocalStorage = () => { const useSaveToLocalStorage = () => {
const chats = useStore((state) => state.chats); const chatsRef = useRef(useStore.getState().chats);
useEffect(() => { useEffect(() => {
if (chats) localStorage.setItem('chats', JSON.stringify(chats)); const unsubscribe = useStore.subscribe((state) => {
}, [chats]); if (chatsRef && chatsRef.current !== state.chats) {
chatsRef.current = state.chats;
localStorage.setItem('chats', JSON.stringify(state.chats));
}
});
return unsubscribe;
}, []);
}; };
export default useSaveToLocalStorage; export default useSaveToLocalStorage;

View file

@ -1,48 +1,45 @@
import React from 'react'; import React from 'react';
import useStore from '@store/store'; import useStore from '@store/store';
import { MessageInterface } from '@type/chat'; import { ChatInterface } from '@type/chat';
import { getChatCompletionStream as getChatCompletionStreamFree } from '@api/freeApi'; import { getChatCompletionStream as getChatCompletionStreamFree } from '@api/freeApi';
import { getChatCompletionStream as getChatCompletionStreamCustom } from '@api/customApi'; import { getChatCompletionStream as getChatCompletionStreamCustom } from '@api/customApi';
import { parseEventSource } from '@api/helper'; import { parseEventSource } from '@api/helper';
const useSubmit = () => { const useSubmit = () => {
const [ const error = useStore((state) => state.error);
error, const setError = useStore((state) => state.setError);
setError, const apiFree = useStore((state) => state.apiFree);
apiFree, const apiKey = useStore((state) => state.apiKey);
apiKey, const setGenerating = useStore((state) => state.setGenerating);
setMessages, const generating = useStore((state) => state.generating);
setGenerating, const currentChatIndex = useStore((state) => state.currentChatIndex);
generating, const setChats = useStore((state) => state.setChats);
] = useStore((state) => [
state.error,
state.setError,
state.apiFree,
state.apiKey,
state.setMessages,
state.setGenerating,
state.generating,
]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (generating) return; const chats = useStore.getState().chats;
const messages = useStore.getState().messages; if (generating || !chats) return;
const updatedMessages: MessageInterface[] = JSON.parse( const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));
JSON.stringify(messages)
);
updatedMessages.push({ role: 'assistant', content: '' }); updatedChats[currentChatIndex].messages.push({
role: 'assistant',
content: '',
});
setMessages(updatedMessages); setChats(updatedChats);
setGenerating(true); setGenerating(true);
let stream; let stream;
try { try {
if (apiFree) { if (apiFree) {
stream = await getChatCompletionStreamFree(messages); stream = await getChatCompletionStreamFree(
chats[currentChatIndex].messages
);
} else if (apiKey) { } else if (apiKey) {
stream = await getChatCompletionStreamCustom(apiKey, messages); stream = await getChatCompletionStreamCustom(
apiKey,
chats[currentChatIndex].messages
);
} }
if (stream) { if (stream) {
@ -65,11 +62,12 @@ const useSubmit = () => {
} }
}, ''); }, '');
const updatedMessages: MessageInterface[] = JSON.parse( const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().messages) JSON.stringify(useStore.getState().chats)
); );
const updatedMessages = updatedChats[currentChatIndex].messages;
updatedMessages[updatedMessages.length - 1].content += resultString; updatedMessages[updatedMessages.length - 1].content += resultString;
setMessages(updatedMessages); setChats(updatedChats);
} }
} }
} }

View file

@ -1,25 +0,0 @@
import React, { useEffect } from 'react';
import useStore from '@store/store';
import { ChatInterface, MessageInterface } from '@type/chat';
const useUpdateCharts = () => {
const [chats, messages, setChats, currentChatIndex] = useStore((state) => [
state.chats,
state.messages,
state.setChats,
state.currentChatIndex,
]);
useEffect(() => {
if (currentChatIndex !== -1 && chats && chats.length > 0) {
const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));
const updatedMessages: MessageInterface[] = JSON.parse(
JSON.stringify(messages)
);
updatedChats[currentChatIndex].messages = updatedMessages;
setChats(updatedChats);
}
}, [messages]);
};
export default useUpdateCharts;