diff --git a/web/containers/Providers/ModelHandler.tsx b/web/containers/Providers/ModelHandler.tsx index 8c565bab1..42376c081 100644 --- a/web/containers/Providers/ModelHandler.tsx +++ b/web/containers/Providers/ModelHandler.tsx @@ -56,7 +56,7 @@ export default function ModelHandler() { const activeModel = useAtomValue(activeModelAtom) const setActiveModel = useSetAtom(activeModelAtom) const setStateModel = useSetAtom(stateModelAtom) - const [subscribedGeneratingMessage, setSubscribedGeneratingMessage] = useAtom( + const subscribedGeneratingMessage = useAtomValue( subscribedGeneratingMessageAtom ) const activeThread = useAtomValue(activeThreadAtom) diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index 4bf5a855e..379b2f966 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -1,7 +1,7 @@ import { Thread, ThreadContent, ThreadState } from '@janhq/core' import { atom } from 'jotai' -import { atomWithStorage } from 'jotai/utils' +import { atomWithStorage, selectAtom } from 'jotai/utils' import { ModelParams } from '@/types/model' @@ -34,6 +34,22 @@ export const threadStatesAtom = atomWithStorage>( {} ) +/** + * Returns whether there is a thread waiting for response or not + */ +const isWaitingForResponseAtom = selectAtom(threadStatesAtom, (threads) => + Object.values(threads).some((t) => t.waitingForResponse) +) + +/** + * Combine 2 states to reduce rerender + * 1. isWaitingForResponse + * 2. isGenerating + */ +export const isBlockingSendAtom = atom( + (get) => get(isWaitingForResponseAtom) || get(isGeneratingResponseAtom) +) + /** * Stores all threads for the current user */ diff --git a/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx index e05d36afb..c47d19d67 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx @@ -16,8 +16,7 @@ import EmptyThread from './EmptyThread' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { activeThreadAtom, - isGeneratingResponseAtom, - threadStatesAtom, + isBlockingSendAtom, } from '@/helpers/atoms/Thread.atom' const ChatConfigurator = memo(() => { @@ -65,12 +64,7 @@ const ChatBody = memo( const prevScrollTop = useRef(0) const isUserManuallyScrollingUp = useRef(false) const currentThread = useAtomValue(activeThreadAtom) - const threadStates = useAtomValue(threadStatesAtom) - const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom) - - const isStreamingResponse = Object.values(threadStates).some( - (threadState) => threadState.waitingForResponse - ) + const isBlockingSend = useAtomValue(isBlockingSendAtom) const count = useMemo( () => (messages?.length ?? 0) + (loadModelError ? 1 : 0), @@ -87,35 +81,11 @@ const ChatBody = memo( useEffect(() => { isUserManuallyScrollingUp.current = false - if (parentRef.current) { + if (parentRef.current && isBlockingSend) { parentRef.current.scrollTo({ top: parentRef.current.scrollHeight }) virtualizer.scrollToIndex(count - 1) } - }, [count, virtualizer]) - - useEffect(() => { - isUserManuallyScrollingUp.current = false - if (parentRef.current && isGeneratingResponse) { - parentRef.current.scrollTo({ top: parentRef.current.scrollHeight }) - virtualizer.scrollToIndex(count - 1) - } - }, [count, virtualizer, isGeneratingResponse]) - - useEffect(() => { - isUserManuallyScrollingUp.current = false - if (parentRef.current && isGeneratingResponse) { - parentRef.current.scrollTo({ top: parentRef.current.scrollHeight }) - virtualizer.scrollToIndex(count - 1) - } - }, [count, virtualizer, isGeneratingResponse, currentThread?.id]) - - useEffect(() => { - isUserManuallyScrollingUp.current = false - if (parentRef.current) { - parentRef.current.scrollTo({ top: parentRef.current.scrollHeight }) - virtualizer.scrollToIndex(count - 1) - } - }, [count, currentThread?.id, virtualizer]) + }, [count, virtualizer, isBlockingSend, currentThread?.id]) const items = virtualizer.getVirtualItems() @@ -124,7 +94,7 @@ const ChatBody = memo( _, instance ) => { - if (isUserManuallyScrollingUp.current === true && isStreamingResponse) + if (isUserManuallyScrollingUp.current === true && isBlockingSend) return false return ( // item.start < (instance.scrollOffset ?? 0) && @@ -136,7 +106,7 @@ const ChatBody = memo( (event: React.UIEvent) => { const currentScrollTop = event.currentTarget.scrollTop - if (prevScrollTop.current > currentScrollTop && isStreamingResponse) { + if (prevScrollTop.current > currentScrollTop && isBlockingSend) { isUserManuallyScrollingUp.current = true } else { const currentScrollTop = event.currentTarget.scrollTop @@ -154,7 +124,7 @@ const ChatBody = memo( } prevScrollTop.current = currentScrollTop }, - [isStreamingResponse] + [isBlockingSend] ) return ( diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx index 24499bd30..990d24c7a 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx @@ -35,22 +35,19 @@ import RichTextEditor from './RichTextEditor' import { showRightPanelAtom } from '@/helpers/atoms/App.atom' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' -import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { spellCheckAtom } from '@/helpers/atoms/Setting.atom' import { activeSettingInputBoxAtom, activeThreadAtom, getActiveThreadIdAtom, - isGeneratingResponseAtom, - threadStatesAtom, + isBlockingSendAtom, } from '@/helpers/atoms/Thread.atom' import { activeTabThreadRightPanelAtom } from '@/helpers/atoms/ThreadRightPanel.atom' const ChatInput = () => { const activeThread = useAtomValue(activeThreadAtom) const { stateModel } = useActiveModel() - const messages = useAtomValue(getCurrentChatMessagesAtom) const spellCheck = useAtomValue(spellCheckAtom) const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom) @@ -67,8 +64,7 @@ const ChatInput = () => { const fileInputRef = useRef(null) const imageInputRef = useRef(null) const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom) - const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom) - const threadStates = useAtomValue(threadStatesAtom) + const isBlockingSend = useAtomValue(isBlockingSendAtom) const activeAssistant = useAtomValue(activeAssistantAtom) const { stopInference } = useActiveModel() @@ -77,10 +73,6 @@ const ChatInput = () => { activeTabThreadRightPanelAtom ) - const isStreamingResponse = Object.values(threadStates).some( - (threadState) => threadState.waitingForResponse - ) - const refAttachmentMenus = useClickOutside(() => setShowAttacmentMenus(false)) const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom) @@ -302,7 +294,7 @@ const ChatInput = () => { )} - {!isGeneratingResponse && !isStreamingResponse ? ( + {!isBlockingSend ? ( <> {currentPrompt.length !== 0 && (