mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-18 21:36:12 +00:00
✨ feat: user onboarding
This commit is contained in:
@@ -24,7 +24,6 @@ export const DEFAULT_AGENT_SEARCH_FC_MODEL = {
|
||||
|
||||
export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {
|
||||
autoCreateTopicThreshold: 2,
|
||||
displayMode: 'chat',
|
||||
enableAutoCreateTopic: true,
|
||||
enableCompressHistory: true,
|
||||
enableHistoryCount: true,
|
||||
|
||||
@@ -5,6 +5,9 @@ export const DEFAULT_COMMON_SETTINGS: UserGeneralConfig = {
|
||||
// contextMenuMode not set default value, use env to calc
|
||||
fontSize: 14,
|
||||
highlighterTheme: 'lobe-theme',
|
||||
isDevMode: false,
|
||||
isLiteMode: false,
|
||||
mermaidTheme: 'lobe-theme',
|
||||
telemetry: true,
|
||||
transitionMode: 'fadeIn',
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ export const DEFAULT_CHAT_GROUP_CHAT_CONFIG: LobeChatGroupChatConfig = {
|
||||
allowDM: true,
|
||||
enableSupervisor: true,
|
||||
maxResponseInRow: 10,
|
||||
openingMessage: '',
|
||||
openingQuestions: [],
|
||||
orchestratorModel: DEFAULT_MODEL,
|
||||
orchestratorProvider: DEFAULT_PROVIDER,
|
||||
responseOrder: 'natural',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DEFAULT_COMMON_SETTINGS } from './common';
|
||||
import { DEFAULT_HOTKEY_CONFIG } from './hotkey';
|
||||
import { DEFAULT_IMAGE_CONFIG } from './image';
|
||||
import { DEFAULT_LLM_CONFIG } from './llm';
|
||||
import { DEFAULT_MEMORY_SETTINGS } from './memory';
|
||||
import { DEFAULT_SYSTEM_AGENT_CONFIG } from './systemAgent';
|
||||
import { DEFAULT_TOOL_CONFIG } from './tool';
|
||||
import { DEFAULT_TTS_CONFIG } from './tts';
|
||||
@@ -16,6 +17,7 @@ export * from './hotkey';
|
||||
export * from './image';
|
||||
export * from './knowledge';
|
||||
export * from './llm';
|
||||
export * from './memory';
|
||||
export * from './systemAgent';
|
||||
export * from './tool';
|
||||
export * from './tts';
|
||||
@@ -27,6 +29,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
|
||||
image: DEFAULT_IMAGE_CONFIG,
|
||||
keyVaults: {},
|
||||
languageModel: DEFAULT_LLM_CONFIG,
|
||||
memory: DEFAULT_MEMORY_SETTINGS,
|
||||
systemAgent: DEFAULT_SYSTEM_AGENT_CONFIG,
|
||||
tool: DEFAULT_TOOL_CONFIG,
|
||||
tts: DEFAULT_TTS_CONFIG,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UserMemorySettings } from '@lobechat/types';
|
||||
|
||||
export const DEFAULT_MEMORY_SETTINGS: UserMemorySettings = {
|
||||
enabled: true,
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
|
||||
import { Plans } from '../subscription';
|
||||
import { TopicDisplayMode } from '../topic';
|
||||
import { UserOnboarding } from './onboarding';
|
||||
import { UserSettings } from './settings';
|
||||
|
||||
export interface LobeUser {
|
||||
@@ -11,6 +12,7 @@ export interface LobeUser {
|
||||
firstName?: string | null;
|
||||
fullName?: string | null;
|
||||
id: string;
|
||||
interests?: string[];
|
||||
latestName?: string | null;
|
||||
username?: string | null;
|
||||
}
|
||||
@@ -59,7 +61,10 @@ export interface UserPreference {
|
||||
* lab experimental features
|
||||
*/
|
||||
lab?: UserLab;
|
||||
telemetry: boolean | null;
|
||||
/**
|
||||
* @deprecated Use settings.general.telemetry instead
|
||||
*/
|
||||
telemetry?: boolean | null;
|
||||
topicDisplayMode?: TopicDisplayMode;
|
||||
/**
|
||||
* whether to use cmd + enter to send message
|
||||
@@ -75,8 +80,11 @@ export interface UserInitializationState {
|
||||
firstName?: string;
|
||||
fullName?: string;
|
||||
hasConversation?: boolean;
|
||||
interests?: string[];
|
||||
/** @deprecated Use onboarding field instead */
|
||||
isOnboard?: boolean;
|
||||
lastName?: string;
|
||||
onboarding?: UserOnboarding;
|
||||
preference: UserPreference;
|
||||
settings: PartialDeep<UserSettings>;
|
||||
subscriptionPlan?: Plans;
|
||||
|
||||
@@ -11,8 +11,12 @@ export interface UserGeneralConfig {
|
||||
contextMenuMode?: ContextMenuMode;
|
||||
fontSize: number;
|
||||
highlighterTheme?: HighlighterProps['theme'];
|
||||
isDevMode: boolean;
|
||||
isLiteMode: boolean;
|
||||
mermaidTheme?: MermaidProps['theme'];
|
||||
neutralColor?: NeutralColors;
|
||||
primaryColor?: PrimaryColors;
|
||||
responseLanguage?: string;
|
||||
telemetry: boolean;
|
||||
transitionMode?: ResponseAnimationStyle;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { UserHotkeyConfig } from './hotkey';
|
||||
import { UserImageConfig } from './image';
|
||||
import { UserKeyVaults } from './keyVaults';
|
||||
import { MarketAuthTokens } from './market';
|
||||
import { UserMemorySettings } from './memory';
|
||||
import { UserModelProviderConfig } from './modelProvider';
|
||||
import { UserSystemAgentConfig } from './systemAgent';
|
||||
import { UserToolConfig } from './tool';
|
||||
@@ -19,6 +20,7 @@ export * from './hotkey';
|
||||
export * from './image';
|
||||
export * from './keyVaults';
|
||||
export * from './market';
|
||||
export * from './memory';
|
||||
export * from './modelProvider';
|
||||
export * from './sync';
|
||||
export * from './systemAgent';
|
||||
@@ -36,6 +38,7 @@ export interface UserSettings {
|
||||
keyVaults: UserKeyVaults;
|
||||
languageModel: UserModelProviderConfig;
|
||||
market?: MarketAuthTokens;
|
||||
memory?: UserMemorySettings;
|
||||
systemAgent: UserSystemAgentConfig;
|
||||
tool: UserToolConfig;
|
||||
tts: UserTTSConfig;
|
||||
@@ -54,6 +57,7 @@ export const UserSettingsSchema = z
|
||||
keyVaults: z.any().optional(),
|
||||
languageModel: z.any().optional(),
|
||||
market: z.any().optional(),
|
||||
memory: z.any().optional(),
|
||||
systemAgent: z.any().optional(),
|
||||
tool: z.any().optional(),
|
||||
tts: z.any().optional(),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface UserMemorySettings {
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import { UserAuthState, initialAuthState } from './slices/auth/initialState';
|
||||
import { CommonState, initialCommonState } from './slices/common/initialState';
|
||||
import { OnboardingState, initialOnboardingState } from './slices/onboarding/initialState';
|
||||
import { UserPreferenceState, initialPreferenceState } from './slices/preference/initialState';
|
||||
import { UserSettingsState, initialSettingsState } from './slices/settings/initialState';
|
||||
|
||||
export type UserState = UserSettingsState & UserPreferenceState & UserAuthState & CommonState;
|
||||
export type UserState = UserSettingsState &
|
||||
UserPreferenceState &
|
||||
UserAuthState &
|
||||
CommonState &
|
||||
OnboardingState;
|
||||
|
||||
export const initialState: UserState = {
|
||||
...initialSettingsState,
|
||||
...initialPreferenceState,
|
||||
...initialAuthState,
|
||||
...initialCommonState,
|
||||
...initialOnboardingState,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { authSelectors, userProfileSelectors } from './slices/auth/selectors';
|
||||
export { onboardingSelectors } from './slices/onboarding/selectors';
|
||||
export { labPreferSelectors, preferenceSelectors } from './slices/preference/selectors';
|
||||
export {
|
||||
keyVaultsConfigSelectors,
|
||||
|
||||
@@ -243,7 +243,7 @@ describe('userProfileSelectors', () => {
|
||||
|
||||
describe('authSelectors', () => {
|
||||
describe('isLogin', () => {
|
||||
it('should return true when auth is disabled', () => {
|
||||
it('should return false when not signed in (regardless of auth enabled state)', () => {
|
||||
enableAuth = false;
|
||||
|
||||
const store: UserStore = {
|
||||
@@ -251,7 +251,8 @@ describe('authSelectors', () => {
|
||||
enableAuth: () => false,
|
||||
} as UserStore;
|
||||
|
||||
expect(authSelectors.isLogin(store)).toBe(true);
|
||||
// isLogin now only checks isSignedIn, not enableAuth
|
||||
expect(authSelectors.isLogin(store)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when signed in', () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ export const userProfileSelectors = {
|
||||
displayUserName: (s: UserStore): string => username(s) || s.user?.email || '',
|
||||
email: (s: UserStore): string => s.user?.email || '',
|
||||
fullName: (s: UserStore): string => s.user?.fullName || '',
|
||||
interests: (s: UserStore): string[] => s.user?.interests || [],
|
||||
nickName,
|
||||
userAvatar: (s: UserStore): string => s.user?.avatar || '',
|
||||
userId: (s: UserStore) => s.user?.id,
|
||||
@@ -45,22 +46,12 @@ export const userProfileSelectors = {
|
||||
username,
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用此方法可以兼容不需要登录鉴权的情况
|
||||
*/
|
||||
const isLogin = (s: UserStore) => {
|
||||
// 如果没有开启鉴权,说明不需要登录,默认是登录态
|
||||
if (!enableAuth) return true;
|
||||
|
||||
return s.isSignedIn;
|
||||
};
|
||||
|
||||
export const authSelectors = {
|
||||
authProviders: (s: UserStore): SSOProvider[] => s.authProviders || [],
|
||||
hasPasswordAccount: (s: UserStore) => s.hasPasswordAccount ?? false,
|
||||
isLoaded: (s: UserStore) => s.isLoaded,
|
||||
isLoadedAuthProviders: (s: UserStore) => s.isLoadedAuthProviders ?? false,
|
||||
isLogin,
|
||||
isLogin: (s: UserStore) => s.isSignedIn,
|
||||
isLoginWithAuth: (s: UserStore) => s.isSignedIn,
|
||||
isLoginWithBetterAuth: (s: UserStore): boolean => (s.isSignedIn && enableBetterAuth) || false,
|
||||
isLoginWithClerk: (s: UserStore): boolean => (s.isSignedIn && enableClerk) || false,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { withSWR } from '~test-utils';
|
||||
import { DEFAULT_PREFERENCE } from '@/const/user';
|
||||
import { userService } from '@/services/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { preferenceSelectors } from '@/store/user/selectors';
|
||||
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
||||
import { GlobalServerConfig } from '@/types/serverConfig';
|
||||
import { UserInitializationState, UserPreference } from '@/types/user';
|
||||
|
||||
@@ -232,8 +232,8 @@ describe('createCommonSlice', () => {
|
||||
await waitFor(() => expect(result.current.data).toBeUndefined());
|
||||
});
|
||||
|
||||
it('should return false when userAllowTrace is already set', async () => {
|
||||
vi.spyOn(preferenceSelectors, 'userAllowTrace').mockReturnValueOnce(true);
|
||||
it('should return false when telemetry is already set', async () => {
|
||||
vi.spyOn(userGeneralSettingsSelectors, 'telemetry').mockReturnValueOnce(true);
|
||||
|
||||
const { result } = renderHook(() => useUserStore().useCheckTrace(true), {
|
||||
wrapper: withSWR,
|
||||
@@ -243,7 +243,7 @@ describe('createCommonSlice', () => {
|
||||
});
|
||||
|
||||
it('should call messageService.messageCountToCheckTrace when needed', async () => {
|
||||
vi.spyOn(preferenceSelectors, 'userAllowTrace').mockReturnValueOnce(null);
|
||||
vi.spyOn(userGeneralSettingsSelectors, 'telemetry').mockReturnValueOnce(undefined as any);
|
||||
|
||||
act(() => {
|
||||
useUserStore.setState({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isDesktop } from '@lobechat/const';
|
||||
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
|
||||
import useSWR, { SWRResponse, mutate } from 'swr';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
@@ -13,7 +14,7 @@ import type { UserSettings } from '@/types/user/settings';
|
||||
import { merge } from '@/utils/merge';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { preferenceSelectors } from '../preference/selectors';
|
||||
import { userGeneralSettingsSelectors } from '../settings/selectors';
|
||||
|
||||
const n = setNamespace('common');
|
||||
|
||||
@@ -25,6 +26,7 @@ export interface CommonAction {
|
||||
refreshUserState: () => Promise<void>;
|
||||
updateAvatar: (avatar: string) => Promise<void>;
|
||||
updateFullName: (fullName: string) => Promise<void>;
|
||||
updateInterests: (interests: string[]) => Promise<void>;
|
||||
updateKeyVaultConfig: (provider: string, config: any) => Promise<void>;
|
||||
updateUsername: (username: string) => Promise<void>;
|
||||
useCheckTrace: (shouldFetch: boolean) => SWRResponse;
|
||||
@@ -57,6 +59,11 @@ export const createCommonSlice: StateCreator<
|
||||
await get().refreshUserState();
|
||||
},
|
||||
|
||||
updateInterests: async (interests) => {
|
||||
await userService.updateInterests(interests);
|
||||
await get().refreshUserState();
|
||||
},
|
||||
|
||||
updateKeyVaultConfig: async (provider, config) => {
|
||||
await get().setSettings({ keyVaults: { [provider]: config } });
|
||||
},
|
||||
@@ -70,10 +77,10 @@ export const createCommonSlice: StateCreator<
|
||||
useSWR<boolean>(
|
||||
shouldFetch ? 'checkTrace' : null,
|
||||
() => {
|
||||
const userAllowTrace = preferenceSelectors.userAllowTrace(get());
|
||||
const telemetry = userGeneralSettingsSelectors.telemetry(get());
|
||||
|
||||
// if user have set the trace, return false
|
||||
if (typeof userAllowTrace === 'boolean') return Promise.resolve(false);
|
||||
// if user have set the telemetry, return false
|
||||
if (typeof telemetry === 'boolean') return Promise.resolve(false);
|
||||
|
||||
return Promise.resolve(get().isUserCanEnableTrace);
|
||||
},
|
||||
@@ -83,7 +90,7 @@ export const createCommonSlice: StateCreator<
|
||||
),
|
||||
useInitUserState: (isLogin, serverConfig, options) =>
|
||||
useOnlyFetchOnceSWR<UserInitializationState>(
|
||||
!!isLogin ? GET_USER_STATE_KEY : null,
|
||||
!!isLogin || isDesktop ? GET_USER_STATE_KEY : null,
|
||||
() => userService.getUserState(),
|
||||
{
|
||||
onError: (error) => {
|
||||
@@ -115,6 +122,7 @@ export const createCommonSlice: StateCreator<
|
||||
firstName: data.firstName,
|
||||
fullName: data.fullName,
|
||||
id: data.userId,
|
||||
interests: data.interests,
|
||||
latestName: data.lastName,
|
||||
username: data.username,
|
||||
} as LobeUser)
|
||||
@@ -128,6 +136,7 @@ export const createCommonSlice: StateCreator<
|
||||
isUserCanEnableTrace: data.canEnableTrace,
|
||||
isUserHasConversation: data.hasConversation,
|
||||
isUserStateInit: true,
|
||||
onboarding: data.onboarding,
|
||||
preference,
|
||||
settings: data.settings || {},
|
||||
subscriptionPlan: data.subscriptionPlan,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Plans } from '@/types/subscription';
|
||||
|
||||
export interface CommonState {
|
||||
/** @deprecated Use onboarding field instead */
|
||||
isOnboard: boolean;
|
||||
isShowPWAGuide: boolean;
|
||||
isUserCanEnableTrace: boolean;
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { CURRENT_ONBOARDING_VERSION, INBOX_SESSION_ID } from '@lobechat/const';
|
||||
import type { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { userService } from '@/services/user';
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
import type { UserStore } from '@/store/user';
|
||||
|
||||
import { settingsSelectors } from '../settings/selectors';
|
||||
import { onboardingSelectors } from './selectors';
|
||||
|
||||
export interface OnboardingAction {
|
||||
finishOnboarding: () => Promise<void>;
|
||||
goToNextStep: () => void;
|
||||
goToPreviousStep: () => void;
|
||||
/**
|
||||
* Internal method to process the step update queue
|
||||
*/
|
||||
internal_processStepUpdateQueue: () => Promise<void>;
|
||||
/**
|
||||
* Internal method to queue a step update
|
||||
*/
|
||||
internal_queueStepUpdate: (step: number) => void;
|
||||
setOnboardingStep: (step: number) => Promise<void>;
|
||||
/**
|
||||
* Toggle plugin in default agent config for onboarding
|
||||
*/
|
||||
toggleInboxAgentDefaultPlugin: (id: string, open?: boolean) => Promise<void>;
|
||||
/**
|
||||
* Update default model for both user settings and inbox agent
|
||||
*/
|
||||
updateDefaultModel: (model: string, provider: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const createOnboardingSlice: StateCreator<
|
||||
UserStore,
|
||||
[['zustand/devtools', never]],
|
||||
[],
|
||||
OnboardingAction
|
||||
> = (set, get) => ({
|
||||
finishOnboarding: async () => {
|
||||
const currentStep = onboardingSelectors.currentStep(get());
|
||||
|
||||
await userService.updateOnboarding({
|
||||
currentStep,
|
||||
finishedAt: new Date().toISOString(),
|
||||
version: CURRENT_ONBOARDING_VERSION,
|
||||
});
|
||||
|
||||
await get().refreshUserState();
|
||||
},
|
||||
|
||||
goToNextStep: () => {
|
||||
const currentStep = onboardingSelectors.currentStep(get());
|
||||
const nextStep = currentStep + 1;
|
||||
|
||||
// Optimistic update: immediately update local state
|
||||
set({ localOnboardingStep: nextStep }, false, 'goToNextStep/optimistic');
|
||||
|
||||
// Queue the server update
|
||||
get().internal_queueStepUpdate(nextStep);
|
||||
},
|
||||
|
||||
goToPreviousStep: () => {
|
||||
const currentStep = onboardingSelectors.currentStep(get());
|
||||
if (currentStep <= 1) return;
|
||||
|
||||
const prevStep = currentStep - 1;
|
||||
|
||||
// Optimistic update: immediately update local state
|
||||
set({ localOnboardingStep: prevStep }, false, 'goToPreviousStep/optimistic');
|
||||
|
||||
// Queue the server update
|
||||
get().internal_queueStepUpdate(prevStep);
|
||||
},
|
||||
|
||||
internal_processStepUpdateQueue: async () => {
|
||||
const { isProcessingStepQueue, stepUpdateQueue } = get();
|
||||
if (isProcessingStepQueue || stepUpdateQueue.length === 0) return;
|
||||
|
||||
set({ isProcessingStepQueue: true }, false, 'processStepUpdateQueue/start');
|
||||
|
||||
while (get().stepUpdateQueue.length > 0) {
|
||||
const step = get().stepUpdateQueue[0];
|
||||
|
||||
try {
|
||||
await userService.updateOnboarding({
|
||||
currentStep: step,
|
||||
version: CURRENT_ONBOARDING_VERSION,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update onboarding step:', error);
|
||||
}
|
||||
|
||||
// Remove the completed task
|
||||
set(
|
||||
{ stepUpdateQueue: get().stepUpdateQueue.slice(1) },
|
||||
false,
|
||||
'processStepUpdateQueue/shift',
|
||||
);
|
||||
}
|
||||
|
||||
set({ isProcessingStepQueue: false }, false, 'processStepUpdateQueue/end');
|
||||
|
||||
// Sync with server state after all updates complete
|
||||
await get().refreshUserState();
|
||||
},
|
||||
|
||||
internal_queueStepUpdate: (step) => {
|
||||
const { stepUpdateQueue } = get();
|
||||
|
||||
if (stepUpdateQueue.length === 0) {
|
||||
// Queue is empty, add task and start processing
|
||||
set({ stepUpdateQueue: [step] }, false, 'queueStepUpdate/push');
|
||||
get().internal_processStepUpdateQueue();
|
||||
} else if (stepUpdateQueue.length === 1) {
|
||||
// One task is executing, add as pending
|
||||
set({ stepUpdateQueue: [...stepUpdateQueue, step] }, false, 'queueStepUpdate/push');
|
||||
} else {
|
||||
// Queue is full (length >= 2), replace the pending task
|
||||
set({ stepUpdateQueue: [stepUpdateQueue[0], step] }, false, 'queueStepUpdate/replace');
|
||||
}
|
||||
},
|
||||
|
||||
setOnboardingStep: async (step) => {
|
||||
// Optimistic update
|
||||
set({ localOnboardingStep: step }, false, 'setOnboardingStep/optimistic');
|
||||
|
||||
await userService.updateOnboarding({
|
||||
currentStep: step,
|
||||
version: CURRENT_ONBOARDING_VERSION,
|
||||
});
|
||||
|
||||
await get().refreshUserState();
|
||||
},
|
||||
|
||||
toggleInboxAgentDefaultPlugin: async (id, open) => {
|
||||
const currentSettings = settingsSelectors.currentSettings(get());
|
||||
const currentPlugins = currentSettings.defaultAgent?.config?.plugins || [];
|
||||
|
||||
const index = currentPlugins.indexOf(id);
|
||||
const shouldOpen = open !== undefined ? open : index === -1;
|
||||
|
||||
const agentStore = getAgentStoreState();
|
||||
const inboxAgentId = agentStore.builtinAgentIdMap[INBOX_SESSION_ID];
|
||||
|
||||
// Calculate inbox agent's new plugins
|
||||
const inboxPlugins = inboxAgentId ? agentStore.agentMap[inboxAgentId]?.plugins || [] : [];
|
||||
const inboxIndex = inboxPlugins.indexOf(id);
|
||||
let newInboxPlugins: string[];
|
||||
if (shouldOpen) {
|
||||
newInboxPlugins = inboxIndex === -1 ? [...inboxPlugins, id] : inboxPlugins;
|
||||
} else {
|
||||
newInboxPlugins = inboxIndex !== -1 ? inboxPlugins.filter((p) => p !== id) : inboxPlugins;
|
||||
}
|
||||
|
||||
if (inboxAgentId) {
|
||||
await agentStore.updateAgentConfigById(inboxAgentId, { plugins: newInboxPlugins });
|
||||
}
|
||||
},
|
||||
|
||||
updateDefaultModel: async (model, provider) => {
|
||||
const agentStore = getAgentStoreState();
|
||||
const inboxAgentId = agentStore.builtinAgentIdMap[INBOX_SESSION_ID];
|
||||
|
||||
await Promise.all([
|
||||
// 1. Update user settings' defaultAgentConfig
|
||||
get().updateDefaultAgent({ config: { model, provider } }),
|
||||
// 2. Update inbox agent's model
|
||||
inboxAgentId && agentStore.updateAgentConfigById(inboxAgentId, { model, provider }),
|
||||
]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { UserOnboarding } from '@/types/user';
|
||||
|
||||
export interface OnboardingState {
|
||||
/**
|
||||
* Whether the step update queue is currently being processed.
|
||||
*/
|
||||
isProcessingStepQueue: boolean;
|
||||
/**
|
||||
* Local step for optimistic UI updates.
|
||||
* When set, takes precedence over server state for immediate UI feedback.
|
||||
*/
|
||||
localOnboardingStep?: number;
|
||||
onboarding?: UserOnboarding;
|
||||
/**
|
||||
* Queue for managing server updates with max length 2.
|
||||
* - Position 0: Currently executing task
|
||||
* - Position 1: Pending task (replaced if new task arrives while queue is full)
|
||||
*/
|
||||
stepUpdateQueue: number[];
|
||||
}
|
||||
|
||||
export const initialOnboardingState: OnboardingState = {
|
||||
isProcessingStepQueue: false,
|
||||
localOnboardingStep: undefined,
|
||||
onboarding: undefined,
|
||||
stepUpdateQueue: [],
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CURRENT_ONBOARDING_VERSION } from '@lobechat/const';
|
||||
|
||||
import type { UserStore } from '../../store';
|
||||
|
||||
/**
|
||||
* Returns the current step for UI display.
|
||||
* Prioritizes local optimistic state over server state for immediate feedback.
|
||||
*/
|
||||
const currentStep = (s: UserStore) => s.localOnboardingStep ?? s.onboarding?.currentStep ?? 1;
|
||||
|
||||
const version = (s: UserStore) => s.onboarding?.version ?? CURRENT_ONBOARDING_VERSION;
|
||||
|
||||
const finishedAt = (s: UserStore) => s.onboarding?.finishedAt;
|
||||
|
||||
const isFinished = (s: UserStore) => !!s.onboarding?.finishedAt;
|
||||
|
||||
/**
|
||||
* Check if user needs to go through onboarding.
|
||||
*/
|
||||
const needsOnboarding = (s: Pick<UserStore, 'onboarding'>) => {
|
||||
return (
|
||||
!s.onboarding?.finishedAt ||
|
||||
(s.onboarding?.version && s.onboarding.version < CURRENT_ONBOARDING_VERSION)
|
||||
);
|
||||
};
|
||||
|
||||
export const onboardingSelectors = {
|
||||
currentStep,
|
||||
finishedAt,
|
||||
isFinished,
|
||||
needsOnboarding,
|
||||
version,
|
||||
};
|
||||
@@ -29,19 +29,6 @@ describe('preferenceSelectors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('userAllowTrace', () => {
|
||||
it('should return the value of telemetry preference', () => {
|
||||
store.preference.telemetry = true;
|
||||
expect(preferenceSelectors.userAllowTrace(store)).toBe(true);
|
||||
|
||||
store.preference.telemetry = false;
|
||||
expect(preferenceSelectors.userAllowTrace(store)).toBe(false);
|
||||
|
||||
store.preference.telemetry = null;
|
||||
expect(preferenceSelectors.userAllowTrace(store)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hideSyncAlert', () => {
|
||||
it('should return the value of hideSyncAlert preference', () => {
|
||||
store.preference.hideSyncAlert = true;
|
||||
|
||||
@@ -3,8 +3,6 @@ import { DEFAULT_PREFERENCE } from '@lobechat/const';
|
||||
import type { UserState } from '@/store/user/initialState';
|
||||
|
||||
export const labPreferSelectors = {
|
||||
enableGroupChat: (s: UserState): boolean =>
|
||||
s.preference.lab?.enableGroupChat ?? DEFAULT_PREFERENCE.lab!.enableGroupChat!,
|
||||
enableInputMarkdown: (s: UserState): boolean =>
|
||||
s.preference.lab?.enableInputMarkdown ?? DEFAULT_PREFERENCE.lab!.enableInputMarkdown!,
|
||||
};
|
||||
|
||||
@@ -6,8 +6,6 @@ const useCmdEnterToSend = (s: UserStore): boolean => s.preference.useCmdEnterToS
|
||||
const topicDisplayMode = (s: UserStore) =>
|
||||
s.preference.topicDisplayMode || DEFAULT_PREFERENCE.topicDisplayMode;
|
||||
|
||||
const userAllowTrace = (s: UserStore) => s.preference.telemetry;
|
||||
|
||||
const hideSyncAlert = (s: UserStore) => s.preference.hideSyncAlert;
|
||||
|
||||
const hideSettingsMoveGuide = (s: UserStore) => s.preference.guide?.moveSettingsToAvatar;
|
||||
@@ -28,5 +26,4 @@ export const preferenceSelectors = {
|
||||
showUploadFileInKnowledgeBaseTip,
|
||||
topicDisplayMode,
|
||||
useCmdEnterToSend,
|
||||
userAllowTrace,
|
||||
};
|
||||
|
||||
@@ -100,7 +100,6 @@ exports[`settingsSelectors > defaultAgent > should merge DEFAULT_AGENT and s.set
|
||||
"config": {
|
||||
"chatConfig": {
|
||||
"autoCreateTopicThreshold": 2,
|
||||
"displayMode": "chat",
|
||||
"enableAutoCreateTopic": true,
|
||||
"enableCompressHistory": true,
|
||||
"enableHistoryCount": true,
|
||||
@@ -145,7 +144,6 @@ exports[`settingsSelectors > defaultAgentConfig > should merge DEFAULT_AGENT_CON
|
||||
{
|
||||
"chatConfig": {
|
||||
"autoCreateTopicThreshold": 2,
|
||||
"displayMode": "chat",
|
||||
"enableAutoCreateTopic": true,
|
||||
"enableCompressHistory": true,
|
||||
"enableHistoryCount": true,
|
||||
|
||||
@@ -19,7 +19,10 @@ describe('settingsSelectors', () => {
|
||||
animationMode: 'agile',
|
||||
fontSize: 12,
|
||||
highlighterTheme: 'lobe-theme',
|
||||
isDevMode: false,
|
||||
isLiteMode: false,
|
||||
mermaidTheme: 'lobe-theme',
|
||||
telemetry: true,
|
||||
transitionMode: 'fadeIn',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ const contextMenuMode = (s: UserStore) => {
|
||||
if (config !== undefined) return config;
|
||||
return isDesktop ? 'default' : 'disabled';
|
||||
};
|
||||
const telemetry = (s: UserStore) => generalConfig(s).telemetry;
|
||||
|
||||
export const userGeneralSettingsSelectors = {
|
||||
animationMode,
|
||||
@@ -27,5 +28,6 @@ export const userGeneralSettingsSelectors = {
|
||||
mermaidTheme,
|
||||
neutralColor,
|
||||
primaryColor,
|
||||
telemetry,
|
||||
transitionMode,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DEFAULT_AGENT_CONFIG,
|
||||
DEFAULT_AGENT_META,
|
||||
DEFAULT_HOTKEY_CONFIG,
|
||||
DEFAULT_MEMORY_SETTINGS,
|
||||
DEFAULT_SYSTEM_AGENT_CONFIG,
|
||||
DEFAULT_TTS_CONFIG,
|
||||
} from '@lobechat/const';
|
||||
@@ -27,6 +28,9 @@ export const getProviderConfigById = (provider: string) => (s: UserStore) =>
|
||||
|
||||
const currentImageSettings = (s: UserStore) => currentSettings(s).image;
|
||||
|
||||
const currentMemorySettings = (s: UserStore) =>
|
||||
merge(DEFAULT_MEMORY_SETTINGS, currentSettings(s).memory);
|
||||
|
||||
const currentTTS = (s: UserStore) => merge(DEFAULT_TTS_CONFIG, currentSettings(s).tts);
|
||||
|
||||
const defaultAgent = (s: UserStore) => merge(DEFAULT_AGENT, currentSettings(s).defaultAgent);
|
||||
@@ -44,6 +48,7 @@ const getHotkeyById = (id: HotkeyId) => (s: UserStore) =>
|
||||
|
||||
export const settingsSelectors = {
|
||||
currentImageSettings,
|
||||
currentMemorySettings,
|
||||
currentSettings,
|
||||
currentSystemAgent,
|
||||
currentTTS,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createDevtools } from '../middleware/createDevtools';
|
||||
import { type UserState, initialState } from './initialState';
|
||||
import { type UserAuthAction, createAuthSlice } from './slices/auth/action';
|
||||
import { type CommonAction, createCommonSlice } from './slices/common/action';
|
||||
import { type OnboardingAction, createOnboardingSlice } from './slices/onboarding/action';
|
||||
import { type PreferenceAction, createPreferenceSlice } from './slices/preference/action';
|
||||
import { type UserSettingsAction, createSettingsSlice } from './slices/settings/action';
|
||||
|
||||
@@ -16,7 +17,8 @@ export type UserStore = UserState &
|
||||
UserSettingsAction &
|
||||
PreferenceAction &
|
||||
UserAuthAction &
|
||||
CommonAction;
|
||||
CommonAction &
|
||||
OnboardingAction;
|
||||
|
||||
const createStore: StateCreator<UserStore, [['zustand/devtools', never]]> = (...parameters) => ({
|
||||
...initialState,
|
||||
@@ -24,6 +26,7 @@ const createStore: StateCreator<UserStore, [['zustand/devtools', never]]> = (...
|
||||
...createPreferenceSlice(...parameters),
|
||||
...createAuthSlice(...parameters),
|
||||
...createCommonSlice(...parameters),
|
||||
...createOnboardingSlice(...parameters),
|
||||
});
|
||||
|
||||
// =============== Implement useStore ============ //
|
||||
|
||||
Reference in New Issue
Block a user