@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.
- package/CHANGELOG.md +50 -0
- package/docs/self-hosting/advanced/feature-flags.mdx +45 -0
- package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +42 -0
- package/docs/self-hosting/environment-variables/basic.mdx +11 -2
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +11 -2
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/DragUpload.tsx +92 -42
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -2
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +4 -3
- package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
- package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
- package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
- package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
- package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
- package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
- package/src/app/(main)/settings/system-agent/features/Translation.tsx +0 -2
- package/src/components/FileList/ImageFileItem.tsx +1 -1
- package/src/components/ModelProviderIcon/index.tsx +2 -2
- package/src/components/ModelSelect/index.tsx +5 -14
- package/src/const/meta.ts +1 -2
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/hooks/useSyncData.ts +3 -1
- package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -1
- package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
- package/src/layout/GlobalProvider/index.tsx +1 -1
- package/src/locales/default/components.ts +1 -0
- package/src/services/message/client.test.ts +0 -24
- package/src/services/message/client.ts +0 -5
- package/src/services/message/type.ts +0 -1
- package/src/services/user/client.test.ts +100 -0
- package/src/services/user/client.ts +16 -14
- package/src/services/user/index.ts +0 -2
- package/src/services/user/type.ts +2 -4
- package/src/store/user/initialState.ts +10 -1
- package/src/store/user/selectors.ts +3 -7
- package/src/store/user/slices/auth/action.test.ts +5 -87
- package/src/store/user/slices/auth/action.ts +3 -58
- package/src/store/user/slices/auth/initialState.ts +2 -1
- package/src/store/user/slices/common/action.test.ts +196 -20
- package/src/store/user/slices/common/action.ts +55 -26
- package/src/store/user/slices/common/initialState.ts +9 -0
- package/src/store/user/slices/modelList/action.test.ts +363 -0
- package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
- package/src/store/user/slices/modelList/initialState.ts +15 -0
- package/src/store/user/slices/modelList/selectors/index.ts +2 -0
- package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
- package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
- package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
- package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
- package/src/store/user/slices/preference/action.test.ts +0 -52
- package/src/store/user/slices/preference/action.ts +1 -17
- package/src/store/user/slices/preference/initialState.ts +0 -5
- package/src/store/user/slices/preference/selectors.test.ts +2 -2
- package/src/store/user/slices/preference/selectors.ts +1 -1
- package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
- package/src/store/user/slices/settings/initialState.ts +0 -12
- package/src/store/user/slices/settings/selectors/index.ts +0 -3
- package/src/store/user/slices/sync/action.test.ts +19 -5
- package/src/store/user/slices/sync/action.ts +9 -6
- package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
- package/src/store/user/store.ts +5 -2
- package/src/styles/antdOverride.ts +6 -0
- package/src/types/serverConfig.ts +3 -1
- package/src/types/user/index.ts +13 -0
- package/src/utils/parseModels.test.ts +121 -1
- package/src/utils/parseModels.ts +9 -4
- package/src/store/user/slices/settings/actions/index.ts +0 -18
- package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
- /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
- /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
- /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
- /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
- /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
- /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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
54
|
+
const [setModelProviderConfig, updateEnabledModels] = useUserStore((s) => [
|
|
55
55
|
s.setModelProviderConfig,
|
|
56
|
-
s.
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
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';
|
package/src/hooks/useSyncData.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
s.
|
|
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
|
-
|
|
31
|
-
|
|
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>
|
|
@@ -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
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
+
async getUserState(): Promise<UserInitializationState> {
|
|
22
19
|
const user = await UserModel.getUser();
|
|
23
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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 &
|
|
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
|
};
|