@lobehub/chat 0.152.10 → 0.152.12

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 (53) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/ar/common.json +14 -0
  3. package/locales/bg-BG/common.json +14 -0
  4. package/locales/de-DE/common.json +14 -0
  5. package/locales/en-US/common.json +14 -0
  6. package/locales/es-ES/common.json +14 -0
  7. package/locales/fr-FR/common.json +14 -0
  8. package/locales/it-IT/common.json +14 -0
  9. package/locales/ja-JP/common.json +14 -0
  10. package/locales/ko-KR/common.json +14 -0
  11. package/locales/nl-NL/common.json +14 -0
  12. package/locales/pl-PL/common.json +14 -0
  13. package/locales/pt-BR/common.json +14 -0
  14. package/locales/ru-RU/common.json +14 -0
  15. package/locales/tr-TR/common.json +14 -0
  16. package/locales/vi-VN/common.json +14 -0
  17. package/locales/zh-CN/common.json +14 -0
  18. package/locales/zh-TW/common.json +14 -0
  19. package/next.config.mjs +7 -0
  20. package/package.json +1 -1
  21. package/src/app/(main)/(mobile)/me/page.tsx +2 -2
  22. package/src/app/(main)/@nav/_layout/Desktop/Avatar.test.tsx +55 -0
  23. package/src/app/(main)/@nav/_layout/Desktop/Avatar.tsx +44 -2
  24. package/src/app/(main)/@nav/_layout/Desktop/BottomActions.tsx +4 -126
  25. package/src/app/(main)/@nav/_layout/Desktop/index.tsx +1 -1
  26. package/src/app/(main)/settings/common/features/Common.tsx +6 -6
  27. package/src/features/AgentSetting/AgentMeta/AutoGenerateInput.tsx +31 -26
  28. package/src/features/AgentSetting/AgentMeta/AutoGenerateSelect.tsx +3 -1
  29. package/src/features/AgentSetting/AgentMeta/index.tsx +2 -0
  30. package/src/features/AvatarWithUpload/index.tsx +8 -44
  31. package/src/features/DataImporter/index.tsx +11 -1
  32. package/src/features/User/UserAvatar.tsx +67 -0
  33. package/src/features/User/UserInfo.tsx +41 -0
  34. package/src/features/User/UserPanel/LangButton.tsx +57 -0
  35. package/src/features/User/UserPanel/Popover.tsx +35 -0
  36. package/src/features/User/UserPanel/ThemeButton.tsx +70 -0
  37. package/src/features/User/UserPanel/UserInfo.tsx +35 -0
  38. package/src/features/User/UserPanel/index.tsx +62 -0
  39. package/src/features/User/UserPanel/useMenu.tsx +158 -0
  40. package/src/features/User/UserPanel/useNewVersion.tsx +12 -0
  41. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +35 -0
  42. package/src/layout/AuthProvider/NextAuth/index.tsx +8 -1
  43. package/src/locales/default/common.ts +14 -0
  44. package/src/store/user/slices/auth/initialState.ts +6 -8
  45. package/src/store/user/slices/auth/selectors.ts +4 -1
  46. package/src/store/user/slices/preference/action.test.ts +41 -3
  47. package/src/store/user/slices/preference/action.ts +8 -2
  48. package/src/store/user/slices/preference/initialState.ts +14 -5
  49. package/src/store/user/slices/preference/selectors.test.ts +82 -0
  50. package/src/store/user/slices/preference/selectors.ts +4 -0
  51. package/src/store/user/slices/settings/actions/general.ts +8 -0
  52. package/src/types/user.ts +9 -0
  53. package/src/app/(main)/settings/page.tsx +0 -7
@@ -1,123 +1,14 @@
1
- import { ActionIcon, DiscordIcon, Icon } from '@lobehub/ui';
2
- import { Badge, ConfigProvider, Dropdown, MenuProps } from 'antd';
3
- import {
4
- Book,
5
- Feather,
6
- FileClock,
7
- Github,
8
- HardDriveDownload,
9
- HardDriveUpload,
10
- Heart,
11
- Settings,
12
- Settings2,
13
- } from 'lucide-react';
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Book, Github } from 'lucide-react';
14
3
  import Link from 'next/link';
