@lobehub/chat 0.161.8 → 0.161.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/self-hosting/advanced/feature-flags.mdx +45 -0
  3. package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +42 -0
  4. package/docs/self-hosting/environment-variables/basic.mdx +11 -2
  5. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +11 -2
  6. package/package.json +1 -1
  7. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/DragUpload.tsx +92 -42
  8. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -2
  9. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +4 -3
  10. package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
  11. package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
  12. package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
  13. package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
  14. package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
  15. package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
  16. package/src/app/(main)/settings/system-agent/features/Translation.tsx +0 -2
  17. package/src/components/FileList/ImageFileItem.tsx +1 -1
  18. package/src/components/ModelProviderIcon/index.tsx +2 -2
  19. package/src/components/ModelSelect/index.tsx +5 -14
  20. package/src/const/meta.ts +1 -2
  21. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  22. package/src/hooks/useSyncData.ts +3 -1
  23. package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -1
  24. package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
  25. package/src/layout/GlobalProvider/index.tsx +1 -1
  26. package/src/locales/default/components.ts +1 -0
  27. package/src/services/message/client.test.ts +0 -24
  28. package/src/services/message/client.ts +0 -5
  29. package/src/services/message/type.ts +0 -1
  30. package/src/services/user/client.test.ts +100 -0
  31. package/src/services/user/client.ts +16 -14
  32. package/src/services/user/index.ts +0 -2
  33. package/src/services/user/type.ts +2 -4
  34. package/src/store/user/initialState.ts +10 -1
  35. package/src/store/user/selectors.ts +3 -7
  36. package/src/store/user/slices/auth/action.test.ts +5 -87
  37. package/src/store/user/slices/auth/action.ts +3 -58
  38. package/src/store/user/slices/auth/initialState.ts +2 -1
  39. package/src/store/user/slices/common/action.test.ts +196 -20
  40. package/src/store/user/slices/common/action.ts +55 -26
  41. package/src/store/user/slices/common/initialState.ts +9 -0
  42. package/src/store/user/slices/modelList/action.test.ts +363 -0
  43. package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
  44. package/src/store/user/slices/modelList/initialState.ts +15 -0
  45. package/src/store/user/slices/modelList/selectors/index.ts +2 -0
  46. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
  47. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
  48. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
  49. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
  50. package/src/store/user/slices/preference/action.test.ts +0 -52
  51. package/src/store/user/slices/preference/action.ts +1 -17
  52. package/src/store/user/slices/preference/initialState.ts +0 -5
  53. package/src/store/user/slices/preference/selectors.test.ts +2 -2
  54. package/src/store/user/slices/preference/selectors.ts +1 -1
  55. package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
  56. package/src/store/user/slices/settings/initialState.ts +0 -12
  57. package/src/store/user/slices/settings/selectors/index.ts +0 -3
  58. package/src/store/user/slices/sync/action.test.ts +19 -5
  59. package/src/store/user/slices/sync/action.ts +9 -6
  60. package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
  61. package/src/store/user/store.ts +5 -2
  62. package/src/styles/antdOverride.ts +6 -0
  63. package/src/types/serverConfig.ts +3 -1
  64. package/src/types/user/index.ts +13 -0
  65. package/src/utils/parseModels.test.ts +121 -1
  66. package/src/utils/parseModels.ts +9 -4
  67. package/src/store/user/slices/settings/actions/index.ts +0 -18
  68. package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
  69. /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
  70. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
  71. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
  72. /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
  73. /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
  74. /package/src/store/user/slices/settings/selectors/{selectors.test.ts → settings.test.ts} +0 -0
