From 849e6180cb6d11f5329623a2005a3b4bba5f40e8 Mon Sep 17 00:00:00 2001 From: Ben Lopata <42045469+bLopata@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:31:50 -0600 Subject: [PATCH 1/4] Adds refactors and dependency million to reduce re-renders. --- www/app/Chat.tsx | 529 ++++++----- www/components/markdownWrapper.tsx | 17 +- www/components/sidebar.tsx | 463 +++++----- www/next.config.mjs | 6 +- www/package.json | 1 + www/pnpm-lock.yaml | 1318 +++++++++++++++++++++++++++- 6 files changed, 1857 insertions(+), 477 deletions(-) diff --git a/www/app/Chat.tsx b/www/app/Chat.tsx index 9bf3edf..c30a694 100644 --- a/www/app/Chat.tsx +++ b/www/app/Chat.tsx @@ -6,7 +6,7 @@ import dynamic from 'next/dynamic'; import { FaLightbulb, FaPaperPlane } from 'react-icons/fa'; import Swal from 'sweetalert2'; -import { useRef, useEffect, useState, ElementRef, useMemo } from 'react'; +import { useRef, useEffect, useState, ElementRef, useMemo, memo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { usePostHog } from 'posthog-js/react'; @@ -29,9 +29,12 @@ const Thoughts = dynamic(() => import('@/components/thoughts'), { const MessageBox = dynamic(() => import('@/components/messagebox'), { ssr: false, }); +const MemoizedMessageBox = memo(MessageBox); + const Sidebar = dynamic(() => import('@/components/sidebar'), { ssr: false, }); +const MemoizedSidebar = memo(Sidebar); async function fetchStream( type: 'thought' | 'response' | 'honcho', @@ -129,11 +132,45 @@ export default function Chat({ const [thought, setThought] = useState(''); const [canSend, setCanSend] = useState(false); + const canUseApp = useMemo( + () => isSubscribed || freeMessages > 0, + [isSubscribed, freeMessages] + ); const posthog = usePostHog(); const input = useRef>(null); const messageContainerRef = useRef>(null); const [, scrollToBottom] = useAutoScroll(messageContainerRef); + const router = useRouter(); + + const firstChat = useMemo(() => { + // Check if there are no conversations or only one conversation with no messages + return ( + !initialConversations?.length || + (initialConversations.length === 1 && !initialMessages?.length) || + freeMessages === 50 + ); + }, [ + initialConversations?.length, + initialMessages?.length, + freeMessages, + ]); + + const defaultMessage: Message = useMemo( + () => ({ + content: + `${firstChat ? "I'm Bloom, your Aristotelian learning companion," : "Welcome back! I'm" + } here to guide your intellectual journey. + + The more we chat, the more I learn about you as a person. That helps me adapt to your interests and needs. + + What's on your mind? Let's dive in. 🌱`, + isUser: false, + id: '', + metadata: {}, + }), + [firstChat] + ); useEffect(() => { if (typeof window !== 'undefined') { @@ -144,14 +181,6 @@ export default function Chat({ } }, [posthog, initialUserId, initialEmail]); - const setIsThoughtsOpen = ( - isOpen: boolean, - messageId: string | null = null - ) => { - setIsThoughtsOpenState(isOpen); - setOpenThoughtMessageId(isOpen ? messageId : null); - }; - const conversationsFetcher = async () => { const result = await getConversations(); return result; @@ -224,15 +253,25 @@ export default function Chat({ }, }); - const handleReactionAdded = async (messageId: string, reaction: Reaction) => { - if (!userId || !conversationId) return; + const toggleSidebar = useCallback(() => { + setIsSidebarOpen((prev) => !prev); + }, []); - try { - await addOrRemoveReaction(conversationId, messageId, reaction); + const setIsThoughtsOpenCallback = useCallback( + (isOpen: boolean, messageId: string | null = null) => { + setIsThoughtsOpenState(isOpen); + setOpenThoughtMessageId(isOpen ? messageId : null); + }, + [] + ); - // Optimistically update the local data - mutateMessages( - (currentMessages) => { + const handleReactionAdded = useCallback( + async (messageId: string, reaction: Reaction) => { + if (!userId || !conversationId) return; + + try { + await addOrRemoveReaction(conversationId, messageId, reaction); + mutateMessages((currentMessages) => { if (!currentMessages) return currentMessages; return currentMessages.map((msg) => { if (msg.id === messageId) { @@ -246,188 +285,220 @@ export default function Chat({ } return msg; }); - }, - { revalidate: false } - ); - } catch (error) { - console.error('Failed to update reaction:', error); - } - }; - - async function chat(message?: string) { - const rawMessage = message || input.current?.value; - if (!userId || !rawMessage) return; - - // Process message to have double newline for markdown - const messageToSend = rawMessage.replace(/\n/g, '\n\n'); - - if (input.current) input.current.value = ''; + }, { revalidate: false }); + } catch (error) { + console.error('Failed to update reaction:', error); + } + }, + [userId, conversationId, mutateMessages] + ); - // Check free message allotment upfront if not subscribed - if (!isSubscribed) { - const currentCount = await getFreeMessageCount(userId); - if (currentCount <= 0) { + const chat = useCallback( + async (message?: string) => { + const rawMessage = message || input.current?.value; + if (!userId || !rawMessage) return; + if (!canUseApp) { + setCanSend(false); Swal.fire({ - title: 'Free Messages Depleted', - text: 'You have used all your free messages. Subscribe to continue using Bloom!', + title: 'Subscription Required', + text: 'You have no active subscription. Subscribe to continue using Bloom!', icon: 'warning', confirmButtonColor: '#3085d6', confirmButtonText: 'Subscribe', - showCancelButton: true, + showCancelButton: false, + }).then((result) => { + if (result.isConfirmed) { + router.push('/settings'); + } }); return; } - } - setCanSend(false); + // Process message to have double newline for markdown + const messageToSend = rawMessage.replace(/\n/g, '\n\n'); - const newMessages = [ - ...messages!, - { - content: messageToSend, - isUser: true, - id: '', - metadata: {}, - }, - { - content: '', - isUser: false, - id: '', - metadata: {}, - }, - ]; - await mutateMessages(newMessages, { revalidate: false }); - scrollToBottom(); + if (input.current) input.current.value = ''; - await new Promise((resolve) => setTimeout(resolve, 1000)); + setCanSend(false); - let thoughtReader: ReadableStreamDefaultReader | null = null; - let responseReader: ReadableStreamDefaultReader | null = null; + const newMessages = [ + ...messages!, + { + content: messageToSend, + isUser: true, + id: '', + metadata: {}, + }, + { + content: '', + isUser: false, + id: '', + metadata: {}, + }, + ]; + await mutateMessages(newMessages, { revalidate: false }); + scrollToBottom(); - try { - // Get thought stream - const thoughtStream = await fetchStream( - 'thought', - messageToSend, - conversationId! - ); - if (!thoughtStream) throw new Error('Failed to get thought stream'); + await new Promise((resolve) => setTimeout(resolve, 1000)); - thoughtReader = thoughtStream.getReader(); - let thoughtText = ''; - setThought(''); + let thoughtReader: ReadableStreamDefaultReader | null = null; + let responseReader: ReadableStreamDefaultReader | null = null; - // Process thought stream - while (true) { - const { done, value } = await thoughtReader.read(); - if (done) break; + try { + // Get thought stream + const thoughtStream = await fetchStream( + 'thought', + messageToSend, + conversationId! + ); + if (!thoughtStream) throw new Error('Failed to get thought stream'); - thoughtText += new TextDecoder().decode(value); - setThought(thoughtText); - } + thoughtReader = thoughtStream.getReader(); + let thoughtText = ''; + setThought(''); - // Cleanup thought stream - thoughtReader.releaseLock(); - thoughtReader = null; + // Process thought stream + while (true) { + const { done, value } = await thoughtReader.read(); + if (done) break; - const honchoResponse = await fetchStream( - 'honcho', - messageToSend, - conversationId!, - thoughtText - ); - const honchoContent = (await new Response( - honchoResponse - ).json()) as HonchoResponse; + thoughtText += new TextDecoder().decode(value); + setThought(thoughtText); + } - const pureThought = thoughtText; + // Cleanup thought stream + thoughtReader.releaseLock(); + thoughtReader = null; - thoughtText += - '\n\nHoncho Dialectic Response:\n\n' + honchoContent.content; - setThought(thoughtText); + const honchoResponse = await fetchStream( + 'honcho', + messageToSend, + conversationId!, + thoughtText + ); + const honchoContent = (await new Response( + honchoResponse + ).json()) as HonchoResponse; - await new Promise((resolve) => setTimeout(resolve, 2000)); + const pureThought = thoughtText; - // Get response stream using the thought and dialectic response - const responseStream = await fetchStream( - 'response', - messageToSend, - conversationId!, - pureThought, - honchoContent.content - ); - if (!responseStream) throw new Error('Failed to get response stream'); - - responseReader = responseStream.getReader(); - let currentModelOutput = ''; - - // Process response stream - while (true) { - const { done, value } = await responseReader.read(); - if (done) { - if (!isSubscribed) { - const success = await useFreeTrial(userId); - if (success) { - const newCount = await getFreeMessageCount(userId); - setFreeMessages(newCount); + thoughtText += + '\n\nHoncho Dialectic Response:\n\n' + honchoContent.content; + setThought(thoughtText); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Get response stream using the thought and dialectic response + const responseStream = await fetchStream( + 'response', + messageToSend, + conversationId!, + pureThought, + honchoContent.content + ); + if (!responseStream) throw new Error('Failed to get response stream'); + + responseReader = responseStream.getReader(); + let currentModelOutput = ''; + + // Process response stream + while (true) { + const { done, value } = await responseReader.read(); + if (done) { + if (!isSubscribed) { + const success = await useFreeTrial(userId); + if (success) { + const newCount = await getFreeMessageCount(userId); + setFreeMessages(newCount); + } } + break; } - break; + + currentModelOutput += new TextDecoder().decode(value); + + mutateMessages( + [ + ...(newMessages?.slice(0, -1) || []), + { + content: currentModelOutput, + isUser: false, + id: '', + metadata: {}, + }, + ], + { revalidate: false } + ); + + scrollToBottom(); } - currentModelOutput += new TextDecoder().decode(value); - - mutateMessages( - [ - ...(newMessages?.slice(0, -1) || []), - { - content: currentModelOutput, - isUser: false, - id: '', - metadata: {}, - }, - ], - { revalidate: false } - ); + responseReader.releaseLock(); + responseReader = null; + await mutateMessages(); scrollToBottom(); - } - - responseReader.releaseLock(); - responseReader = null; - - await mutateMessages(); - scrollToBottom(); - setCanSend(true); - } catch (error) { - console.error('Chat error:', error); - setCanSend(true); - await mutateMessages(); - scrollToBottom(); - } finally { - // Cleanup - if (thoughtReader) { - try { - thoughtReader.releaseLock(); - } catch (e) { - console.error('Error releasing thought reader:', e); + setCanSend(true); + } catch (error) { + console.error('Chat error:', error); + setCanSend(true); + await mutateMessages(); + scrollToBottom(); + } finally { + // Cleanup + if (thoughtReader) { + try { + thoughtReader.releaseLock(); + } catch (e) { + console.error('Error releasing thought reader:', e); + } } - } - if (responseReader) { - try { - responseReader.releaseLock(); - } catch (e) { - console.error('Error releasing response reader:', e); + if (responseReader) { + try { + responseReader.releaseLock(); + } catch (e) { + console.error('Error releasing response reader:', e); + } } } - } - } - - const canUseApp = useMemo( - () => isSubscribed || freeMessages > 0, - [isSubscribed, freeMessages] + }, + [ + userId, + canUseApp, + messages, + conversationId, + isSubscribed, + mutateMessages, + scrollToBottom, + router, + ] ); + const memoizedTextarea = useMemo(() => ( +