@lobehub/chat 0.152.9 → 0.152.11

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 (61) 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/features/AvatarBanner.tsx +9 -5
  22. package/src/app/(main)/(mobile)/me/features/Cate.tsx +1 -3
  23. package/src/app/(main)/(mobile)/me/features/ExtraCate.tsx +1 -3
  24. package/src/app/(main)/(mobile)/me/page.tsx +3 -3
  25. package/src/app/(main)/@nav/_layout/Desktop/Avatar.test.tsx +55 -0
  26. package/src/app/(main)/@nav/_layout/Desktop/Avatar.tsx +44 -2
  27. package/src/app/(main)/@nav/_layout/Desktop/BottomActions.tsx +4 -126
  28. package/src/app/(main)/@nav/_layout/Desktop/index.tsx +1 -1
  29. package/src/app/(main)/chat/features/SessionListContent/ListItem/index.tsx +5 -1
  30. package/src/app/(main)/market/features/AgentCard/index.tsx +4 -2
  31. package/src/app/(main)/market/features/AgentList.tsx +10 -1
  32. package/src/app/(main)/market/features/TagList.tsx +8 -4
  33. package/src/app/(main)/settings/_layout/Desktop/Header.tsx +1 -0
  34. package/src/app/(main)/settings/_layout/Mobile/Header.tsx +1 -1
  35. package/src/app/(main)/settings/common/features/Common.tsx +6 -6
  36. package/src/components/Cell/Divider.tsx +2 -2
  37. package/src/components/Cell/index.tsx +2 -1
  38. package/src/features/AvatarWithUpload/index.tsx +8 -44
  39. package/src/features/DataImporter/index.tsx +11 -1
  40. package/src/features/User/UserAvatar.tsx +67 -0
  41. package/src/features/User/UserInfo.tsx +41 -0
  42. package/src/features/User/UserPanel/LangButton.tsx +57 -0
  43. package/src/features/User/UserPanel/Popover.tsx +35 -0
  44. package/src/features/User/UserPanel/ThemeButton.tsx +70 -0
  45. package/src/features/User/UserPanel/UserInfo.tsx +35 -0
  46. package/src/features/User/UserPanel/index.tsx +62 -0
  47. package/src/features/User/UserPanel/useMenu.tsx +158 -0
  48. package/src/features/User/UserPanel/useNewVersion.tsx +12 -0
  49. package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +35 -0
  50. package/src/layout/AuthProvider/NextAuth/index.tsx +8 -1
  51. package/src/locales/default/common.ts +14 -0
  52. package/src/store/user/slices/auth/initialState.ts +6 -8
  53. package/src/store/user/slices/auth/selectors.ts +4 -1
  54. package/src/store/user/slices/preference/action.test.ts +41 -3
  55. package/src/store/user/slices/preference/action.ts +8 -2
  56. package/src/store/user/slices/preference/initialState.ts +14 -5
  57. package/src/store/user/slices/preference/selectors.test.ts +82 -0
  58. package/src/store/user/slices/preference/selectors.ts +4 -0
  59. package/src/store/user/slices/settings/actions/general.ts +8 -0
  60. package/src/types/user.ts +9 -0
  61. 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
  />