15
- import { useRouter } from 'next/navigation';
16
4
  import { memo } from 'react';
17
5
  import { useTranslation } from 'react-i18next';
18
- import { Flexbox } from 'react-layout-kit';
19
6
 
20
- import { ABOUT, CHANGELOG, DISCORD, DOCUMENTS, FEEDBACK, GITHUB } from '@/const/url';
21
- import DataImporter from '@/features/DataImporter';
22
- import { configService } from '@/services/config';
23
- import { useGlobalStore } from '@/store/global';
24
- import { SidebarTabKey } from '@/store/global/initialState';
7
+ import { DOCUMENTS, GITHUB } from '@/const/url';
25
8
 
26
- export interface BottomActionProps {
27
- tab?: SidebarTabKey;
28
- }
29
-
30
- const BottomActions = memo<BottomActionProps>(({ tab }) => {
31
- const router = useRouter();
9
+ const BottomActions = memo(() => {
32
10
  const { t } = useTranslation('common');
33
11
 
34
- const [hasNewVersion, useCheckLatestVersion] = useGlobalStore((s) => [
35
- s.hasNewVersion,
36
- s.useCheckLatestVersion,
37
- ]);
38
-
39
- useCheckLatestVersion();
40
-
41
- const items: MenuProps['items'] = [
42
- {
43
- icon: <Icon icon={HardDriveUpload} />,
44
- key: 'import',
45
- label: <DataImporter>{t('import')}</DataImporter>,
46
- },
47
- {
48
- children: [
49
- {
50
- key: 'allAgent',
51
- label: <div>{t('exportType.allAgent')}</div>,
52
- onClick: configService.exportAgents,
53
- },
54
- {
55
- key: 'allAgentWithMessage',
56
- label: <div>{t('exportType.allAgentWithMessage')}</div>,
57
- onClick: configService.exportSessions,
58
- },
59
- {
60
- key: 'globalSetting',
61
- label: <div>{t('exportType.globalSetting')}</div>,
62
- onClick: configService.exportSettings,
63
- },
64
- {
65
- type: 'divider',
66
- },
67
- {
68
- key: 'all',
69
- label: <div>{t('exportType.all')}</div>,
70
- onClick: configService.exportAll,
71
- },
72
- ],
73
- icon: <Icon icon={HardDriveDownload} />,
74
- key: 'export',
75
- label: t('export'),
76
- },
77
- {
78
- type: 'divider',
79
- },
80
- {
81
- icon: <Icon icon={Feather} />,
82
- key: 'feedback',
83
- label: t('feedback'),
84
- onClick: () => window.open(FEEDBACK, '__blank'),
85
- },
86
- {
87
- icon: <Icon icon={FileClock} />,
88
- key: 'changelog',
89
- label: t('changelog'),
90
- onClick: () => window.open(CHANGELOG, '__blank'),
91
- },
92
- {
93
- icon: <Icon icon={DiscordIcon} />,
94
- key: 'wiki',
95
- label: 'Discord',
96
- onClick: () => window.open(DISCORD, '__blank'),
97
- },
98
- {
99
- icon: <Icon icon={Heart} />,
100
- key: 'about',
101
- label: t('about'),
102
- onClick: () => window.open(ABOUT, '__blank'),
103
- },
104
- {
105
- type: 'divider',
106
- },
107
- {
108
- icon: <Icon icon={Settings} />,
109
- key: 'setting',
110
- label: (
111
- <Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
112
- {t('setting')} {hasNewVersion && <Badge count={t('upgradeVersion.hasNew')} />}
113
- </Flexbox>
114
- ),
115
- onClick: () => {
116
- router.push('/settings/common');
117
- },
118
- },
119
- ];
120
-
121
12
  return (
122
13
  <>
123
14
  <Link aria-label={'GitHub'} href={GITHUB} target={'_blank'}>
@@ -126,19 +17,6 @@ const BottomActions = memo<BottomActionProps>(({ tab }) => {
126
17
  <Link aria-label={t('document')} href={DOCUMENTS} target={'_blank'}>
127
18
  <ActionIcon icon={Book} placement={'right'} title={t('document')} />
128
19
  </Link>
129
- <Dropdown arrow={false} menu={{ items }} trigger={['click']}>
130
- {hasNewVersion ? (
131
- <Flexbox>
132
- <ConfigProvider theme={{ components: { Badge: { dotSize: 8 } } }}>
133
- <Badge dot offset={[-4, 4]}>
134
- <ActionIcon active={tab === SidebarTabKey.Setting} icon={Settings2} />
135
- </Badge>
136
- </ConfigProvider>
137
- </Flexbox>
138
- ) : (
139
- <ActionIcon active={tab === SidebarTabKey.Setting} icon={Settings2} />
140
- )}
141
- </Dropdown>
142
20
  </>
143
21
  );
144
22
  });
@@ -14,7 +14,7 @@ const Nav = memo(() => {
14
14
  return (
15
15
  <SideNav
16
16
  avatar={<Avatar />}
17
- bottomActions={<BottomActions tab={sidebarKey} />}
17
+ bottomActions={<BottomActions />}
18
18
  style={{ height: '100%', zIndex: 100 }}
19
19
  topActions={<TopActions tab={sidebarKey} />}
20
20
  />
@@ -10,13 +10,12 @@ import { useTranslation } from 'react-i18next';
10
10
  import { useSyncSettings } from '@/app/(main)/settings/hooks/useSyncSettings';
11
11
  import { FORM_STYLE } from '@/const/layoutTokens';
12
12
  import { DEFAULT_SETTINGS } from '@/const/settings';
13
- import { useOAuthSession } from '@/hooks/useOAuthSession';
14
13
  import { useChatStore } from '@/store/chat';
15
14
  import { useFileStore } from '@/store/file';
16
15
  import { useSessionStore } from '@/store/session';
17
16
  import { useToolStore } from '@/store/tool';
18
17
  import { useUserStore } from '@/store/user';
19
- import { settingsSelectors } from '@/store/user/selectors';
18
+ import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors';
20
19
 
21
20
  type SettingItemGroup = ItemGroup;
22
21
 
@@ -29,7 +28,8 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin
29
28
  const { t } = useTranslation('setting');
30
29
  const [form] = Form.useForm();
31
30
 
32
- const { user, isOAuthLoggedIn } = useOAuthSession();
31
+ const isSignedIn = useUserStore((s) => s.isSignedIn);
32
+ const user = useUserStore(userProfileSelectors.userProfile, isEqual);
33
33
 
34
34
  const [clearSessions, clearSessionGroups] = useSessionStore((s) => [
35
35
  s.clearSessions,
@@ -110,18 +110,18 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin
110
110
  name: 'password',
111
111
  },
112
112
  {
113
- children: isOAuthLoggedIn ? (
113
+ children: isSignedIn ? (
114
114
  <Button onClick={handleSignOut}>{t('settingSystem.oauth.signout.action')}</Button>
115
115
  ) : (
116
116
  <Button onClick={handleSignIn} type="primary">
117
117
  {t('settingSystem.oauth.signin.action')}
118
118
  </Button>
119
119
  ),
120
- desc: isOAuthLoggedIn
120
+ desc: isSignedIn
121
121
  ? `${user?.email} ${t('settingSystem.oauth.info.desc')}`
122
122
  : t('settingSystem.oauth.signin.desc'),
123
123
  hidden: !showOAuthLogin,
124
- label: isOAuthLoggedIn
124
+ label: isSignedIn
125
125
  ? t('settingSystem.oauth.info.title')
126
126
  : t('settingSystem.oauth.signin.title'),
127
127
  minWidth: undefined,
@@ -5,37 +5,42 @@ import { Wand2 } from 'lucide-react';
5
5
  import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
+
8
9
  export interface AutoGenerateInputProps extends InputProps {
10
+ canAutoGenerate?: boolean;
9
11
  loading?: boolean;
10
12
  onGenerate?: () => void;
11
13
  }
12
14
 
13
- const AutoGenerateInput = memo<AutoGenerateInputProps>(({ loading, onGenerate, ...props }) => {
14
- const { t } = useTranslation('common');
15
- const theme = useTheme();
15
+ const AutoGenerateInput = memo<AutoGenerateInputProps>(
16
+ ({ loading, onGenerate, canAutoGenerate, ...props }) => {
17
+ const { t } = useTranslation('common');
18
+ const theme = useTheme();
16
19
 
17
- return (
18
- <Input
19
- suffix={
20
- onGenerate && (
21
- <ActionIcon
22
- active
23
- icon={Wand2}
24
- loading={loading}
25
- onClick={onGenerate}
26
- size={'small'}
27
- style={{
28
- color: theme.colorInfo,
29
- marginRight: -4,
30
- }}
31
- title={t('autoGenerate')}
32
- />
33
- )
34
- }
35
- type={'block'}
36
- {...props}
37
- />
38
- );
39
- });
20
+ return (
21
+ <Input
22
+ suffix={
23
+ onGenerate && (
24
+ <ActionIcon
25
+ active
26
+ disable={!canAutoGenerate}
27
+ icon={Wand2}
28
+ loading={loading}
29
+ onClick={onGenerate}
30
+ size="small"
31
+ style={{
32
+ color: theme.colorInfo,
33
+ marginRight: -4,
34
+ }}
35
+ title={t('autoGenerate')}
36
+ />
37
+ )
38
+ }
39
+ type="block"
40
+ {...props}
41
+ />
42
+ );
43
+ },
44
+ );
40
45
 
41
46
  export default AutoGenerateInput;
@@ -7,12 +7,13 @@ import { memo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
9
  export interface AutoGenerateInputProps extends SelectProps {
10
+ canAutoGenerate?: boolean;
10
11
  loading?: boolean;
11
12
  onGenerate?: () => void;
12
13
  }
13
14
 
14
15
  const AutoGenerateSelect = memo<AutoGenerateInputProps>(
15
- ({ loading, onGenerate, value, ...props }) => {
16
+ ({ loading, onGenerate, value, canAutoGenerate, ...props }) => {
16
17
  const { t } = useTranslation('common');
17
18
  const theme = useTheme();
18
19
 
@@ -25,6 +26,7 @@ const AutoGenerateSelect = memo<AutoGenerateInputProps>(
25
26
  onGenerate && (
26
27
  <ActionIcon
27
28
  active
29
+ disable={!canAutoGenerate}
28
30
  icon={Wand2}
29
31
  loading={loading}
30
32
  onClick={onGenerate}
@@ -63,6 +63,8 @@ const AgentMeta = memo(() => {
63
63
  return {
64
64
  children: (
65
65
  <AutoGenerate
66
+ canAutoGenerate={hasSystemRole}
67
+ disabled={!hasSystemRole}
66
68
  loading={loading[item.key as keyof SessionLoadingState]}
67
69
  onChange={item.onChange}
68
70
  onGenerate={() => {
@@ -1,49 +1,21 @@
1
1
  'use client';
2
2
 
3
3
  import { Upload } from 'antd';
4
- import { createStyles } from 'antd-style';
5
- import NextImage from 'next/image';
6
- import { CSSProperties, memo, useCallback } from 'react';
4
+ import { memo, useCallback } from 'react';
7
5
 
8
- import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
9
6
  import { useUserStore } from '@/store/user';
10
- import { userProfileSelectors } from '@/store/user/selectors';
11
7
  import { imageToBase64 } from '@/utils/imageToBase64';
12
8
  import { createUploadImageHandler } from '@/utils/uploadFIle';
13
9
 
14
- const useStyle = createStyles(
15
- ({ css, token }) => css`
16
- cursor: pointer;
17
- overflow: hidden;
18
- border-radius: 50%;
19
- transition:
20
- scale 400ms ${token.motionEaseOut},
21
- box-shadow 100ms ${token.motionEaseOut};
10
+ import UserAvatar, { type UserAvatarProps } from '../User/UserAvatar';
22
11
 
23
- &:hover {
24
- box-shadow: 0 0 0 3px ${token.colorText};
25
- }
26
-
27
- &:active {
28
- scale: 0.8;
29
- }
30
- `,
31
- );
32
-
33
- interface AvatarWithUploadProps {
12
+ interface AvatarWithUploadProps extends UserAvatarProps {
34
13
  compressSize?: number;
35
- id?: string;
36
- size?: number;
37
- style?: CSSProperties;
38
14
  }
39
15
 
40
16
  const AvatarWithUpload = memo<AvatarWithUploadProps>(
41
- ({ size = 40, compressSize = 256, style, id }) => {
42
- const { styles } = useStyle();
43
- const [avatar, updateAvatar] = useUserStore((s) => [
44
- userProfileSelectors.userAvatar(s),
45
- s.updateAvatar,
46
- ]);
17
+ ({ size = 40, compressSize = 256, ...rest }) => {
18
+ const updateAvatar = useUserStore((s) => s.updateAvatar);
47
19
 
48
20
  const handleUploadAvatar = useCallback(
49
21
  createUploadImageHandler((avatar) => {
@@ -58,17 +30,9 @@ const AvatarWithUpload = memo<AvatarWithUploadProps>(
58
30
  );
59
31
 
60
32
  return (
61
- <div className={styles} id={id} style={{ maxHeight: size, maxWidth: size, ...style }}>
62
- <Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
63
- <NextImage
64
- alt={avatar ? 'userAvatar' : 'LobeChat'}
65
- height={size}
66
- src={!!avatar ? avatar : DEFAULT_USER_AVATAR_URL}
67
- unoptimized
68
- width={size}
69
- />
70
- </Upload>
71
- </div>
33
+ <Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
34
+ <UserAvatar clickable size={size} {...rest} />
35
+ </Upload>
72
36
  );
73
37
  },
74
38
  );
@@ -14,6 +14,15 @@ const useStyles = createStyles(({ css, token }) => {
14
14
  const size = 28;
15
15
 
16
16
  return {
17
+ children: css`
18
+ &::before {
19
+ content: '';
20
+ position: absolute;
21
+ inset: 0;
22
+ background-color: transparent;
23
+ }
24
+ `,
25
+
17
26
  loader: css`
18
27
  transform: translateX(-${size * 2}px);
19
28
 
@@ -231,7 +240,8 @@ const DataImporter = memo<DataImporterProps>(({ children, onFinishImport }) => {
231
240
  maxCount={1}
232
241
  showUploadList={false}
233
242
  >
234
- {children}
243
+ {/* a very hackable solution: add a pseudo before to have a large hot zone */}
244
+ <div className={styles.children}>{children}</div>
235
245
  </Upload>
236
246
  </>
237
247
  );
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { Avatar, type AvatarProps } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
5
+ import { memo } from 'react';
6
+
7
+ import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
8
+ import { useUserStore } from '@/store/user';
9
+ import { userProfileSelectors } from '@/store/user/selectors';
10
+
11
+ const useStyles = createStyles(({ css, token }) => ({
12
+ clickable: css`
13
+ position: relative;
14
+ transition: all 200ms ease-out 0s;
15
+
16
+ &::before {
17
+ content: '';
18
+
19
+ position: absolute;
20
+ transform: skewX(-45deg) translateX(-400%);
21
+
22
+ overflow: hidden;
23
+
24
+ box-sizing: border-box;
25
+ width: 25%;
26
+ height: 100%;
27
+
28
+ background: rgba(255, 255, 255, 50%);
29
+
30
+ transition: all 200ms ease-out 0s;
31
+ }
32
+
33
+ &:hover {
34
+ box-shadow: 0 0 0 2px ${token.colorPrimary};
35
+
36
+ &::before {
37
+ transform: skewX(-45deg) translateX(400%);
38
+ }
39
+ }
40
+ `,
41
+ }));
42
+
43
+ export interface UserAvatarProps extends AvatarProps {
44
+ clickable?: boolean;
45
+ }
46
+
47
+ const UserAvatar = memo<UserAvatarProps>(
48
+ ({ size = 40, background, clickable, className, ...rest }) => {
49
+ const { styles, cx } = useStyles();
50
+ const avatar = useUserStore(userProfileSelectors.userAvatar);
51
+ return (
52
+ <Avatar
53
+ alt={avatar ? 'UserAvatar' : 'LobeChat'}
54
+ avatar={avatar || DEFAULT_USER_AVATAR_URL}
55
+ background={avatar ? background : undefined}
56
+ className={cx(clickable && styles.clickable, className)}
57
+ size={size}
58
+ unoptimized
59
+ {...rest}
60
+ />
61
+ );
62
+ },
63
+ );
64
+
65
+ UserAvatar.displayName = 'UserAvatar';
66
+
67
+ export default UserAvatar;
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { createStyles } from 'antd-style';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import UserAvatar from './UserAvatar';
9
+
10
+ const DEFAULT_USERNAME = 'LobeChat Community Edition';
11
+
12
+ const useStyles = createStyles(({ css, token }) => ({
13
+ nickname: css`
14
+ font-size: 16px;
15
+ font-weight: bold;
16
+ line-height: 1;
17
+ `,
18
+ username: css`
19
+ line-height: 1;
20
+ color: ${token.colorTextDescription};
21
+ `,
22
+ }));
23
+
24
+ const UserInfo = memo<{ onClick?: () => void }>(({ onClick }) => {
25
+ const { t } = useTranslation('common');
26
+ const { styles, theme } = useStyles();
27
+
28
+ const DEFAULT_NICKNAME = t('userPanel.defaultNickname');
29
+
30
+ return (
31
+ <Flexbox align={'center'} gap={12} horizontal paddingBlock={12} paddingInline={16}>
32
+ <UserAvatar background={theme.colorFill} onClick={onClick} size={48} />
33
+ <Flexbox flex={1} gap={6}>
34
+ <div className={styles.nickname}>{DEFAULT_NICKNAME}</div>
35
+ <div className={styles.username}>{DEFAULT_USERNAME}</div>
36
+ </Flexbox>
37
+ </Flexbox>
38
+ );
39
+ });
40
+
41
+ export default UserInfo;
@@ -0,0 +1,57 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Popover } from 'antd';
3
+ import { useTheme } from 'antd-style';
4
+ import { Languages } from 'lucide-react';
5
+ import { memo, useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import Menu, { type MenuProps } from '@/components/Menu';
9
+ import { localeOptions } from '@/locales/resources';
10
+ import { useUserStore } from '@/store/user';
11
+ import { settingsSelectors } from '@/store/user/selectors';
12
+
13
+ const LangButton = memo(() => {
14
+ const theme = useTheme();
15
+ const [language, switchLocale] = useUserStore((s) => [
16
+ settingsSelectors.currentSettings(s).language,
17
+ s.switchLocale,
18
+ ]);
19
+
20
+ const { t } = useTranslation('setting');
21
+
22
+ const items: MenuProps['items'] = useMemo(
23
+ () => [
24
+ {
25
+ key: 'auto',
26
+ label: t('settingTheme.lang.autoMode'),
27
+ onClick: () => switchLocale('auto'),
28
+ },
29
+ ...localeOptions.map((item) => ({
30
+ key: item.value,
31
+ label: item.label,
32
+ onClick: () => switchLocale(item.value),
33
+ })),
34
+ ],
35
+ [t],
36
+ );
37
+
38
+ return (
39
+ <Popover
40
+ arrow={false}
41
+ content={<Menu items={items} selectable selectedKeys={[language]} />}
42
+ overlayInnerStyle={{
43
+ padding: 0,
44
+ }}
45
+ placement={'right'}
46
+ trigger={['click', 'hover']}
47
+ >
48
+ <ActionIcon
49
+ icon={Languages}
50
+ size={{ blockSize: 32, fontSize: 16 }}
51
+ style={{ border: `1px solid ${theme.colorFillSecondary}` }}
52
+ />
53
+ </Popover>
54
+ );
55
+ });
56
+
57
+ export default LangButton;
@@ -0,0 +1,35 @@
1
+ import { memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import BrandWatermark from '@/components/BrandWatermark';
5
+ import Menu from '@/components/Menu';
6
+
7
+ import UserInfo from '../UserInfo';
8
+ import LangButton from './LangButton';
9
+ import ThemeButton from './ThemeButton';
10
+ import { useMenu } from './useMenu';
11
+
12
+ const PopoverContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
13
+ const { mainItems } = useMenu();
14
+
15
+ return (
16
+ <Flexbox gap={2} style={{ minWidth: 300 }}>
17
+ <UserInfo />
18
+ <Menu items={mainItems} onClick={closePopover} />
19
+ <Flexbox
20
+ align={'center'}
21
+ horizontal
22
+ justify={'space-between'}
23
+ style={{ padding: '6px 6px 6px 16px' }}
24
+ >
25
+ <BrandWatermark />
26
+ <Flexbox align={'center'} flex={'none'} gap={6} horizontal>
27
+ <LangButton />
28
+ <ThemeButton />
29
+ </Flexbox>
30
+ </Flexbox>
31
+ </Flexbox>
32
+ );
33
+ });
34
+
35
+ export default PopoverContent;
@@ -0,0 +1,70 @@
1
+ import { ActionIcon, Icon } from '@lobehub/ui';
2
+ import { Popover } from 'antd';
3
+ import { useTheme } from 'antd-style';
4
+ import { Monitor, Moon, Sun } from 'lucide-react';
5
+ import { memo, useMemo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+
8
+ import Menu, { type MenuProps } from '@/components/Menu';
9
+ import { useUserStore } from '@/store/user';
10
+ import { settingsSelectors } from '@/store/user/selectors';
11
+
12
+ const themeIcons = {
13
+ auto: Monitor,
14
+ dark: Moon,
15
+ light: Sun,
16
+ };
17
+
18
+ const ThemeButton = memo(() => {
19
+ const theme = useTheme();
20
+ const [themeMode, switchThemeMode] = useUserStore((s) => [
21
+ settingsSelectors.currentSettings(s).themeMode,
22
+ s.switchThemeMode,
23
+ ]);
24
+
25
+ const { t } = useTranslation('setting');
26
+
27
+ const items: MenuProps['items'] = useMemo(
28
+ () => [
29
+ {
30
+ icon: <Icon icon={themeIcons.auto} />,
31
+ key: 'auto',
32
+ label: t('settingTheme.themeMode.auto'),
33
+ onClick: () => switchThemeMode('auto'),
34
+ },
35
+ {
36
+ icon: <Icon icon={themeIcons.light} />,
37
+ key: 'light',
38
+ label: t('settingTheme.themeMode.light'),
39
+ onClick: () => switchThemeMode('light'),
40
+ },
41
+ {
42
+ icon: <Icon icon={themeIcons.dark} />,
43
+ key: 'dark',
44
+ label: t('settingTheme.themeMode.dark'),
45
+ onClick: () => switchThemeMode('dark'),
46
+ },
47
+ ],
48
+ [t],
49
+ );
50
+
51
+ return (
52
+ <Popover
53
+ arrow={false}
54
+ content={<Menu items={items} selectable selectedKeys={[themeMode]} />}
55
+ overlayInnerStyle={{
56
+ padding: 0,
57
+ }}
58
+ placement={'right'}
59
+ trigger={['click', 'hover']}
60
+ >
61
+ <ActionIcon
62
+ icon={themeIcons[themeMode]}
63
+ size={{ blockSize: 32, fontSize: 16 }}
64
+ style={{ border: `1px solid ${theme.colorFillSecondary}` }}
65
+ />
66
+ </Popover>
67
+ );
68
+ });
69
+
70
+ export default ThemeButton;