From 3a68f29c0f49c1835c81956bc7f8ae3721c511ad Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 27 Nov 2024 22:48:41 +0700 Subject: [PATCH] fix: app re-render issues caused by bad state handling --- web/containers/Layout/index.tsx | 27 ++------ web/containers/MainViewContainer/index.tsx | 28 +++++++- web/containers/Providers/CoreConfigurator.tsx | 64 +++++++++++++++++++ web/containers/Providers/index.tsx | 59 ++--------------- web/helpers/atoms/AppConfig.atom.ts | 33 ++++++++-- web/helpers/atoms/Model.atom.ts | 8 ++- web/helpers/atoms/Setting.atom.ts | 18 +++++- web/helpers/atoms/Thread.atom.ts | 4 +- web/hooks/useStarterScreen.ts | 31 +++++---- .../ChatBody/OnDeviceStarterScreen/index.tsx | 14 ++-- .../ThreadCenterPanel/ChatBody/index.tsx | 4 +- .../Thread/ThreadCenterPanel/index.tsx | 4 +- web/screens/Thread/index.tsx | 36 +++++++---- 13 files changed, 198 insertions(+), 132 deletions(-) create mode 100644 web/containers/Providers/CoreConfigurator.tsx diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx index 8a3f417f44..e787163d48 100644 --- a/web/containers/Layout/index.tsx +++ b/web/containers/Layout/index.tsx @@ -1,10 +1,8 @@ 'use client' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' -import { motion as m } from 'framer-motion' - -import { useAtom, useAtomValue } from 'jotai' +import { useAtomValue, useSetAtom } from 'jotai' import { twMerge } from 'tailwind-merge' @@ -36,7 +34,7 @@ import { mainViewStateAtom } from '@/helpers/atoms/App.atom' import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom' const BaseLayout = () => { - const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom) + const setMainViewState = useSetAtom(mainViewStateAtom) const importModelStage = useAtomValue(getImportModelStageAtom) const reduceTransparent = useAtomValue(reduceTransparentAtom) @@ -68,24 +66,7 @@ const BaseLayout = () => {
-
-
- - - -
-
+ {importModelStage === 'SELECTING_MODEL' && } {importModelStage === 'MODEL_SELECTED' && } diff --git a/web/containers/MainViewContainer/index.tsx b/web/containers/MainViewContainer/index.tsx index 4f3b4986a9..ba7f87fd2b 100644 --- a/web/containers/MainViewContainer/index.tsx +++ b/web/containers/MainViewContainer/index.tsx @@ -1,5 +1,10 @@ +import { memo } from 'react' + +import { motion as m } from 'framer-motion' import { useAtomValue } from 'jotai' +import { twMerge } from 'tailwind-merge' + import { MainViewState } from '@/constants/screens' import HubScreen from '@/screens/Hub' @@ -31,7 +36,26 @@ const MainViewContainer = () => { break } - return children + return ( +
+
+ + {children} + +
+
+ ) } -export default MainViewContainer +export default memo(MainViewContainer) diff --git a/web/containers/Providers/CoreConfigurator.tsx b/web/containers/Providers/CoreConfigurator.tsx new file mode 100644 index 0000000000..8af31162de --- /dev/null +++ b/web/containers/Providers/CoreConfigurator.tsx @@ -0,0 +1,64 @@ +'use client' + +import { PropsWithChildren, useCallback, useEffect, useState } from 'react' + +import Loader from '@/containers/Loader' + +import { setupCoreServices } from '@/services/coreService' +import { + isCoreExtensionInstalled, + setupBaseExtensions, +} from '@/services/extensionService' + +import { extensionManager } from '@/extension' + +export const CoreConfigurator = ({ children }: PropsWithChildren) => { + const [setupCore, setSetupCore] = useState(false) + const [activated, setActivated] = useState(false) + const [settingUp, setSettingUp] = useState(false) + + const setupExtensions = useCallback(async () => { + // Register all active extensions + await extensionManager.registerActive() + + setTimeout(async () => { + if (!isCoreExtensionInstalled()) { + setSettingUp(true) + await setupBaseExtensions() + return + } + + extensionManager.load() + setSettingUp(false) + setActivated(true) + }, 500) + }, []) + + // Services Setup + useEffect(() => { + setupCoreServices() + setSetupCore(true) + return () => { + extensionManager.unload() + } + }, []) + + useEffect(() => { + if (setupCore) { + // Electron + if (window && window.core?.api) { + setupExtensions() + } else { + // Host + setActivated(true) + } + } + }, [setupCore, setupExtensions]) + + return ( + <> + {settingUp && } + {setupCore && activated && <>{children}} + + ) +} diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index f9e240b948..67778e30c4 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -1,23 +1,17 @@ 'use client' -import { PropsWithChildren, useCallback, useEffect, useState } from 'react' +import { PropsWithChildren } from 'react' import { Toaster } from 'react-hot-toast' -import Loader from '@/containers/Loader' import EventListener from '@/containers/Providers/EventListener' import JotaiWrapper from '@/containers/Providers/Jotai' import ThemeWrapper from '@/containers/Providers/Theme' -import { setupCoreServices } from '@/services/coreService' -import { - isCoreExtensionInstalled, - setupBaseExtensions, -} from '@/services/extensionService' - import Umami from '@/utils/umami' +import { CoreConfigurator } from './CoreConfigurator' import DataLoader from './DataLoader' import DeepLinkListener from './DeepLinkListener' @@ -26,57 +20,12 @@ import Responsive from './Responsive' import SettingsHandler from './SettingsHandler' -import { extensionManager } from '@/extension' - const Providers = ({ children }: PropsWithChildren) => { - const [setupCore, setSetupCore] = useState(false) - const [activated, setActivated] = useState(false) - const [settingUp, setSettingUp] = useState(false) - - const setupExtensions = useCallback(async () => { - // Register all active extensions - await extensionManager.registerActive() - - setTimeout(async () => { - if (!isCoreExtensionInstalled()) { - setSettingUp(true) - await setupBaseExtensions() - return - } - - extensionManager.load() - setSettingUp(false) - setActivated(true) - }, 500) - }, []) - - // Services Setup - useEffect(() => { - setupCoreServices() - setSetupCore(true) - return () => { - extensionManager.unload() - } - }, []) - - useEffect(() => { - if (setupCore) { - // Electron - if (window && window.core?.api) { - setupExtensions() - } else { - // Host - setActivated(true) - } - } - }, [setupCore, setupExtensions]) - return ( - {settingUp && } - {setupCore && activated && ( + <> @@ -87,7 +36,7 @@ const Providers = ({ children }: PropsWithChildren) => { {children} - )} + ) diff --git a/web/helpers/atoms/AppConfig.atom.ts b/web/helpers/atoms/AppConfig.atom.ts index f4acc7dc22..68a375f3b2 100644 --- a/web/helpers/atoms/AppConfig.atom.ts +++ b/web/helpers/atoms/AppConfig.atom.ts @@ -12,14 +12,35 @@ export const janDataFolderPathAtom = atom('') export const experimentalFeatureEnabledAtom = atomWithStorage( EXPERIMENTAL_FEATURE, - false + false, + undefined, + { getOnInit: true } ) -export const proxyEnabledAtom = atomWithStorage(PROXY_FEATURE_ENABLED, false) -export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '') +export const proxyEnabledAtom = atomWithStorage( + PROXY_FEATURE_ENABLED, + false, + undefined, + { getOnInit: true } +) +export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '', undefined, { + getOnInit: true, +}) -export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false) -export const vulkanEnabledAtom = atomWithStorage(VULKAN_ENABLED, false) -export const quickAskEnabledAtom = atomWithStorage(QUICK_ASK_ENABLED, false) +export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false, undefined, { + getOnInit: true, +}) +export const vulkanEnabledAtom = atomWithStorage( + VULKAN_ENABLED, + false, + undefined, + { getOnInit: true } +) +export const quickAskEnabledAtom = atomWithStorage( + QUICK_ASK_ENABLED, + false, + undefined, + { getOnInit: true } +) export const hostAtom = atom('http://localhost:1337/') diff --git a/web/helpers/atoms/Model.atom.ts b/web/helpers/atoms/Model.atom.ts index dd4414801a..445e36a4a8 100644 --- a/web/helpers/atoms/Model.atom.ts +++ b/web/helpers/atoms/Model.atom.ts @@ -16,7 +16,9 @@ enum ModelStorageAtomKeys { */ export const downloadedModelsAtom = atomWithStorage( ModelStorageAtomKeys.DownloadedModels, - [] + [], + undefined, + { getOnInit: true } ) /** @@ -25,7 +27,9 @@ export const downloadedModelsAtom = atomWithStorage( */ export const configuredModelsAtom = atomWithStorage( ModelStorageAtomKeys.AvailableModels, - [] + [], + undefined, + { getOnInit: true } ) export const removeDownloadedModelAtom = atom( diff --git a/web/helpers/atoms/Setting.atom.ts b/web/helpers/atoms/Setting.atom.ts index 57ca878541..904e85fe52 100644 --- a/web/helpers/atoms/Setting.atom.ts +++ b/web/helpers/atoms/Setting.atom.ts @@ -13,10 +13,22 @@ export const REDUCE_TRANSPARENT = 'reduceTransparent' export const SPELL_CHECKING = 'spellChecking' export const themesOptionsAtom = atom<{ name: string; value: string }[]>([]) export const janThemesPathAtom = atom(undefined) -export const selectedThemeIdAtom = atomWithStorage(THEME, '') +export const selectedThemeIdAtom = atomWithStorage( + THEME, + '', + undefined, + { getOnInit: true } +) export const themeDataAtom = atom(undefined) export const reduceTransparentAtom = atomWithStorage( REDUCE_TRANSPARENT, - false + false, + undefined, + { getOnInit: true } +) +export const spellCheckAtom = atomWithStorage( + SPELL_CHECKING, + false, + undefined, + { getOnInit: true } ) -export const spellCheckAtom = atomWithStorage(SPELL_CHECKING, false) diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index 1945fea45d..e0ea433ce7 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -207,7 +207,9 @@ export const setThreadModelParamsAtom = atom( */ export const activeSettingInputBoxAtom = atomWithStorage( ACTIVE_SETTING_INPUT_BOX, - false + false, + undefined, + { getOnInit: true } ) /** diff --git a/web/hooks/useStarterScreen.ts b/web/hooks/useStarterScreen.ts index 4af19bd647..c551ee6019 100644 --- a/web/hooks/useStarterScreen.ts +++ b/web/hooks/useStarterScreen.ts @@ -1,22 +1,20 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue } from 'jotai' import { isLocalEngine } from '@/utils/modelEngine' import { extensionManager } from '@/extension' -import { - downloadedModelsAtom, - selectedModelAtom, -} from '@/helpers/atoms/Model.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { threadsAtom } from '@/helpers/atoms/Thread.atom' export function useStarterScreen() { const downloadedModels = useAtomValue(downloadedModelsAtom) const threads = useAtomValue(threadsAtom) - const setSelectedModel = useSetAtom(selectedModelAtom) - const isDownloadALocalModel = downloadedModels.some((x) => - isLocalEngine(x.engine) + + const isDownloadALocalModel = useMemo( + () => downloadedModels.some((x) => isLocalEngine(x.engine)), + [downloadedModels] ) const [extensionHasSettings, setExtensionHasSettings] = useState< @@ -24,9 +22,6 @@ export function useStarterScreen() { >([]) useEffect(() => { - if (isDownloadALocalModel) { - setSelectedModel(downloadedModels[0]) - } const getAllSettings = async () => { const extensionsMenu: { name?: string @@ -66,12 +61,16 @@ export function useStarterScreen() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - const isAnyRemoteModelConfigured = extensionHasSettings.some( - (x) => x.apiKey.length > 1 + const isAnyRemoteModelConfigured = useMemo( + () => extensionHasSettings.some((x) => x.apiKey.length > 1), + [extensionHasSettings] ) - const isShowStarterScreen = - !isAnyRemoteModelConfigured && !isDownloadALocalModel && !threads.length + const isShowStarterScreen = useMemo( + () => + !isAnyRemoteModelConfigured && !isDownloadALocalModel && !threads.length, + [isAnyRemoteModelConfigured, isDownloadALocalModel, threads] + ) return { extensionHasSettings, diff --git a/web/screens/Thread/ThreadCenterPanel/ChatBody/OnDeviceStarterScreen/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatBody/OnDeviceStarterScreen/index.tsx index 5c042000a6..44d1748ed9 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatBody/OnDeviceStarterScreen/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatBody/OnDeviceStarterScreen/index.tsx @@ -24,6 +24,8 @@ import useDownloadModel from '@/hooks/useDownloadModel' import { modelDownloadStateAtom } from '@/hooks/useDownloadState' +import { useStarterScreen } from '@/hooks/useStarterScreen' + import { formatDownloadPercentage, toGibibytes } from '@/utils/converter' import { getLogoEngine, @@ -38,16 +40,8 @@ import { } from '@/helpers/atoms/Model.atom' import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' -type Props = { - extensionHasSettings: { - name?: string - setting: string - apiKey: string - provider: string - }[] -} - -const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => { +const OnDeviceStarterScreen = () => { + const { extensionHasSettings } = useStarterScreen() const [searchValue, setSearchValue] = useState('') const [isOpen, setIsOpen] = useState(Boolean(searchValue.length)) const downloadingModels = useAtomValue(getDownloadingModelAtom) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx index 6b3f4150a4..38af2cfc0d 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react' + import { MessageStatus } from '@janhq/core' import { useAtomValue } from 'jotai' @@ -44,4 +46,4 @@ const ChatBody = () => { ) } -export default ChatBody +export default memo(ChatBody) diff --git a/web/screens/Thread/ThreadCenterPanel/index.tsx b/web/screens/Thread/ThreadCenterPanel/index.tsx index 1f23e9dc5b..01ba0aaeb5 100644 --- a/web/screens/Thread/ThreadCenterPanel/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { useEffect, useState } from 'react' +import { memo, useEffect, useState } from 'react' import { Accept, useDropzone } from 'react-dropzone' @@ -232,4 +232,4 @@ const ThreadCenterPanel = () => { ) } -export default ThreadCenterPanel +export default memo(ThreadCenterPanel) diff --git a/web/screens/Thread/index.tsx b/web/screens/Thread/index.tsx index b576c905cc..6789c181d4 100644 --- a/web/screens/Thread/index.tsx +++ b/web/screens/Thread/index.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react' + import { useStarterScreen } from '@/hooks/useStarterScreen' import ThreadLeftPanel from '@/screens/Thread/ThreadLeftPanel' @@ -9,19 +11,31 @@ import ModalDeleteThread from './ThreadLeftPanel/ModalDeleteThread' import ModalEditTitleThread from './ThreadLeftPanel/ModalEditTitleThread' import ThreadRightPanel from './ThreadRightPanel' +type Props = { + isShowStarterScreen: boolean +} + +const ThreadPanels = memo(({ isShowStarterScreen }: Props) => { + return isShowStarterScreen ? ( + + ) : ( + <> + + + + + ) +}) + +const WelcomeController = () => { + const { isShowStarterScreen } = useStarterScreen() + return +} + const ThreadScreen = () => { - const { extensionHasSettings, isShowStarterScreen } = useStarterScreen() return (
- {isShowStarterScreen ? ( - - ) : ( - <> - - - - - )} + {/* Showing variant modal action for thread screen */} @@ -31,4 +45,4 @@ const ThreadScreen = () => { ) } -export default ThreadScreen +export default memo(ThreadScreen)