@@ -21,11 +21,14 @@ const useStyles = createStyles(({ css, token, responsive }) => {
21
21
  border-radius: 0;
22
22
  }
23
23
  `,
24
+ title: css`
25
+ line-height: 1.2;
26
+ `,
24
27
  };
25
28
  });
26
29
 
27
30
  const ListItem = memo<ListItemProps & { avatar: string; avatarBackground?: string }>(
28
- ({ avatar, avatarBackground, active, showAction, actions, ...props }) => {
31
+ ({ avatar, avatarBackground, active, showAction, actions, title, ...props }) => {
29
32
  const ref = useRef(null);
30
33
  const isHovering = useHover(ref);
31
34
  const { mobile } = useResponsive();
@@ -52,6 +55,7 @@ const ListItem = memo<ListItemProps & { avatar: string; avatarBackground?: strin
52
55
  className={styles.container}
53
56
  ref={ref}
54
57
  showAction={actions && (isHovering || showAction)}
58
+ title={<span className={styles.title}>{title}</span>}
55
59
  {...(props as any)}
56
60
  />
57
61
  );
@@ -28,12 +28,13 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
28
28
 
29
29
  background: ${token.colorBgContainer};
30
30
  border-radius: ${token.borderRadiusLG}px;
31
- box-shadow: 0 0 1px 1px ${token.colorFillQuaternary} inset;
31
+ box-shadow: 0 0 1px 1px ${isDarkMode ? token.colorFillQuaternary : token.colorFillSecondary}
32
+ inset;
32
33
 
33
34
  transition: box-shadow 0.2s ${token.motionEaseInOut};
34
35
 
35
36
  &:hover {
36
- box-shadow: 0 0 1px 1px ${token.colorFillSecondary} inset;
37
+ box-shadow: 0 0 1px 1px ${isDarkMode ? token.colorFillSecondary : token.colorFill} inset;
37
38
  }
38
39
  `,
39
40
  desc: css`
@@ -50,6 +51,7 @@ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
50
51
  title: css`
51
52
  margin-bottom: 0 !important;
52
53
  font-size: 18px !important;
54
+ font-weight: bold;
53
55
  `,
54
56
  }));
55
57
 
@@ -58,7 +58,16 @@ const AgentList = memo<AgentListProps>(({ mobile }) => {
58
58
  [],
59
59
  );
60
60
 
61
- if (isLoading) return <Skeleton paragraph={{ rows: 8 }} title={false} />;
61
+ if (isLoading || (!searchKeywords && agentList?.length === 0)) {
62
+ return (
63
+ <>
64
+ <h2 className={styles.title}>{t('title.recentSubmits')}</h2>
65
+ <Skeleton paragraph={{ rows: 8 }} title={false} />
66
+ <h2 className={styles.title}>{t('title.allAgents')}</h2>
67
+ <Skeleton paragraph={{ rows: 8 }} title={false} />
68
+ </>
69
+ );
70
+ }
62
71
 
63
72
  if (searchKeywords) {
64
73
  if (agentList.length === 0) return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Button } from 'antd';
3
+ import { Button, Skeleton } from 'antd';
4
4
  import { createStyles, useResponsive } from 'antd-style';
5
5
  import isEqual from 'fast-deep-equal';
6
6
  import { startCase } from 'lodash-es';
@@ -9,7 +9,7 @@ import { Flexbox } from 'react-layout-kit';
9
9
 
10
10
  import { agentMarketSelectors, useMarketStore } from '@/store/market';
11
11
 
12
- const useStyles = createStyles(({ css, token }) => ({
12
+ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
13
13
  active: css`
14
14
  color: ${token.colorBgLayout};
15
15
  background: ${token.colorPrimary};
@@ -20,11 +20,11 @@ const useStyles = createStyles(({ css, token }) => ({
20
20
  }
21
21
  `,
22
22
  tag: css`
23
- background: ${token.colorBgContainer};
23
+ background: ${isDarkMode ? token.colorBgContainer : token.colorFillTertiary};
24
24
  border: none;
25
25
 
26
26
  &:hover {
27
- background: ${token.colorBgElevated} !important;
27
+ background: ${isDarkMode ? token.colorBgElevated : token.colorFill} !important;
28
28
  }
29
29
  `,
30
30
  }));
@@ -38,6 +38,10 @@ const TagList = memo(() => {
38
38
  ]);
39
39
  const agentTagList = useMarketStore(agentMarketSelectors.getAgentTagList, isEqual);
40
40
 
41
+ if (agentTagList?.length === 0) {
42
+ return <Skeleton paragraph={{ rows: 4 }} title={false} />;
43
+ }
44
+
41
45
  const list = md ? agentTagList : agentTagList.slice(0, 20);
42
46
 
43
47
  return (
@@ -21,6 +21,7 @@ const useStyles = createStyles(({ token, css }) => ({
21
21
  title: css`
22
22
  font-size: 18px;
23
23
  font-weight: 700;
24
+ line-height: 1.2;
24
25
  `,
25
26
  }));
26
27
 
@@ -22,7 +22,7 @@ const Header = memo(() => {
22
22
  <MobileNavBarTitle
23
23
  title={
24
24
  <Flexbox align={'center'} gap={4} horizontal>
25
- {t(`tab.${activeSettingsKey}`)}
25
+ <span style={{ lineHeight: 1.2 }}> {t(`tab.${activeSettingsKey}`)}</span>
26
26
  {activeSettingsKey === SettingsTabs.Sync && (
27
27
  <Tag color={'gold'}>{t('tab.experiment')}</Tag>
28
28
  )}
@@ -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,
@@ -4,9 +4,9 @@ import { createStyles } from 'antd-style';
4
4
  import { memo } from 'react';
5
5
 
6
6
  const useStyles = createStyles(
7
- ({ css, token }) => css`
7
+ ({ css, token, isDarkMode }) => css`
8
8
  height: 6px;
9
- background: ${token.colorBgLayout};
9
+ background: ${isDarkMode ? token.colorBgContainer : token.colorBgLayout};
10
10
  `,
11
11
  );
12
12
 
@@ -5,10 +5,11 @@ import { ReactNode, memo } from 'react';
5
5
 
6
6
  const { Item } = List;
7
7
 
8
- const useStyles = createStyles(({ css }) => ({
8
+ const useStyles = createStyles(({ css, token, isDarkMode }) => ({
9
9
  container: css`
10
10
  position: relative;
11
11
  padding-block: 16px !important;
12
+ background: ${isDarkMode ? token.colorBgLayout : token.colorBgContainer};
12
13
  border-radius: 0;
13
14
  `,
14
15
  }));
@@ -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;