@@ -0,0 +1,78 @@
1
+ import { Modal } from '@lobehub/ui';
2
+ import { Button, FormInstance } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { memo, useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useUserStore } from '@/store/user';
8
+ import { modelConfigSelectors } from '@/store/user/slices/modelList/selectors';
9
+
10
+ import ModelConfigForm from './Form';
11
+
12
+ interface ModelConfigModalProps {
13
+ provider?: string;
14
+ showAzureDeployName?: boolean;
15
+ }
16
+
17
+ const ModelConfigModal = memo<ModelConfigModalProps>(({ showAzureDeployName, provider }) => {
18
+ const { t } = useTranslation('setting');
19
+ const { t: tc } = useTranslation('common');
20
+ const [formInstance, setFormInstance] = useState<FormInstance>();
21
+
22
+ const [open, id, editingProvider, dispatchCustomModelCards, toggleEditingCustomModelCard] =
23
+ useUserStore((s) => [
24
+ !!s.editingCustomCardModel && provider === s.editingCustomCardModel?.provider,
25
+ s.editingCustomCardModel?.id,
26
+ s.editingCustomCardModel?.provider,
27
+ s.dispatchCustomModelCards,
28
+ s.toggleEditingCustomModelCard,
29
+ ]);
30
+
31
+ const modelCard = useUserStore(
32
+ modelConfigSelectors.getCustomModelCard({ id, provider: editingProvider }),
33
+ isEqual,
34
+ );
35
+
36
+ const closeModal = () => {
37
+ toggleEditingCustomModelCard(undefined);
38
+ };
39
+
40
+ return (
41
+ <Modal
42
+ destroyOnClose
43
+ footer={[
44
+ <Button key="cancel" onClick={closeModal}>
45
+ {tc('cancel')}
46
+ </Button>,
47
+
48
+ <Button
49
+ key="ok"
50
+ onClick={() => {
51
+ if (!editingProvider || !id || !formInstance) return;
52
+ const data = formInstance.getFieldsValue();
53
+
54
+ dispatchCustomModelCards(editingProvider as any, { id, type: 'update', value: data });
55
+
56
+ closeModal();
57
+ }}
58
+ style={{ marginInlineStart: '16px' }}
59
+ type="primary"
60
+ >
61
+ {tc('ok')}
62
+ </Button>,
63
+ ]}
64
+ maskClosable
65
+ onCancel={closeModal}
66
+ open={open}
67
+ title={t('llm.customModelCards.modelConfig.modalTitle')}
68
+ zIndex={1251} // Select is 1150
69
+ >
70
+ <ModelConfigForm
71
+ initialValues={modelCard}
72
+ onFormInstanceReady={setFormInstance}
73
+ showAzureDeployName={showAzureDeployName}
74
+ />
75
+ </Modal>
76
+ );
77
+ });
78
+ export default ModelConfigModal;
@@ -1,6 +1,10 @@
1
+ import { ActionIcon, Tooltip } from '@lobehub/ui';
1
2
  import { Typography } from 'antd';
3
+ import { useTheme } from 'antd-style';
2
4
  import isEqual from 'fast-deep-equal';
5
+ import { Recycle } from 'lucide-react';
3
6
  import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
4
8
  import { Flexbox } from 'react-layout-kit';
5
9
 
6
10
  import ModelIcon from '@/components/ModelIcon';
@@ -16,25 +20,45 @@ interface OptionRenderProps {
16
20
  id: string;
17
21
  isAzure?: boolean;
18
22
  provider: GlobalLLMProviderKey;
23
+ removed?: boolean;
19
24
  }
