From ffcae0f5ebd85df32f187f82659aabd175b6551c Mon Sep 17 00:00:00 2001 From: maskoff7 Date: Wed, 11 Sep 2024 21:10:48 +0300 Subject: [PATCH] Optimize React components and hooks for better performance - ChatConversation: Memoize isLoadingMessage calculation and onSubmit function - OllamaModelsRepository: Memoize columns array and handleSortingChange function - ChatLayout: Memoize refetchInterval, activesInboxes, and archivesInboxes - useWebSocketMessage: Optimize useEffect dependencies and memoize wsMessage - SheetProject: Memoize table object and optimize WebSocket useEffect dependencies These changes aim to reduce unnecessary re-renders and calculations, potentially improving the overall performance of the application. This commit focuses on improving the performance of several key components and hooks in our application by implementing React optimization techniques. ChatConversation: - Memoized the isLoadingMessage calculation using useMemo to prevent unnecessary recalculations on re-renders. - Wrapped the onSubmit function with useCallback to ensure it's not recreated on every render, potentially reducing unnecessary re-renders of child components. - Optimized the useEffect for setting firstMessageWorkflow by using a more specific dependency array. OllamaModelsRepository: - Memoized the columns array using useMemo to prevent recreation on each render. - Implemented useCallback for the handleSortingChange function to optimize performance when passed as a prop to child components. - Split the useEffect for installedOllamaModelsMap and selectedTagMap setup into separate effects for better separation of concerns. ChatLayout: - Memoized the refetchInterval function using useCallback to prevent unnecessary recreations. - Implemented useMemo for activesInboxes and archivesInboxes calculations to optimize performance, especially beneficial if the inboxes array is large. useWebSocketMessage: - Optimized the useEffect dependencies by only including necessary parts of the auth object, reducing unnecessary effect runs. - Memoized the WebSocket message creation using useMemo to prevent unnecessary recalculations. SheetProject: - Memoized the table object using useMemo to prevent unnecessary recreation on each render, potentially improving performance in data-heavy scenarios. - Optimized the WebSocket useEffect dependencies by only including necessary parts of the auth object. These optimizations aim to reduce unnecessary re-renders, calculations, and effect runs throughout the application. By implementing these React best practices, we expect to see improved performance, especially in scenarios with large datasets or frequent updates.This commit focuses on improving the performance of several key components and hooks in our application by implementing React optimization techniques. ChatConversation: - Memoized the isLoadingMessage calculation using useMemo to prevent unnecessary recalculations on re-renders. - Wrapped the onSubmit function with useCallback to ensure it's not recreated on every render, potentially reducing unnecessary re-renders of child components. - Optimized the useEffect for setting firstMessageWorkflow by using a more specific dependency array. OllamaModelsRepository: - Memoized the columns array using useMemo to prevent recreation on each render. - Implemented useCallback for the handleSortingChange function to optimize performance when passed as a prop to child components. - Split the useEffect for installedOllamaModelsMap and selectedTagMap setup into separate effects for better separation of concerns. ChatLayout: - Memoized the refetchInterval function using useCallback to prevent unnecessary recreations. - Implemented useMemo for activesInboxes and archivesInboxes calculations to optimize performance, especially beneficial if the inboxes array is large. useWebSocketMessage: - Optimized the useEffect dependencies by only including necessary parts of the auth object, reducing unnecessary effect runs. - Memoized the WebSocket message creation using useMemo to prevent unnecessary recalculations. SheetProject: - Memoized the table object using useMemo to prevent unnecessary recreation on each render, potentially improving performance in data-heavy scenarios. - Optimized the WebSocket useEffect dependencies by only including necessary parts of the auth object. These optimizations aim to reduce unnecessary re-renders, calculations, and effect runs throughout the application. By implementing these React best practices, we expect to see improved performance, especially in scenarios with large datasets or frequent updates. --- .../components/ollama-models-repository.tsx | 18 + .../src/pages/chat/chat-conversation.tsx | 477 ++++++++++++++++-- .../shinkai-desktop/src/pages/chat/layout.tsx | 37 +- .../src/pages/sheet-project.tsx | 56 +- 4 files changed, 486 insertions(+), 102 deletions(-) diff --git a/apps/shinkai-desktop/src/components/shinkai-node-manager/components/ollama-models-repository.tsx b/apps/shinkai-desktop/src/components/shinkai-node-manager/components/ollama-models-repository.tsx index abe02b445..99d96ff37 100644 --- a/apps/shinkai-desktop/src/components/shinkai-node-manager/components/ollama-models-repository.tsx +++ b/apps/shinkai-desktop/src/components/shinkai-node-manager/components/ollama-models-repository.tsx @@ -241,6 +241,24 @@ export const OllamaModelsRepository = ({ return `${model}:${tag}`; }; + useEffect(() => { + if (installedOllamaModels) { + installedOllamaModels.models.forEach((model) => { + installedOllamaModelsMap.set(model.name, model); + }); + } + }, [installedOllamaModels, installedOllamaModelsMap]); + + useEffect(() => { + FILTERED_OLLAMA_MODELS_REPOSITORY.forEach((model) => { + selectedTagMap.set(model.name, model.tags[0]); + }); + }, [selectedTagMap]); + + const handleSortingChange = useCallback((updatedSorting: SortingState) => { + setSorting(updatedSorting); + }, []); + return (
{ const [messageContent, setMessageContent] = useState(''); useEffect(() => { - if (!enabled) return; - if (lastMessage?.data) { - try { - const parseData: WsMessage = JSON.parse(lastMessage.data); - if (parseData.message_type !== 'Stream') return; - if (parseData.metadata?.is_done) { - const paginationKey = [ - FunctionKey.GET_CHAT_CONVERSATION_PAGINATION, - { - nodeAddress: auth?.node_address ?? '', - inboxId: inboxId as string, - shinkaiIdentity: auth?.shinkai_identity ?? '', - profile: auth?.profile ?? '', - }, - ]; - queryClient.invalidateQueries({ queryKey: paginationKey }); - } - - setMessageContent((prev) => prev + parseData.message); - return; - } catch (error) { - console.error('Failed to parse ws message', error); + if (!enabled || !lastMessage?.data) return; + try { + const parseData: WsMessage = JSON.parse(lastMessage.data); + if (parseData.message_type !== 'Stream') return; + if (parseData.metadata?.is_done) { + const paginationKey = [ + FunctionKey.GET_CHAT_CONVERSATION_PAGINATION, + { + nodeAddress: auth?.node_address ?? '', + inboxId: inboxId as string, + shinkaiIdentity: auth?.shinkai_identity ?? '', + profile: auth?.profile ?? '', + }, + ]; + queryClient.invalidateQueries({ queryKey: paginationKey }); } + + setMessageContent((prev) => prev + parseData.message); + } catch (error) { + console.error('Failed to parse ws message', error); } - }, [ - auth?.my_device_encryption_sk, - auth?.my_device_identity_sk, - auth?.node_address, - auth?.node_encryption_pk, - auth?.profile, - auth?.profile_encryption_sk, - auth?.profile_identity_sk, - auth?.shinkai_identity, - enabled, - inboxId, - lastMessage?.data, - queryClient, - ]); + }, [enabled, lastMessage?.data, auth?.node_address, auth?.shinkai_identity, auth?.profile, inboxId, queryClient]); - useEffect(() => { - if (!enabled) return; - const wsMessage = { + const wsMessage = useMemo(() => { + if (!enabled) return null; + const message = { subscriptions: [{ topic: 'inbox', subtopic: inboxId }], unsubscriptions: [], }; - const wsMessageString = JSON.stringify(wsMessage); - const shinkaiMessage = ShinkaiMessageBuilderWrapper.ws_connection( - wsMessageString, + return ShinkaiMessageBuilderWrapper.ws_connection( + JSON.stringify(message), auth?.profile_encryption_sk ?? '', auth?.profile_identity_sk ?? '', auth?.node_encryption_pk ?? '', @@ -233,21 +216,409 @@ const useWebSocketMessage = ({ enabled }: UseWebSocketMessage) => { auth?.shinkai_identity ?? '', '', ); - sendMessage(shinkaiMessage); - }, [ - auth?.node_encryption_pk, - auth?.profile, - auth?.profile_encryption_sk, - auth?.profile_identity_sk, - auth?.shinkai_identity, - enabled, - inboxId, - sendMessage, - ]); + }, [enabled, inboxId, auth]); + + useEffect(() => { + if (wsMessage) { + sendMessage(wsMessage); + } + }, [wsMessage, sendMessage]); return { messageContent, readyState, setMessageContent }; }; +const ChatConversation = () => { + const { captureAnalyticEvent } = useAnalytics(); + const { t } = useTranslation(); + const size = partial({ standard: 'jedec' }); + const { inboxId: encodedInboxId = '' } = useParams(); + const auth = useAuth((state) => state.auth); + const fromPreviousMessagesRef = useRef(false); + + const inboxId = decodeURIComponent(encodedInboxId); + const currentInbox = useGetCurrentInbox(); + const hasProviderEnableStreaming = + currentInbox?.agent?.model.split(':')?.[0] === Models.Ollama || + currentInbox?.agent?.model.split(':')?.[0] === Models.Gemini || + currentInbox?.agent?.model.split(':')?.[0] === Models.Exo; + + const hasProviderEnableTools = + currentInbox?.agent?.model.split(':')?.[0] === Models.OpenAI; + + const chatForm = useForm({ + resolver: zodResolver(chatMessageFormSchema), + defaultValues: { + message: '', + }, + }); + + const workflowSelected = useWorkflowSelectionStore( + (state) => state.workflowSelected, + ); + const setWorkflowSelected = useWorkflowSelectionStore( + (state) => state.setWorkflowSelected, + ); + + const currentMessage = useWatch({ + control: chatForm.control, + name: 'message', + }); + const debounceMessage = useDebounce(currentMessage, 500); + + const { getRootProps: getRootFileProps, getInputProps: getInputFileProps } = + useDropzone({ + multiple: false, + onDrop: (acceptedFiles) => { + const file = acceptedFiles[0]; + chatForm.setValue('file', file, { shouldValidate: true }); + }, + }); + + const currentFile = useWatch({ + control: chatForm.control, + name: 'file', + }); + + const { + data, + fetchPreviousPage, + hasPreviousPage, + isPending: isChatConversationLoading, + isFetchingPreviousPage, + isSuccess: isChatConversationSuccess, + } = useGetChatConversationWithPagination({ + token: auth?.api_v2_key ?? '', + nodeAddress: auth?.node_address ?? '', + inboxId: inboxId as string, + shinkaiIdentity: auth?.shinkai_identity ?? '', + profile: auth?.profile ?? '', + refetchIntervalEnabled: !hasProviderEnableStreaming, + }); + + const { + data: workflowRecommendations, + isSuccess: isWorkflowRecommendationsSuccess, + } = useGetWorkflowSearch( + { + nodeAddress: auth?.node_address ?? '', + token: auth?.api_v2_key ?? '', + search: debounceMessage, + }, + { + enabled: !!debounceMessage && !!currentMessage, + select: (data) => data.slice(0, 3), + }, + ); + + const [firstMessageWorkflow, setFirstMessageWorkflow] = useState<{ + name: string; + author: string; + tool_router_key: string; + } | null>(null); + + useEffect(() => { + const firstMessage = data?.pages[0]?.[0]; + if (firstMessage?.workflowName) { + const [name, author] = firstMessage.workflowName.split(':::'); + setFirstMessageWorkflow({ + name, + author, + tool_router_key: firstMessage.workflowName, + }); + } + }, [data?.pages[0]?.[0]?.workflowName]); + + const isLoadingMessage = useMemo(() => { + const lastMessage = data?.pages?.at(-1)?.at(-1); + return isJobInbox(inboxId) && lastMessage?.isLocal; + }, [data?.pages, inboxId]); + + const { messageContent, setMessageContent } = useWebSocketMessage({ + enabled: hasProviderEnableStreaming, + }); + + const { widgetTool, setWidgetTool } = useWebSocketTools({ + enabled: hasProviderEnableTools, + }); + + const { mutateAsync: sendMessageToInbox } = useSendMessageToInbox(); + const { mutateAsync: sendMessageToJob } = useSendMessageToJob({ + onSuccess: () => { + captureAnalyticEvent('AI Chat', undefined); + }, + }); + const { mutateAsync: sendTextMessageWithFilesForInbox } = + useSendMessageWithFilesToInbox({ + onSuccess: () => { + captureAnalyticEvent('AI Chat with Files', { + filesCount: 1, + }); + }, + }); + + const regenerateMessage = async ( + content: string, + parentHash: string, + workflowName?: string, + ) => { + setMessageContent(''); // trick to clear the ws stream message + if (!auth) return; + const decodedInboxId = decodeURIComponent(inboxId); + const jobId = extractJobIdFromInbox(decodedInboxId); + + await sendMessageToJob({ + nodeAddress: auth.node_address, + token: auth.api_v2_key, + jobId, + message: content, + parent: parentHash, + workflowName, + }); + }; + + const onSubmit = useCallback(async (data: ChatMessageFormSchema) => { + setMessageContent(''); // trick to clear the ws stream message + if (!auth || data.message.trim() === '') return; + fromPreviousMessagesRef.current = false; + + let workflowKeyToUse = workflowSelected?.tool_router_key; + if (!workflowKeyToUse && firstMessageWorkflow) { + workflowKeyToUse = firstMessageWorkflow.tool_router_key; + } + + if (data.file) { + await sendTextMessageWithFilesForInbox({ + nodeAddress: auth?.node_address ?? '', + sender: auth.shinkai_identity, + senderSubidentity: auth.profile, + receiver: auth.shinkai_identity, + message: data.message, + inboxId: inboxId, + files: [currentFile], + workflowName: workflowKeyToUse, + my_device_encryption_sk: auth.my_device_encryption_sk, + my_device_identity_sk: auth.my_device_identity_sk, + node_encryption_pk: auth.node_encryption_pk, + profile_encryption_sk: auth.profile_encryption_sk, + profile_identity_sk: auth.profile_identity_sk, + }); + chatForm.reset(); + return; + } + + if (isJobInbox(inboxId)) { + const jobId = extractJobIdFromInbox(inboxId); + + await sendMessageToJob({ + token: auth.api_v2_key, + nodeAddress: auth.node_address, + jobId: jobId, + message: data.message, + parent: '', // Note: we should set the parent if we want to retry or branch out + workflowName: workflowKeyToUse, + }); + } else { + const sender = `${auth.shinkai_identity}/${auth.profile}/device/${auth.registration_name}`; + const receiver = extractReceiverShinkaiName(inboxId, sender); + await sendMessageToInbox({ + nodeAddress: auth?.node_address ?? '', + sender: auth.shinkai_identity, + sender_subidentity: `${auth.profile}/device/${auth.registration_name}`, + receiver, + message: data.message, + inboxId: inboxId, + my_device_encryption_sk: auth.my_device_encryption_sk, + my_device_identity_sk: auth.my_device_identity_sk, + node_encryption_pk: auth.node_encryption_pk, + profile_encryption_sk: auth.profile_encryption_sk, + profile_identity_sk: auth.profile_identity_sk, + }); + } + chatForm.reset(); + setWorkflowSelected(undefined); + }, [auth, inboxId, workflowSelected, firstMessageWorkflow, currentFile, sendTextMessageWithFilesForInbox, sendMessageToJob, sendMessageToInbox, chatForm, setWorkflowSelected]); + + useEffect(() => { + chatForm.reset(); + setWorkflowSelected(undefined); + }, [chatForm, inboxId]); + + const isLimitReachedErrorLastMessage = useMemo(() => { + const lastMessage = data?.pages?.at(-1)?.at(-1); + if (!lastMessage) return; + const errorCode = extractErrorPropertyOrContent( + lastMessage.content, + 'error', + ); + return errorCode === ErrorCodes.ShinkaiBackendInferenceLimitReached; + }, [data?.pages]); + + const isWorkflowSelectedAndFilesPresent = + workflowSelected && currentFile !== undefined; + + useEffect(() => { + if (isWorkflowSelectedAndFilesPresent) { + chatForm.setValue( + 'message', + `${formatText(workflowSelected.name)} - ${workflowSelected.description}`, + ); + } + }, [chatForm, isWorkflowSelectedAndFilesPresent, workflowSelected]); + + return ( +
+ + { + setWidgetTool(null); + }} + /> + } + noMoreMessageLabel={t('chat.allMessagesLoaded')} + paginatedMessages={data} + regenerateMessage={regenerateMessage} + /> + {isLimitReachedErrorLastMessage && ( + + + + {t('chat.limitReachedTitle')} + + +
+ {t('chat.limitReachedDescription')} +
+
+
+ )} + + {!isLimitReachedErrorLastMessage && ( +
+
+
+ ( + + + {t('chat.enterMessage')} + + +
+
+ + + + +
+ + +
+
+ + + {t('common.uploadFile')} + + +
+
+ +
+ + + + {t('chat.sendMessage')} + + + } + disabled={ + isLoadingMessage || + isWorkflowSelectedAndFilesPresent + } + // isLoading={isLoadingMessage} + onChange={field.onChange} + onSubmit={chatForm.handleSubmit(onSubmit)} + topAddons={ + <> + {workflowSelected && ( +
+ + + +
+ +
+ + {formatText( + workflowSelected.name, + )}{' '} + + -{' '} + + {workflowSelected.description} + +
+
+
+ + + {workflowSelected.description} + + +
+
+ +
+ )} const ChatConversation = () => { const { captureAnalyticEvent } = useAnalytics(); const { t } = useTranslation(); diff --git a/apps/shinkai-desktop/src/pages/chat/layout.tsx b/apps/shinkai-desktop/src/pages/chat/layout.tsx index 7495581e4..a77b25db6 100644 --- a/apps/shinkai-desktop/src/pages/chat/layout.tsx +++ b/apps/shinkai-desktop/src/pages/chat/layout.tsx @@ -299,32 +299,25 @@ const ChatLayout = () => { const { t } = useTranslation(); const auth = useAuth((state) => state.auth); const navigate = useNavigate(); - const { inboxes, isPending, isSuccess } = useGetInboxes( - { nodeAddress: auth?.node_address ?? '', token: auth?.api_v2_key ?? '' }, + + const refetchInterval = useCallback(() => { + return isInboxesLoading ? false : 5000; + }, [isInboxesLoading]); + + const { data: inboxes = [], isPending, isSuccess } = useGetInboxes( + { + nodeAddress: auth?.node_address ?? '', + shinkaiIdentity: auth?.shinkai_identity ?? '', + profile: auth?.profile ?? '', + }, { - refetchIntervalInBackground: true, - refetchInterval: (query) => { - const allInboxesAreCompleted = query.state.data?.every((inbox) => { - return ( - inbox.last_message && - !( - inbox.last_message.sender === auth?.shinkai_identity && - inbox.last_message.sender_subidentity === auth.profile - ) - ); - }); - return allInboxesAreCompleted ? 0 : 3000; - }, + enabled: !!auth, + refetchInterval, }, ); - const activesInboxes = useMemo(() => { - return inboxes?.filter((inbox) => !inbox.is_finished); - }, [inboxes]); - - const archivesInboxes = useMemo(() => { - return inboxes?.filter((inbox) => inbox.is_finished); - }, [inboxes]); + const activesInboxes = useMemo(() => inboxes.filter((inbox) => !inbox.is_finished), [inboxes]); + const archivesInboxes = useMemo(() => inboxes.filter((inbox) => inbox.is_finished), [inboxes]); return (
diff --git a/apps/shinkai-desktop/src/pages/sheet-project.tsx b/apps/shinkai-desktop/src/pages/sheet-project.tsx index 9f0bd8bd0..a6b46f874 100644 --- a/apps/shinkai-desktop/src/pages/sheet-project.tsx +++ b/apps/shinkai-desktop/src/pages/sheet-project.tsx @@ -207,33 +207,35 @@ const SheetProject = () => { [sheetInfo?.columns, sheetInfo?.display_columns], ); - const table = useReactTable({ - data, - columns, - state: { - columnVisibility, - columnFilters, - rowSelection, - sorting, - }, - columnResizeMode: 'onChange', - columnResizeDirection: 'ltr', - enableRowSelection: true, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - onColumnVisibilityChange: setColumnVisibility, - getFilteredRowModel: getFilteredRowModel(), - // getPaginationRowModel: getPaginationRowModel(), // enable if we have pagination - onColumnFiltersChange: setColumnFilters, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - defaultColumn: { - size: 200, - minSize: 50, - maxSize: 500, - }, - getRowId: (row) => row.rowId, - }); + const table = useMemo(() => { + return useReactTable({ + data, + columns, + state: { + columnVisibility, + columnFilters, + rowSelection, + sorting, + }, + columnResizeMode: 'onChange', + columnResizeDirection: 'ltr', + enableRowSelection: true, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onColumnVisibilityChange: setColumnVisibility, + getFilteredRowModel: getFilteredRowModel(), + // getPaginationRowModel: getPaginationRowModel(), // enable if we have pagination + onColumnFiltersChange: setColumnFilters, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + defaultColumn: { + size: 200, + minSize: 50, + maxSize: 500, + }, + getRowId: (row) => row.rowId, + }); + }, [data, columns, columnVisibility, columnFilters, rowSelection, sorting]); const { rows } = table.getRowModel(); const leafColumns = table.getVisibleLeafColumns();