From 98c844bf55626b7835c550c892dea778a8511e53 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Mon, 22 Apr 2024 02:51:21 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20model=20list=20menu?= =?UTF-8?q?=20not=20display=20correctly=20(#2133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor: remove preference hydration * 🐛 fix: fix model-list menu not display correctly * 🐛 fix: fix azure not show the deployment name * ♻️ refactor: rename the StoreHydration --- .../components/ProviderModelList/Option.tsx | 42 +++++++------ .../components/ProviderModelList/index.tsx | 1 + src/layout/GlobalProvider/StoreHydration.tsx | 61 ------------------- .../GlobalProvider/StoreInitialization.tsx | 45 ++++++++++++++ src/layout/GlobalProvider/index.tsx | 4 +- .../global/hooks/useEffectAfterHydrated.ts | 22 ------- src/store/global/slices/common/action.ts | 6 +- src/store/global/slices/preference/action.ts | 38 ++++++++---- .../global/slices/preference/initialState.ts | 7 ++- .../global/slices/settings/actions/llm.ts | 17 +++--- src/store/global/store.ts | 30 ++------- src/utils/localStorage.ts | 36 +++++++++++ 12 files changed, 156 insertions(+), 153 deletions(-) delete mode 100644 src/layout/GlobalProvider/StoreHydration.tsx create mode 100644 src/layout/GlobalProvider/StoreInitialization.tsx delete mode 100644 src/store/global/hooks/useEffectAfterHydrated.ts create mode 100644 src/utils/localStorage.ts diff --git a/src/app/settings/llm/components/ProviderModelList/Option.tsx b/src/app/settings/llm/components/ProviderModelList/Option.tsx index da9c73922699..004413bada08 100644 --- a/src/app/settings/llm/components/ProviderModelList/Option.tsx +++ b/src/app/settings/llm/components/ProviderModelList/Option.tsx @@ -11,28 +11,32 @@ import { GlobalLLMProviderKey } from '@/types/settings'; import CustomModelOption from './CustomModelOption'; -const OptionRender = memo<{ displayName: string; id: string; provider: GlobalLLMProviderKey }>( - ({ displayName, id, provider }) => { - const model = useGlobalStore((s) => modelProviderSelectors.getModelCardById(id)(s), isEqual); +interface OptionRenderProps { + displayName: string; + id: string; + isAzure?: boolean; + provider: GlobalLLMProviderKey; +} +const OptionRender = memo(({ displayName, id, provider, isAzure }) => { + const model = useGlobalStore((s) => modelProviderSelectors.getModelCardById(id)(s), isEqual); - // if there is isCustom, it means it is a user defined custom model - if (model?.isCustom) return ; + // if there is isCustom, it means it is a user defined custom model + if (model?.isCustom || isAzure) return ; - return ( - - - - - {displayName} - - - - {id} - + return ( + + + + + {displayName} + + + {id} + - ); - }, -); + + ); +}); export default OptionRender; diff --git a/src/app/settings/llm/components/ProviderModelList/index.tsx b/src/app/settings/llm/components/ProviderModelList/index.tsx index 3fb0ec110442..9632f007ab7b 100644 --- a/src/app/settings/llm/components/ProviderModelList/index.tsx +++ b/src/app/settings/llm/components/ProviderModelList/index.tsx @@ -111,6 +111,7 @@ const ProviderModelListSelect = memo( ); diff --git a/src/layout/GlobalProvider/StoreHydration.tsx b/src/layout/GlobalProvider/StoreHydration.tsx deleted file mode 100644 index 181c961b0453..000000000000 --- a/src/layout/GlobalProvider/StoreHydration.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; - -import { useResponsive } from 'antd-style'; -import { useRouter } from 'next/navigation'; -import { memo, useEffect } from 'react'; - -import { useEnabledDataSync } from '@/hooks/useSyncData'; -import { useGlobalStore } from '@/store/global'; -import { useEffectAfterGlobalHydrated } from '@/store/global/hooks/useEffectAfterHydrated'; - -const StoreHydration = memo(() => { - const [useFetchServerConfig, useFetchUserConfig] = useGlobalStore((s) => [ - s.useFetchServerConfig, - s.useFetchUserConfig, - ]); - - const { isLoading } = useFetchServerConfig(); - - useFetchUserConfig(!isLoading); - - useEnabledDataSync(); - - useEffect(() => { - // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated - useGlobalStore.persist.rehydrate(); - }, []); - - const { mobile } = useResponsive(); - useEffectAfterGlobalHydrated( - (store) => { - const prevState = store.getState().isMobile; - - if (prevState !== mobile) { - store.setState({ isMobile: mobile }); - } - }, - [mobile], - ); - - const router = useRouter(); - - useEffectAfterGlobalHydrated( - (store) => { - store.setState({ router }); - }, - [router], - ); - - useEffect(() => { - router.prefetch('/chat'); - router.prefetch('/chat/settings'); - router.prefetch('/market'); - router.prefetch('/settings/common'); - router.prefetch('/settings/agent'); - router.prefetch('/settings/sync'); - }, [router]); - - return null; -}); - -export default StoreHydration; diff --git a/src/layout/GlobalProvider/StoreInitialization.tsx b/src/layout/GlobalProvider/StoreInitialization.tsx new file mode 100644 index 000000000000..ce98e65bc13d --- /dev/null +++ b/src/layout/GlobalProvider/StoreInitialization.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { memo, useEffect } from 'react'; +import { createStoreUpdater } from 'zustand-utils'; + +import { useIsMobile } from '@/hooks/useIsMobile'; +import { useEnabledDataSync } from '@/hooks/useSyncData'; +import { useGlobalStore } from '@/store/global'; + +const StoreInitialization = memo(() => { + const [useFetchServerConfig, useFetchUserConfig, useInitPreference] = useGlobalStore((s) => [ + s.useFetchServerConfig, + s.useFetchUserConfig, + s.useInitPreference, + ]); + // init the system preference + useInitPreference(); + + const { isLoading } = useFetchServerConfig(); + useFetchUserConfig(!isLoading); + + useEnabledDataSync(); + + const useStoreUpdater = createStoreUpdater(useGlobalStore); + + const mobile = useIsMobile(); + const router = useRouter(); + + useStoreUpdater('isMobile', mobile); + useStoreUpdater('router', router); + + useEffect(() => { + router.prefetch('/chat'); + router.prefetch('/chat/settings'); + router.prefetch('/market'); + router.prefetch('/settings/common'); + router.prefetch('/settings/agent'); + router.prefetch('/settings/sync'); + }, [router]); + + return null; +}); + +export default StoreInitialization; diff --git a/src/layout/GlobalProvider/index.tsx b/src/layout/GlobalProvider/index.tsx index 73fd6b308d80..c0d61549f4d8 100644 --- a/src/layout/GlobalProvider/index.tsx +++ b/src/layout/GlobalProvider/index.tsx @@ -13,7 +13,7 @@ import { getAntdLocale } from '@/utils/locale'; import AppTheme from './AppTheme'; import Locale from './Locale'; -import StoreHydration from './StoreHydration'; +import StoreInitialization from './StoreInitialization'; import StyleRegistry from './StyleRegistry'; let DebugUI: FC = () => null; @@ -50,7 +50,7 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => { defaultNeutralColor={neutralColor?.value as any} defaultPrimaryColor={primaryColor?.value as any} > - + {children} diff --git a/src/store/global/hooks/useEffectAfterHydrated.ts b/src/store/global/hooks/useEffectAfterHydrated.ts deleted file mode 100644 index 8cb9caefb4d0..000000000000 --- a/src/store/global/hooks/useEffectAfterHydrated.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; - -import { useGlobalStore } from '../store'; - -export const useEffectAfterGlobalHydrated = ( - fn: (store: typeof useGlobalStore) => void, - deps: any[] = [], -) => { - useEffect(() => { - const hasRehydrated = useGlobalStore.persist.hasHydrated(); - - if (hasRehydrated) { - // 等价 useEffect 多次触发 - fn(useGlobalStore); - } else { - // 等价于 useEffect 第一次触发 - useGlobalStore.persist.onFinishHydration(() => { - fn(useGlobalStore); - }); - } - }, deps); -}; diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index aeeb71b4e2ad..d973441b6866 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -147,7 +147,7 @@ export const createCommonSlice: StateCreator< }, { onSuccess: (syncEnabled) => { - set({ syncEnabled }); + set({ syncEnabled }, false, n('useEnabledSync')); }, revalidateOnFocus: false, }, @@ -165,7 +165,7 @@ export const createCommonSlice: StateCreator< set({ defaultSettings, serverConfig: data }, false, n('initGlobalConfig')); - get().refreshDefaultModelProviderList(); + get().refreshDefaultModelProviderList({ trigger: 'fetchServerConfig' }); } }, revalidateOnFocus: false, @@ -188,7 +188,7 @@ export const createCommonSlice: StateCreator< ); // when get the user config ,refresh the model provider list to the latest - get().refreshModelProviderList(); + get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' }); const { language } = settingsSelectors.currentSettings(get()); if (language === 'auto') { diff --git a/src/store/global/slices/preference/action.ts b/src/store/global/slices/preference/action.ts index f31a8bdf5606..058794bf592f 100644 --- a/src/store/global/slices/preference/action.ts +++ b/src/store/global/slices/preference/action.ts @@ -1,11 +1,13 @@ import { produce } from 'immer'; +import { SWRResponse } from 'swr'; import type { StateCreator } from 'zustand/vanilla'; +import { useClientDataSWR } from '@/libs/swr'; import type { GlobalStore } from '@/store/global'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; -import type { GlobalPreference, GlobalPreferenceState, Guide } from './initialState'; +import type { GlobalPreference, Guide } from './initialState'; const n = setNamespace('preference'); @@ -18,7 +20,8 @@ export interface PreferenceAction { toggleMobileTopic: (visible?: boolean) => void; toggleSystemRole: (visible?: boolean) => void; updateGuideState: (guide: Partial) => void; - updatePreference: (preference: Partial, action?: string) => void; + updatePreference: (preference: Partial, action?: any) => void; + useInitPreference: () => SWRResponse; } export const createPreferenceSlice: StateCreator< @@ -31,7 +34,7 @@ export const createPreferenceSlice: StateCreator< const showChatSideBar = typeof newValue === 'boolean' ? newValue : !get().preference.showChatSideBar; - get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue) as string); + get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue)); }, toggleExpandSessionGroup: (id, expand) => { const { preference } = get(); @@ -50,13 +53,13 @@ export const createPreferenceSlice: StateCreator< const mobileShowTopic = typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic; - get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue) as string); + get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue)); }, toggleSystemRole: (newValue) => { const showSystemRole = typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic; - get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue) as string); + get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue)); }, updateGuideState: (guide) => { const { updatePreference } = get(); @@ -64,12 +67,23 @@ export const createPreferenceSlice: StateCreator< updatePreference({ guide: nextGuide }); }, updatePreference: (preference, action) => { - set( - produce((draft: GlobalPreferenceState) => { - draft.preference = merge(draft.preference, preference); - }), - false, - action, - ); + const nextPreference = merge(get().preference, preference); + + set({ preference: nextPreference }, false, action || n('updatePreference')); + + get().preferenceStorage.saveToLocalStorage(nextPreference); }, + + useInitPreference: () => + useClientDataSWR( + 'preference', + () => get().preferenceStorage.getFromLocalStorage(), + { + onSuccess: (preference) => { + if (preference) { + set({ preference }, false, n('initPreference')); + } + }, + }, + ), }); diff --git a/src/store/global/slices/preference/initialState.ts b/src/store/global/slices/preference/initialState.ts index 3a50d968df74..0f96d3254f25 100644 --- a/src/store/global/slices/preference/initialState.ts +++ b/src/store/global/slices/preference/initialState.ts @@ -1,4 +1,5 @@ import { SessionDefaultGroup, SessionGroupId } from '@/types/session'; +import { AsyncLocalStorage } from '@/utils/localStorage'; export interface Guide { // Topic 引导 @@ -18,6 +19,7 @@ export interface GlobalPreference { showSessionPanel?: boolean; showSystemRole?: boolean; telemetry: boolean | null; + /** * whether to use cmd + enter to send message */ @@ -26,10 +28,10 @@ export interface GlobalPreference { export interface GlobalPreferenceState { /** - * 用户偏好的 UI 状态 - * @localStorage + * the user preference, which only store in local storage */ preference: GlobalPreference; + preferenceStorage: AsyncLocalStorage; } export const initialPreferenceState: GlobalPreferenceState = { @@ -45,4 +47,5 @@ export const initialPreferenceState: GlobalPreferenceState = { telemetry: null, useCmdEnterToSend: false, }, + preferenceStorage: new AsyncLocalStorage('LOBE_PREFERENCE'), }; diff --git a/src/store/global/slices/settings/actions/llm.ts b/src/store/global/slices/settings/actions/llm.ts index 02faa096afd1..6a5086a9b4d4 100644 --- a/src/store/global/slices/settings/actions/llm.ts +++ b/src/store/global/slices/settings/actions/llm.ts @@ -20,11 +20,14 @@ import { import { GlobalStore } from '@/store/global'; import { ChatModelCard } from '@/types/llm'; import { GlobalLLMConfig, GlobalLLMProviderKey } from '@/types/settings'; +import { setNamespace } from '@/utils/storeDebug'; import { CustomModelCardDispatch, customModelCardsReducer } from '../reducers/customModelCard'; import { modelProviderSelectors } from '../selectors/modelProvider'; import { settingsSelectors } from '../selectors/settings'; +const n = setNamespace('settings'); + /** * 设置操作 */ @@ -36,8 +39,8 @@ export interface LLMSettingsAction { /** * make sure the default model provider list is sync to latest state */ - refreshDefaultModelProviderList: () => void; - refreshModelProviderList: () => void; + refreshDefaultModelProviderList: (params?: { trigger?: string }) => void; + refreshModelProviderList: (params?: { trigger?: string }) => void; removeEnabledModels: (provider: GlobalLLMProviderKey, model: string) => Promise; setModelProviderConfig: ( provider: T, @@ -69,7 +72,7 @@ export const llmSettingsSlice: StateCreator< await get().setModelProviderConfig(provider, { customModelCards: nextState }); }, - refreshDefaultModelProviderList: () => { + refreshDefaultModelProviderList: (params) => { /** * Because we have several model cards sources, we need to merge the model cards * the priority is below: @@ -113,12 +116,12 @@ export const llmSettingsSlice: StateCreator< ZhiPuProviderCard, ]; - set({ defaultModelProviderList }, false, 'refreshDefaultModelProviderList'); + set({ defaultModelProviderList }, false, n(`refreshDefaultModelList - ${params?.trigger}`)); - get().refreshModelProviderList(); + get().refreshModelProviderList({ trigger: 'refreshDefaultModelList' }); }, - refreshModelProviderList: () => { + refreshModelProviderList: (params) => { const modelProviderList = get().defaultModelProviderList.map((list) => ({ ...list, chatModels: modelProviderSelectors @@ -136,7 +139,7 @@ export const llmSettingsSlice: StateCreator< enabled: modelProviderSelectors.isProviderEnabled(list.id as any)(get()), })); - set({ modelProviderList }, false, 'refreshModelProviderList'); + set({ modelProviderList }, false, n(`refreshModelList - ${params?.trigger}`)); }, removeEnabledModels: async (provider, model) => { diff --git a/src/store/global/store.ts b/src/store/global/store.ts index ec6dbcb18305..75382b4296f4 100644 --- a/src/store/global/store.ts +++ b/src/store/global/store.ts @@ -1,11 +1,10 @@ -import { PersistOptions, devtools, persist, subscribeWithSelector } from 'zustand/middleware'; +import { devtools, subscribeWithSelector } from 'zustand/middleware'; import { shallow } from 'zustand/shallow'; import { createWithEqualityFn } from 'zustand/traditional'; import { StateCreator } from 'zustand/vanilla'; import { isDev } from '@/utils/env'; -import { createHyperStorage } from '../middleware/createHyperStorage'; import { type GlobalState, initialState } from './initialState'; import { type CommonAction, createCommonSlice } from './slices/common/action'; import { type PreferenceAction, createPreferenceSlice } from './slices/preference/action'; @@ -22,32 +21,13 @@ const createStore: StateCreator = (. ...createPreferenceSlice(...parameters), }); -// =============== persist 本地缓存中间件配置 ============ // -type GlobalPersist = Pick; - -const persistOptions: PersistOptions = { - name: 'LOBE_GLOBAL', - - skipHydration: true, - - storage: createHyperStorage({ - localStorage: { - dbName: 'LobeHub', - selectors: ['preference'], - }, - }), -}; - // =============== 实装 useStore ============ // export const useGlobalStore = createWithEqualityFn()( - persist( - subscribeWithSelector( - devtools(createStore, { - name: 'LobeChat_Global' + (isDev ? '_DEV' : ''), - }), - ), - persistOptions, + subscribeWithSelector( + devtools(createStore, { + name: 'LobeChat_Global' + (isDev ? '_DEV' : ''), + }), ), shallow, ); diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts new file mode 100644 index 000000000000..4408c4fb16a8 --- /dev/null +++ b/src/utils/localStorage.ts @@ -0,0 +1,36 @@ +const PREV_KEY = 'LOBE_GLOBAL'; + +type StorageKey = 'LOBE_PREFERENCE'; + +export class AsyncLocalStorage { + private storageKey: StorageKey; + + constructor(storageKey: StorageKey) { + this.storageKey = storageKey; + + // skip server side rendering + if (typeof window === 'undefined') return; + + // migrate old data + if (localStorage.getItem(PREV_KEY)) { + const data = JSON.parse(localStorage.getItem(PREV_KEY) || '{}'); + + const preference = data.state.preference; + + if (data.state?.preference) { + localStorage.setItem('LOBE_PREFERENCE', JSON.stringify(preference)); + } + localStorage.removeItem(PREV_KEY); + } + } + + async saveToLocalStorage(state: object) { + const data = await this.getFromLocalStorage(); + + localStorage.setItem(this.storageKey, JSON.stringify({ ...data, ...state })); + } + + async getFromLocalStorage(key: StorageKey = this.storageKey): Promise { + return JSON.parse(localStorage.getItem(key) || '{}'); + } +}