20
- const OptionRender = memo<OptionRenderProps>(({ displayName, id, provider, isAzure }) => {
25
+ const OptionRender = memo<OptionRenderProps>(({ displayName, id, provider, isAzure, removed }) => {
21
26
  const model = useUserStore((s) => modelProviderSelectors.getModelCardById(id)(s), isEqual);
22
-
27
+ const { t } = useTranslation('components');
28
+ const theme = useTheme();
23
29
  // if there is isCustom, it means it is a user defined custom model
24
30
  if (model?.isCustom || isAzure) return <CustomModelOption id={id} provider={provider} />;
25
31
 
26
32
  return (
27
- <Flexbox align={'center'} gap={8} horizontal>
28
- <ModelIcon model={id} size={32} />
29
- <Flexbox>
30
- <Flexbox align={'center'} gap={8} horizontal>
31
- {displayName}
32
- <ModelInfoTags directionReverse placement={'top'} {...model!} />
33
+ <Flexbox
34
+ align={'center'}
35
+ gap={8}
36
+ horizontal
37
+ justify={'space-between'}
38
+ style={{ paddingInlineEnd: 8 }}
39
+ >
40
+ <Flexbox align={'center'} gap={8} horizontal>
41
+ <ModelIcon model={id} size={32} />
42
+ <Flexbox>
43
+ <Flexbox align={'center'} gap={8} horizontal>
44
+ {displayName}
45
+ <ModelInfoTags directionReverse placement={'top'} {...model!} />
46
+ </Flexbox>
47
+ <Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
48
+ {id}
49
+ </Typography.Text>
33
50
  </Flexbox>
34
- <Typography.Text style={{ fontSize: 12 }} type={'secondary'}>
35
- {id}
36
- </Typography.Text>
37
51
  </Flexbox>
52
+ {removed && (
53
+ <Tooltip
54
+ overlayStyle={{ maxWidth: 300 }}
55
+ placement={'top'}
56
+ style={{ pointerEvents: 'none' }}
57
+ title={t('ModelSelect.removed')}
58
+ >
59
+ <ActionIcon icon={Recycle} style={{ color: theme.colorWarning }} />
60
+ </Tooltip>
61
+ )}
38
62
  </Flexbox>
39
63
  );
40
64
  });
@@ -51,10 +51,9 @@ const ProviderModelListSelect = memo<CustomModelSelectProps>(
51
51
  ({ showModelFetcher = false, provider, showAzureDeployName, notFoundContent, placeholder }) => {
52
52
  const { t } = useTranslation('common');
53
53
  const { t: transSetting } = useTranslation('setting');
54
- const [setModelProviderConfig, dispatchCustomModelCards] = useUserStore((s) => [
54
+ const [setModelProviderConfig, updateEnabledModels] = useUserStore((s) => [
55
55
  s.setModelProviderConfig,
56
- s.dispatchCustomModelCards,
57
- s.useFetchProviderModelList,
56
+ s.updateEnabledModels,
58
57
  ]);
59
58
 
60
59
  const chatModelCards = useUserStore(
@@ -94,21 +93,7 @@ const ProviderModelListSelect = memo<CustomModelSelectProps>(
94
93
  mode="tags"
95
94
  notFoundContent={notFoundContent}
96
95
  onChange={(value, options) => {
97
- setModelProviderConfig(provider, { enabledModels: value.filter(Boolean) });
98
-
99
- // if there is a new model, add it to `customModelCards`
100
- options.forEach((option: { label?: string; value?: string }, index: number) => {
101
- // if is a known model, it should have value
102
- // if is an unknown model, the option will be {}
103
- if (option.value) return;
104
-
105
- const modelId = value[index];
106
-
107
- dispatchCustomModelCards(provider, {
108
- modelCard: { id: modelId },
109
- type: 'add',
110
- });
111
- });
96
+ updateEnabledModels(provider, value, options as any[]);
112
97
  }}
113
98
  optionFilterProp="label"
114
99
  optionRender={({ label, value }) => {
@@ -123,6 +108,18 @@ const ProviderModelListSelect = memo<CustomModelSelectProps>(
123
108
  />
124
109
  );
125
110
 
111
+ if (enabledModels?.some((m) => value === m)) {
112
+ return (
113
+ <OptionRender
114
+ displayName={label as string}
115
+ id={value as string}
116
+ isAzure={showAzureDeployName}
117
+ provider={provider}
118
+ removed
119
+ />
120
+ );
121
+ }
122
+
126
123
  // model is defined by user in client
127
124
  return (
128
125
  <Flexbox align={'center'} gap={8} horizontal>
@@ -3,7 +3,6 @@
3
3
  import { Form, type ItemGroup } from '@lobehub/ui';
4
4
  import { Form as AntForm } from 'antd';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { Globe } from 'lucide-react';
7
6
  import { memo } from 'react';
8
7
  import { useTranslation } from 'react-i18next';
9
8
 
@@ -40,7 +39,6 @@ const Translation = memo(() => {
40
39
  name: [SYSTEM_AGENT_SETTING_KEY, 'translation', 'model'],
41
40
  },
42
41
  ],
43
- icon: Globe,
44
42
  title: t('systemAgent.translation.title'),
45
43
  };
46
44
 
@@ -19,7 +19,7 @@ export const useStyles = createStyles(({ css, token }) => ({
19
19
  `,
20
20
  editableImage: css`
21
21
  background: ${token.colorBgContainer};
22
- border: 1px solid ${token.colorBorderSecondary};
22
+ box-shadow: 0 0 0 1px ${token.colorFill} inset;
23
23
  `,
24
24
  image: css`
25
25
  margin-block: 0 !important;
@@ -5,6 +5,7 @@ import {
5
5
  DeepSeek,
6
6
  Google,
7
7
  Groq,
8
+ LobeHub,
8
9
  Minimax,
9
10
  Mistral,
10
11
  Moonshot,
@@ -16,7 +17,6 @@ import {
16
17
  ZeroOne,
17
18
  Zhipu,
18
19
  } from '@lobehub/icons';
19
- import { Logo } from '@lobehub/ui';
20
20
  import { memo } from 'react';
21
21
  import { Center } from 'react-layout-kit';
22
22
 
@@ -29,7 +29,7 @@ interface ModelProviderIconProps {
29
29
  const ModelProviderIcon = memo<ModelProviderIconProps>(({ provider }) => {
30
30
  switch (provider) {
31
31
  case 'lobehub': {
32
- return <Logo size={20} />;
32
+ return <LobeHub size={20} />;
33
33
  }
34
34
 
35
35
  case ModelProvider.ZhiPu: {
@@ -79,10 +79,10 @@ export const ModelInfoTags = memo<ModelInfoTagsProps>(
79
79
  return (
80
80
  <Flexbox direction={directionReverse ? 'horizontal-reverse' : 'horizontal'} gap={4}>
81
81
  {model.files && (
82
- <Tooltip
82
+ <Tooltip
83
83
  overlayStyle={{ pointerEvents: 'none' }}
84
- placement={placement}
85
- title={t('ModelSelect.featureTag.file')}
84
+ placement={placement}
85
+ title={t('ModelSelect.featureTag.file')}
86
86
  >
87
87
  <div className={cx(styles.tag, styles.tagGreen)} style={{ cursor: 'pointer' }} title="">
88
88
  <Icon icon={LucidePaperclip} />
@@ -90,9 +90,9 @@ export const ModelInfoTags = memo<ModelInfoTagsProps>(
90
90
  </Tooltip>
91
91
  )}
92
92
  {model.vision && (
93
- <Tooltip
93
+ <Tooltip
94
94
  overlayStyle={{ pointerEvents: 'none' }}
95
- placement={placement}
95
+ placement={placement}
96
96
  title={t('ModelSelect.featureTag.vision')}
97
97
  >
98
98
  <div className={cx(styles.tag, styles.tagGreen)} style={{ cursor: 'pointer' }} title="">
@@ -128,15 +128,6 @@ export const ModelInfoTags = memo<ModelInfoTagsProps>(
128
128
  </Center>
129
129
  </Tooltip>
130
130
  )}
131
- {/*{model.isCustom && (*/}
132
- {/* <Tooltip*/}
133
- {/* overlayStyle={{ maxWidth: 300 }}*/}
134
- {/* placement={placement}*/}
135
- {/* title={t('ModelSelect.featureTag.custom')}*/}
136
- {/* >*/}
137
- {/* <Center className={styles.custom}>DIY</Center>*/}
138
- {/* </Tooltip>*/}
139
- {/*)}*/}
140
131
  </Flexbox>
141
132
  );
142
133
  },
package/src/const/meta.ts CHANGED
@@ -5,5 +5,4 @@ export const DEFAULT_USER_AVATAR = '😀';
5
5
  export const DEFAULT_BACKGROUND_COLOR = 'rgba(0,0,0,0)';
6
6
  export const DEFAULT_AGENT_META: MetaData = {};
7
7
  export const DEFAULT_INBOX_AVATAR = '🤯';
8
- export const DEFAULT_USER_AVATAR_URL =
9
- 'https://registry.npmmirror.com/@lobehub/assets-logo/1.2.0/files/assets/logo-3d.webp';
8
+ export const DEFAULT_USER_AVATAR_URL = '/icons/icon-192x192.png';
@@ -22,7 +22,7 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
22
22
  s.logout,
23
23
  s.openUserProfile,
24
24
  s.enableAuth(),
25
- s.enabledNextAuth(),
25
+ s.enabledNextAuth,
26
26
  ]);
27
27
  const { mainItems, logoutItems } = useMenu();
28
28
 
@@ -1,6 +1,7 @@
1
1
  import { useCallback } from 'react';
2
2
 
3
3
  import { useChatStore } from '@/store/chat';
4
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
4
5
  import { useSessionStore } from '@/store/session';
5
6
  import { useUserStore } from '@/store/user';
6
7
  import { syncSettingsSelectors } from '@/store/user/selectors';
@@ -42,7 +43,8 @@ export const useEnabledDataSync = () => {
42
43
  s.useEnabledSync,
43
44
  ]);
44
45
 
46
+ const { enableWebrtc } = useServerConfigStore(featureFlagsSelectors);
45
47
  const syncEvent = useSyncEvent();
46
48
 
47
- useEnabledSync(userEnableSync, userId, syncEvent);
49
+ useEnabledSync(enableWebrtc, { onEvent: syncEvent, userEnableSync, userId });
48
50
  };
@@ -112,7 +112,7 @@ export const useAppearance = () => {
112
112
  colorDanger: theme.colorError,
113
113
  colorInputBackground: theme.colorFillTertiary,
114
114
  colorNeutral: theme.colorText,
115
- colorPrimary: theme.colorPrimary,
115
+
116
116
  colorSuccess: theme.colorSuccess,
117
117
  colorText: theme.colorText,
118
118
  colorTextSecondary: theme.colorTextDescription,
@@ -10,32 +10,40 @@ import { useIsMobile } from '@/hooks/useIsMobile';
10
10
  import { useEnabledDataSync } from '@/hooks/useSyncData';
11
11
  import { useAgentStore } from '@/store/agent';
12
12
  import { useGlobalStore } from '@/store/global';
13
+ import { useServerConfigStore } from '@/store/serverConfig';
13
14
  import { useUserStore } from '@/store/user';
15
+ import { authSelectors } from '@/store/user/selectors';
14
16
 
15
17
  const StoreInitialization = memo(() => {
16
- const [useFetchServerConfig, useFetchUserConfig, useInitPreference] = useUserStore((s) => [
17
- s.useFetchServerConfig,
18
- s.useFetchUserConfig,
19
- s.useInitPreference,
18
+ const router = useRouter();
19
+
20
+ const [useInitUserState, isLogin] = useUserStore((s) => [
21
+ s.useInitUserState,
22
+ authSelectors.isLogin(s),
20
23
  ]);
24
+
25
+ const { serverConfig } = useServerConfigStore();
26
+
21
27
  const useInitGlobalPreference = useGlobalStore((s) => s.useInitGlobalPreference);
22
28
 
23
29
  const useFetchDefaultAgentConfig = useAgentStore((s) => s.useFetchDefaultAgentConfig);
24
30
  // init the system preference
25
- useInitPreference();
26
31
  useInitGlobalPreference();
27
-
28
32
  useFetchDefaultAgentConfig();
29
33
 
30
- const { isLoading } = useFetchServerConfig();
31
- useFetchUserConfig(!isLoading);
34
+ useInitUserState(isLogin, serverConfig, {
35
+ onSuccess: (state) => {
36
+ if (state.isOnboard === false) {
37
+ router.push('/onboard');
38
+ }
39
+ },
40
+ });
32
41
 
33
42
  useEnabledDataSync();
34
43
 
35
44
  const useStoreUpdater = createStoreUpdater(useGlobalStore);
36
45
 
37
46
  const mobile = useIsMobile();
38
- const router = useRouter();
39
47
 
40
48
  useStoreUpdater('isMobile', mobile);
41
49
  useStoreUpdater('router', router);
@@ -58,13 +58,13 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
58
58
  defaultNeutralColor={neutralColor?.value as any}
59
59
  defaultPrimaryColor={primaryColor?.value as any}
60
60
  >
61
- <StoreInitialization />
62
61
  <ServerConfigStoreProvider
63
62
  featureFlags={serverFeatureFlags}
64
63
  isMobile={isMobile}
65
64
  serverConfig={serverConfig}
66
65
  >
67
66
  {children}
67
+ <StoreInitialization />
68
68
  </ServerConfigStoreProvider>
69
69
  <DebugUI />
70
70
  </AppTheme>
@@ -7,6 +7,7 @@ export default {
7
7
  tokens: '该模型单个会话最多支持 {{tokens}} Tokens',
8
8
  vision: '该模型支持视觉识别',
9
9
  },
10
+ removed: '该模型不在列表中,若取消选中将会自动移除',
10
11
  },
11
12
  ModelSwitchPanel: {
12
13
  emptyModel: '没有启用的模型,请前往设置开启',
@@ -343,28 +343,4 @@ describe('MessageClientService', () => {
343
343
  expect(result).toBe(false);
344
344
  });
345
345
  });
346
-
347
- describe('messageCountToCheckTrace', () => {
348
- it('should return true if message count is greater than or equal to 4', async () => {
349
- // Setup
350
- (MessageModel.count as Mock).mockResolvedValue(5);
351
-
352
- // Execute
353
- const result = await messageService.messageCountToCheckTrace();
354
-
355
- // Assert
356
- expect(result).toBe(true);
357
- });
358
-
359
- it('should return false if message count is less than 4', async () => {
360
- // Setup
361
- (MessageModel.count as Mock).mockResolvedValue(3);
362
-
363
- // Execute
364
- const result = await messageService.messageCountToCheckTrace();
365
-
366
- // Assert
367
- expect(result).toBe(false);
368
- });
369
- });
370
346
  });
@@ -80,9 +80,4 @@ export class ClientService implements IMessageService {
80
80
  const number = await this.countMessages();
81
81
  return number > 0;
82
82
  }
83
-
84
- async messageCountToCheckTrace() {
85
- const number = await this.countMessages();
86
- return number >= 4;
87
- }
88
83
  }
@@ -43,5 +43,4 @@ export interface IMessageService {
43
43
  removeAllMessages(): Promise<any>;
44
44
 
45
45
  hasMessages(): Promise<boolean>;
46
- messageCountToCheckTrace(): Promise<boolean>;
47
46
  }
@@ -0,0 +1,100 @@
1
+ import { DeepPartial } from 'utility-types';
2
+ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { UserModel } from '@/database/client/models/user';
5
+ import { GlobalSettings } from '@/types/settings';
6
+ import { UserPreference } from '@/types/user';
7
+ import { AsyncLocalStorage } from '@/utils/localStorage';
8
+
9
+ import { ClientService } from './client';
10
+
11
+ vi.mock('@/database/client/models/user', () => ({
12
+ UserModel: {
13
+ getUser: vi.fn(),
14
+ updateSettings: vi.fn(),
15
+ resetSettings: vi.fn(),
16
+ updateAvatar: vi.fn(),
17
+ },
18
+ }));
19
+
20
+ const mockUser = {
21
+ avatar: 'avatar.png',
22
+ settings: { themeMode: 'light' } as unknown as GlobalSettings,
23
+ uuid: 'user-id',
24
+ };
25
+
26
+ const mockPreference = {
27
+ useCmdEnterToSend: true,
28
+ } as UserPreference;
29
+
30
+ describe('ClientService', () => {
31
+ let clientService: ClientService;
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ clientService = new ClientService();
36
+ });
37
+
38
+ it('should get user state correctly', async () => {
39
+ (UserModel.getUser as Mock).mockResolvedValue(mockUser);
40
+ const spyOn = vi
41
+ .spyOn(clientService['preferenceStorage'], 'getFromLocalStorage')
42
+ .mockResolvedValue(mockPreference);
43
+
44
+ const userState = await clientService.getUserState();
45
+
46
+ expect(userState).toEqual({
47
+ avatar: mockUser.avatar,
48
+ isOnboard: true,
49
+ canEnableTrace: false,
50
+ preference: mockPreference,
51
+ settings: mockUser.settings,
52
+ userId: mockUser.uuid,
53
+ });
54
+ expect(UserModel.getUser).toHaveBeenCalledTimes(1);
55
+ expect(spyOn).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it('should update user settings correctly', async () => {
59
+ const settingsPatch: DeepPartial<GlobalSettings> = { themeMode: 'dark' };
60
+ (UserModel.updateSettings as Mock).mockResolvedValue(undefined);
61
+
62
+ await clientService.updateUserSettings(settingsPatch);
63
+
64
+ expect(UserModel.updateSettings).toHaveBeenCalledWith(settingsPatch);
65
+ expect(UserModel.updateSettings).toHaveBeenCalledTimes(1);
66
+ });
67
+
68
+ it('should reset user settings correctly', async () => {
69
+ (UserModel.resetSettings as Mock).mockResolvedValue(undefined);
70
+
71
+ await clientService.resetUserSettings();
72
+
73
+ expect(UserModel.resetSettings).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it('should update user avatar correctly', async () => {
77
+ const newAvatar = 'new-avatar.png';
78
+ (UserModel.updateAvatar as Mock).mockResolvedValue(undefined);
79
+
80
+ await clientService.updateAvatar(newAvatar);
81
+
82
+ expect(UserModel.updateAvatar).toHaveBeenCalledWith(newAvatar);
83
+ expect(UserModel.updateAvatar).toHaveBeenCalledTimes(1);
84
+ });
85
+
86
+ it('should update user preference correctly', async () => {
87
+ const newPreference = {
88
+ useCmdEnterToSend: false,
89
+ } as UserPreference;
90
+
91
+ const spyOn = vi
92
+ .spyOn(clientService['preferenceStorage'], 'saveToLocalStorage')
93
+ .mockResolvedValue(undefined);
94
+
95
+ await clientService.updatePreference(newPreference);
96
+
97
+ expect(spyOn).toHaveBeenCalledWith(newPreference);
98
+ expect(spyOn).toHaveBeenCalledTimes(1);
99
+ });
100
+ });
@@ -1,27 +1,33 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
+ import { MessageModel } from '@/database/client/models/message';
3
4
  import { UserModel } from '@/database/client/models/user';
4
- import { IUserService } from '@/services/user/type';
5
5
  import { GlobalSettings } from '@/types/settings';
6
- import { UserPreference } from '@/types/user';
6
+ import { UserInitializationState, UserPreference } from '@/types/user';
7
7
  import { AsyncLocalStorage } from '@/utils/localStorage';
8
8
 
9
- export interface UserConfig {
10
- avatar?: string;
11
- settings: DeepPartial<GlobalSettings>;
12
- uuid: string;
13
- }
9
+ import { IUserService } from './type';
14
10
 
15
11
  export class ClientService implements IUserService {
16
12
  private preferenceStorage: AsyncLocalStorage<UserPreference>;
13
+
17
14
  constructor() {
18
15
  this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE');
19
16
  }
20
17
 
21
- getUserConfig = async () => {
18
+ async getUserState(): Promise<UserInitializationState> {
22
19
  const user = await UserModel.getUser();
23
- return user as unknown as UserConfig;
24
- };
20
+ const messageCount = await MessageModel.count();
21
+
22
+ return {
23
+ avatar: user.avatar,
24
+ canEnableTrace: messageCount >= 4,
25
+ isOnboard: true,
26
+ preference: await this.preferenceStorage.getFromLocalStorage(),
27
+ settings: user.settings as GlobalSettings,
28
+ userId: user.uuid,
29
+ };
30
+ }
25
31
 
26
32
  updateUserSettings = async (patch: DeepPartial<GlobalSettings>) => {
27
33
  return UserModel.updateSettings(patch);
@@ -35,10 +41,6 @@ export class ClientService implements IUserService {
35
41
  return UserModel.updateAvatar(avatar);
36
42
  }
37
43
 
38
- async getPreference() {
39
- return this.preferenceStorage.getFromLocalStorage();
40
- }
41
-
42
44
  async updatePreference(preference: UserPreference) {
43
45
  await this.preferenceStorage.saveToLocalStorage(preference);
44
46
  }
@@ -8,6 +8,4 @@
8
8
  // export const userService = ENABLED_SERVER_SERVICE ? new ServerService() : new ClientService();
9
9
  import { ClientService } from './client';
10
10
 
11
- export type { UserConfig } from './client';
12
-
13
11
  export const userService = new ClientService();
@@ -1,12 +1,10 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
- import { UserConfig } from '@/services/user/client';
4
3
  import { GlobalSettings } from '@/types/settings';
5
- import { UserPreference } from '@/types/user';
4
+ import { UserInitializationState, UserPreference } from '@/types/user';
6
5
 
7
6
  export interface IUserService {
8
- getPreference: () => Promise<UserPreference>;
9
- getUserConfig: () => Promise<UserConfig>;
7
+ getUserState: () => Promise<UserInitializationState>;
10
8
  resetUserSettings: () => Promise<any>;
11
9
  updateAvatar: (avatar: string) => Promise<any>;
12
10
  updatePreference: (preference: UserPreference) => Promise<any>;
@@ -1,13 +1,22 @@
1
1
  import { UserAuthState, initialAuthState } from './slices/auth/initialState';
2
+ import { CommonState, initialCommonState } from './slices/common/initialState';
3
+ import { ModelListState, initialModelListState } from './slices/modelList/initialState';
2
4
  import { UserPreferenceState, initialPreferenceState } from './slices/preference/initialState';
3
5
  import { UserSettingsState, initialSettingsState } from './slices/settings/initialState';
4
6
  import { UserSyncState, initialSyncState } from './slices/sync/initialState';
5
7
 
6
- export type UserState = UserSyncState & UserSettingsState & UserPreferenceState & UserAuthState;
8
+ export type UserState = UserSyncState &
9
+ UserSettingsState &
10
+ UserPreferenceState &
11
+ UserAuthState &
12
+ ModelListState &
13
+ CommonState;
7
14
 
8
15
  export const initialState: UserState = {
9
16
  ...initialSyncState,
10
17
  ...initialSettingsState,
11
18
  ...initialPreferenceState,
12
19
  ...initialAuthState,
20
+ ...initialCommonState,
21
+ ...initialModelListState,
13
22
  };