diff --git a/www/app/Chat.tsx b/www/app/Chat.tsx index d2a6e9b..5842f4e 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, useCallback, memo } from 'react'; // import { useRouter } from 'next/navigation'; import { usePostHog } from 'posthog-js/react'; @@ -31,9 +31,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', @@ -130,6 +133,10 @@ 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); @@ -172,14 +179,6 @@ What\'s on your mind? Let\'s dive in. 🌱`, } }, [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; @@ -252,15 +251,25 @@ What\'s on your mind? Let\'s dive in. 🌱`, }, }); - 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); + }, + [] + ); + + const handleReactionAdded = useCallback( + async (messageId: string, reaction: Reaction) => { + if (!userId || !conversationId) return; - // Optimistically update the local data - mutateMessages( - (currentMessages) => { + try { + await addOrRemoveReaction(conversationId, messageId, reaction); + mutateMessages((currentMessages) => { if (!currentMessages) return currentMessages; return currentMessages.map((msg) => { if (msg.id === messageId) { @@ -274,15 +283,16 @@ What\'s on your mind? Let\'s dive in. 🌱`, } return msg; }); - }, - { revalidate: false } - ); - } catch (error) { - console.error('Failed to update reaction:', error); - } - }; + }, { revalidate: false }); + } catch (error) { + console.error('Failed to update reaction:', error); + } + }, + [userId, conversationId, mutateMessages] + ); - async function chat(message?: string) { + const chat = useCallback( + async (message?: string) => { const rawMessage = message || input.current?.value; if (!userId || !rawMessage) return; @@ -291,7 +301,23 @@ What\'s on your mind? Let\'s dive in. 🌱`, if (input.current) input.current.value = ''; - setCanSend(false); + // Check free message allotment upfront if not subscribed + if (!isSubscribed) { + const currentCount = await getFreeMessageCount(userId); + if (currentCount <= 0) { + Swal.fire({ + title: 'Free Messages Depleted', + text: 'You have used all your free messages. Subscribe to continue using Bloom!', + icon: 'warning', + confirmButtonColor: '#3085d6', + confirmButtonText: 'Subscribe', + showCancelButton: true, + }); + return; + } + } + + setCanSend(false); const newMessages = [ ...messages!, @@ -311,10 +337,10 @@ What\'s on your mind? Let\'s dive in. 🌱`, await mutateMessages(newMessages, { revalidate: false }); messageListRef.current?.scrollToBottom(); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); - let thoughtReader: ReadableStreamDefaultReader | null = null; - let responseReader: ReadableStreamDefaultReader | null = null; + let thoughtReader: ReadableStreamDefaultReader | null = null; + let responseReader: ReadableStreamDefaultReader | null = null; try { // Get thought stream @@ -328,22 +354,22 @@ What\'s on your mind? Let\'s dive in. 🌱`, throw new Error('Failed to get thought stream'); } - thoughtReader = thoughtStream.getReader(); - let thoughtText = ''; - setThought(''); + thoughtReader = thoughtStream.getReader(); + let thoughtText = ''; + setThought(''); - // Process thought stream - while (true) { - const { done, value } = await thoughtReader.read(); - if (done) break; + // Process thought stream + while (true) { + const { done, value } = await thoughtReader.read(); + if (done) break; - thoughtText += new TextDecoder().decode(value); - setThought(thoughtText); - } + thoughtText += new TextDecoder().decode(value); + setThought(thoughtText); + } - // Cleanup thought stream - thoughtReader.releaseLock(); - thoughtReader = null; + // Cleanup thought stream + thoughtReader.releaseLock(); + thoughtReader = null; const honchoResponse = await fetchStream( 'honcho', @@ -356,61 +382,61 @@ What\'s on your mind? Let\'s dive in. 🌱`, honchoResponse ).json()) as HonchoResponse; - const pureThought = thoughtText; + const pureThought = thoughtText; - thoughtText += - '\n\nHoncho Dialectic Response:\n\n' + honchoContent.content; - setThought(thoughtText); + thoughtText += + '\n\nHoncho Dialectic Response:\n\n' + honchoContent.content; + setThought(thoughtText); - await new Promise((resolve) => setTimeout(resolve, 2000)); + 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); + // 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 } - ); + currentModelOutput += new TextDecoder().decode(value); + + mutateMessages( + [ + ...(newMessages?.slice(0, -1) || []), + { + content: currentModelOutput, + isUser: false, + id: '', + metadata: {}, + }, + ], + { revalidate: false } + ); messageListRef.current?.scrollToBottom(); } - responseReader.releaseLock(); - responseReader = null; + responseReader.releaseLock(); + responseReader = null; await mutateMessages(); messageListRef.current?.scrollToBottom(); @@ -437,12 +463,7 @@ What\'s on your mind? Let\'s dive in. 🌱`, } } } - } - - const canUseApp = useMemo( - () => isSubscribed || freeMessages > 0, - [isSubscribed, freeMessages] - ); + }, [userId, conversationId, messages, mutateMessages, messageListRef, setThought, setIsThoughtsOpenCallback, handleReactionAdded]); useEffect(() => { if (conversationId?.startsWith('temp-') || messagesLoading) { @@ -452,9 +473,63 @@ What\'s on your mind? Let\'s dive in. 🌱`, } }, [conversationId, messagesLoading]); + const messageBoxProps = useMemo( + () => ({ + userId, + loading: messagesLoading, + conversationId, + setThought, + setIsThoughtsOpen: setIsThoughtsOpenCallback, + onReactionAdded: handleReactionAdded, + }), + [ + userId, + messagesLoading, + conversationId, + setThought, + setIsThoughtsOpenCallback, + handleReactionAdded, + ] + ); + + const emptyMessage = useMemo( + () => ({ + content: '', + id: '', + isUser: false, + metadata: { reaction: null }, + }), + [] + ); + + const memoizedTextarea = useMemo(() => ( +