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 useSaveToLocalStorage from '@hooks/useSaveToLocalStorage';
import useUpdateCharts from '@hooks/useUpdateChats';
import useInitialiseNewChat from '@hooks/useInitialiseNewChat';
import { ChatInterface } from '@type/chat';
function App() {
useSaveToLocalStorage();
useUpdateCharts();
const initialiseNewChat = useInitialiseNewChat();
const [setChats, setMessages, setCurrentChatIndex] = useStore((state) => [
state.setChats,
state.setMessages,
state.setCurrentChatIndex,
]);
const setChats = useStore((state) => state.setChats);
const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
useEffect(() => {
// localStorage.removeItem('chats');
const storedChats = localStorage.getItem('chats');
if (storedChats) {
try {
const chats: ChatInterface[] = JSON.parse(storedChats);
if (chats.length > 0) {
setChats(chats);
setMessages(chats[0].messages);
setCurrentChatIndex(0);
} else {
initialiseNewChat();
}
} catch (e: unknown) {
setChats([]);
setMessages([]);
setCurrentChatIndex(-1);
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 useStore from '@store/store';
@ -11,11 +11,15 @@ import CrossIcon from '@icon/CrossIcon';
import useSubmit from '@hooks/useSubmit';
const ChatContent = () => {
const [messages, inputRole, setError] = useStore((state) => [
state.messages,
state.inputRole,
state.setError,
]);
const inputRole = useStore((state) => state.inputRole);
const setError = useStore((state) => state.setError);
const messages = useStore((state) =>
state.chats ? state.chats[state.currentChatIndex].messages : []
);
const stickyIndex = useStore((state) =>
state.chats ? state.chats[state.currentChatIndex].messages.length : 0
);
const { handleSubmit, error } = useSubmit();
return (
@ -38,7 +42,12 @@ const ChatContent = () => {
<NewMessageButton messageIndex={index} />
</>
))}
<Message role={inputRole} content='' messageIndex={-1} sticky />
<Message
role={inputRole}
content=''
messageIndex={stickyIndex}
sticky
/>
{error !== '' && (
<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 useStore from '@store/store';
import Avatar from './Avatar';
import MessageContent from './MessageContent';
@ -14,7 +13,8 @@ import RoleSelector from './RoleSelector';
// };
const backgroundStyle = ['dark:bg-gray-800', 'bg-gray-50 dark:bg-[#444654]'];
const Message = ({
const Message = React.memo(
({
role,
content,
messageIndex,
@ -25,14 +25,10 @@ const Message = ({
messageIndex: number;
sticky?: boolean;
}) => {
const stickyIndex = useStore((state) => state.messages.length);
return (
<div
className={`w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${
sticky
? backgroundStyle[stickyIndex % 2]
: backgroundStyle[messageIndex % 2]
backgroundStyle[messageIndex % 2]
}`}
key={
messageIndex !== -1
@ -58,6 +54,7 @@ const Message = ({
</div>
</div>
);
};
}
);
export default Message;

View file

@ -17,7 +17,7 @@ import DownChevronArrow from '@icon/DownChevronArrow';
import useSubmit from '@hooks/useSubmit';
import { MessageInterface } from '@type/chat';
import { ChatInterface } from '@type/chat';
import PopupModal from '@components/PopupModal';
@ -56,7 +56,8 @@ const MessageContent = ({
);
};
const ContentView = ({
const ContentView = React.memo(
({
role,
content,
setIsEdit,
@ -67,24 +68,29 @@ const ContentView = ({
setIsEdit: React.Dispatch<React.SetStateAction<boolean>>;
messageIndex: number;
}) => {
const { handleSubmit, error } = useSubmit();
const { handleSubmit } = useSubmit();
const [isDelete, setIsDelete] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const [messages, setMessages] = useStore((state) => [
state.messages,
state.setMessages,
]);
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 handleDelete = () => {
const updatedMessages = JSON.parse(JSON.stringify(messages));
updatedMessages.splice(messageIndex, 1);
setMessages(updatedMessages);
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
updatedChats[currentChatIndex].messages.splice(messageIndex, 1);
setChats(updatedChats);
};
const handleMove = (direction: 'up' | 'down') => {
const updatedMessages = JSON.parse(JSON.stringify(messages));
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];
@ -93,13 +99,16 @@ const ContentView = ({
updatedMessages[messageIndex] = updatedMessages[messageIndex + 1];
updatedMessages[messageIndex + 1] = temp;
}
setMessages(updatedMessages);
setChats(updatedChats);
};
const handleRefresh = () => {
const updatedMessages = JSON.parse(JSON.stringify(messages));
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
const updatedMessages = updatedChats[currentChatIndex].messages;
updatedMessages.splice(updatedMessages.length - 1, 1);
setMessages(updatedMessages);
setChats(updatedChats);
handleSubmit();
};
@ -177,13 +186,13 @@ const ContentView = ({
<div className='flex justify-end gap-2 w-full mt-2'>
{isDelete || (
<>
{role === 'assistant' && messageIndex === messages?.length - 1 && (
{role === 'assistant' && messageIndex === lastMessageIndex && (
<RefreshButton onClick={handleRefresh} />
)}
{messageIndex !== 0 && (
<UpButton onClick={() => handleMove('up')} />
)}
{messageIndex !== messages?.length - 1 && (
{messageIndex !== lastMessageIndex && (
<DownButton onClick={() => handleMove('down')} />
)}
@ -207,7 +216,8 @@ const ContentView = ({
</div>
</>
);
};
}
);
const MessageButton = ({
onClick,
@ -286,11 +296,9 @@ const EditView = ({
messageIndex: number;
sticky?: boolean;
}) => {
const [messages, setMessages, inputRole] = useStore((state) => [
state.messages,
state.setMessages,
state.inputRole,
]);
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);
@ -319,9 +327,10 @@ const EditView = ({
const handleSave = () => {
if (_content === '') return;
const updatedMessages: MessageInterface[] = JSON.parse(
JSON.stringify(messages)
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
const updatedMessages = updatedChats[currentChatIndex].messages;
if (sticky) {
updatedMessages.push({ role: inputRole, content: _content });
_setContent('');
@ -330,26 +339,29 @@ const EditView = ({
updatedMessages[messageIndex].content = _content;
setIsEdit(false);
}
setMessages(updatedMessages);
setChats(updatedChats);
};
const { handleSubmit } = useSubmit();
const handleSaveAndSubmit = () => {
if (_content == '') return;
const updatedMessages: MessageInterface[] = JSON.parse(
JSON.stringify(messages)
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
const updatedMessages = updatedChats[currentChatIndex].messages;
if (sticky) {
updatedMessages.push({ role: inputRole, content: _content });
_setContent('');
setMessages(updatedMessages);
resetTextAreaHeight();
} else {
updatedMessages[messageIndex].content = _content;
const _updatedMessages = updatedMessages.slice(0, messageIndex + 1);
setMessages(_updatedMessages);
updatedChats[currentChatIndex].messages = updatedMessages.slice(
0,
messageIndex + 1
);
setIsEdit(false);
}
setChats(updatedChats);
handleSubmit();
};

View file

@ -3,27 +3,17 @@ import useStore from '@store/store';
import PlusIcon from '@icon/PlusIcon';
import { ChatInterface, MessageInterface } from '@type/chat';
import { ChatInterface } from '@type/chat';
import { defaultSystemMessage } from '@constants/chat';
const NewMessageButton = ({ messageIndex }: { messageIndex: number }) => {
const [
messages,
chats,
setMessages,
currentChatIndex,
setChats,
setCurrentChatIndex,
] = useStore((state) => [
state.messages,
state.chats,
state.setMessages,
state.currentChatIndex,
state.setChats,
state.setCurrentChatIndex,
]);
const NewMessageButton = React.memo(
({ messageIndex }: { messageIndex: number }) => {
const setChats = useStore((state) => state.setChats);
const currentChatIndex = useStore((state) => state.currentChatIndex);
const setCurrentChatIndex = useStore((state) => state.setCurrentChatIndex);
const addChat = () => {
const chats = useStore.getState().chats;
if (chats) {
const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));
let titleIndex = 1;
@ -39,7 +29,6 @@ const NewMessageButton = ({ messageIndex }: { messageIndex: number }) => {
messages: [{ role: 'system', content: defaultSystemMessage }],
});
setChats(updatedChats);
setMessages(updatedChats[0].messages);
setCurrentChatIndex(0);
}
};
@ -48,14 +37,14 @@ const NewMessageButton = ({ messageIndex }: { messageIndex: number }) => {
if (currentChatIndex === -1) {
addChat();
} else {
const updatedMessages: MessageInterface[] = JSON.parse(
JSON.stringify(messages)
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
updatedMessages.splice(messageIndex + 1, 0, {
updatedChats[currentChatIndex].messages.splice(messageIndex + 1, 0, {
content: '',
role: 'user',
});
setMessages(updatedMessages);
setChats(updatedChats);
}
};
@ -69,6 +58,7 @@ const NewMessageButton = ({ messageIndex }: { messageIndex: number }) => {
</div>
</div>
);
};
}
);
export default NewMessageButton;

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import useStore from '@store/store';
import DownChevronArrow from '@icon/DownChevronArrow';
import { MessageInterface, Role, roles } from '@type/chat';
import { ChatInterface, Role, roles } from '@type/chat';
const RoleSelector = ({
role,
@ -13,11 +13,9 @@ const RoleSelector = ({
messageIndex: number;
sticky?: boolean;
}) => {
const [messages, setMessages, setInputRole] = useStore((state) => [
state.messages,
state.setMessages,
state.setInputRole,
]);
const setInputRole = useStore((state) => state.setInputRole);
const setChats = useStore((state) => state.setChats);
const currentChatIndex = useStore((state) => state.currentChatIndex);
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'
onClick={() => {
if (!sticky) {
const updatedMessages: MessageInterface[] = JSON.parse(
JSON.stringify(messages)
const updatedChats: ChatInterface[] = JSON.parse(
JSON.stringify(useStore.getState().chats)
);
updatedMessages[messageIndex].role = r;
setMessages(updatedMessages);
updatedChats[currentChatIndex].messages[messageIndex].role =
r;
setChats(updatedChats);
} else {
setInputRole(r);
}
@ -66,5 +65,4 @@ const RoleSelector = ({
</div>
);
};
export default RoleSelector;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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