@lobehub/chat 1.42.5 → 1.43.0

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 (141) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/docs/.cdn.cache.json +1 -0
  4. package/docs/changelog/2025-01-03-user-profile.mdx +27 -0
  5. package/docs/changelog/2025-01-03-user-profile.zh-CN.mdx +26 -0
  6. package/docs/self-hosting/advanced/auth/next-auth/wechat.mdx +3 -1
  7. package/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx +2 -2
  8. package/locales/ar/auth.json +76 -4
  9. package/locales/bg-BG/auth.json +75 -3
  10. package/locales/de-DE/auth.json +78 -6
  11. package/locales/en-US/auth.json +78 -6
  12. package/locales/es-ES/auth.json +75 -3
  13. package/locales/fa-IR/auth.json +77 -5
  14. package/locales/fr-FR/auth.json +78 -6
  15. package/locales/it-IT/auth.json +76 -4
  16. package/locales/ja-JP/auth.json +76 -4
  17. package/locales/ko-KR/auth.json +75 -3
  18. package/locales/nl-NL/auth.json +76 -4
  19. package/locales/pl-PL/auth.json +76 -4
  20. package/locales/pt-BR/auth.json +76 -4
  21. package/locales/ru-RU/auth.json +75 -3
  22. package/locales/tr-TR/auth.json +74 -3
  23. package/locales/vi-VN/auth.json +75 -3
  24. package/locales/zh-CN/auth.json +75 -3
  25. package/locales/zh-TW/auth.json +75 -3
  26. package/package.json +13 -3
  27. package/src/app/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +4 -0
  28. package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -46
  29. package/src/app/(main)/(mobile)/me/(home)/features/UserBanner.tsx +11 -14
  30. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +6 -21
  31. package/src/app/(main)/(mobile)/me/profile/features/Category.tsx +38 -21
  32. package/src/app/(main)/(mobile)/me/profile/layout.tsx +0 -3
  33. package/src/app/(main)/(mobile)/me/profile/page.tsx +3 -3
  34. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ByTimeMode/index.tsx +1 -1
  35. package/src/app/(main)/chat/loading.tsx +2 -2
  36. package/src/app/(main)/discover/loading.tsx +2 -8
  37. package/src/app/(main)/files/loading.tsx +2 -2
  38. package/src/app/(main)/profile/(home)/Client.tsx +53 -0
  39. package/src/app/(main)/profile/(home)/[[...slugs]]/page.tsx +38 -0
  40. package/src/app/(main)/profile/@category/default.tsx +9 -0
  41. package/src/app/(main)/profile/@category/features/CategoryContent.tsx +38 -0
  42. package/src/app/(main)/profile/_layout/Desktop/Header.tsx +85 -0
  43. package/src/app/(main)/profile/_layout/Desktop/SideBar.tsx +42 -0
  44. package/src/app/(main)/profile/_layout/Desktop/index.tsx +48 -0
  45. package/src/app/(main)/profile/_layout/Mobile/Header.tsx +23 -5
  46. package/src/app/(main)/profile/_layout/Mobile/index.tsx +12 -5
  47. package/src/app/(main)/profile/_layout/type.ts +6 -0
  48. package/src/app/(main)/profile/error.tsx +5 -0
  49. package/src/app/(main)/profile/features/ClerkProfile.tsx +72 -0
  50. package/src/app/(main)/profile/hooks/useCategory.tsx +51 -0
  51. package/src/app/(main)/profile/layout.tsx +7 -17
  52. package/src/app/(main)/profile/loading.tsx +2 -22
  53. package/src/app/(main)/profile/not-found.tsx +3 -0
  54. package/src/app/(main)/profile/security/page.tsx +34 -0
  55. package/src/app/(main)/profile/stats/Client.tsx +52 -0
  56. package/src/app/(main)/profile/stats/features/AiHeatmaps.tsx +130 -0
  57. package/src/app/(main)/profile/stats/features/AssistantsRank.tsx +115 -0
  58. package/src/app/(main)/profile/stats/features/ModelsRank.tsx +84 -0
  59. package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +159 -0
  60. package/src/app/(main)/profile/stats/features/ShareButton/ShareModal.tsx +87 -0
  61. package/src/app/(main)/profile/stats/features/ShareButton/TotalCard.tsx +39 -0
  62. package/src/app/(main)/profile/stats/features/ShareButton/index.tsx +26 -0
  63. package/src/app/(main)/profile/stats/features/TimeLabel.tsx +30 -0
  64. package/src/app/(main)/profile/stats/features/TopicsRank.tsx +103 -0
  65. package/src/app/(main)/profile/stats/features/TotalAssistants.tsx +56 -0
  66. package/src/app/(main)/profile/stats/features/TotalMessages.tsx +56 -0
  67. package/src/app/(main)/profile/stats/features/TotalTopics.tsx +53 -0
  68. package/src/app/(main)/profile/stats/features/TotalWords.tsx +54 -0
  69. package/src/app/(main)/profile/stats/features/Welcome.tsx +86 -0
  70. package/src/app/(main)/profile/{[[...slugs]] → stats}/page.tsx +4 -5
  71. package/src/app/(main)/repos/[id]/evals/dataset/page.tsx +2 -2
  72. package/src/app/(main)/repos/[id]/evals/evaluation/page.tsx +2 -2
  73. package/src/app/(main)/settings/@category/features/CategoryContent.tsx +1 -1
  74. package/src/app/(main)/settings/_layout/Desktop/index.tsx +1 -1
  75. package/src/app/(main)/settings/_layout/Mobile/Header.tsx +1 -1
  76. package/src/app/(main)/settings/_layout/Mobile/index.tsx +2 -0
  77. package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -17
  78. package/src/app/(main)/settings/loading.tsx +2 -2
  79. package/src/components/Loading/BrandTextLoading/index.tsx +2 -2
  80. package/src/components/Statistic/index.tsx +15 -0
  81. package/src/components/StatisticCard/TitleWithPercentage.tsx +80 -0
  82. package/src/components/StatisticCard/growthPercentage.tsx +8 -0
  83. package/src/components/StatisticCard/index.tsx +209 -0
  84. package/src/const/url.ts +3 -3
  85. package/src/database/server/models/__tests__/message.test.ts +346 -35
  86. package/src/database/server/models/__tests__/session.test.ts +185 -2
  87. package/src/database/server/models/__tests__/topic.test.ts +136 -0
  88. package/src/database/server/models/__tests__/user.test.ts +140 -1
  89. package/src/database/server/models/message.ts +109 -14
  90. package/src/database/server/models/session.ts +75 -4
  91. package/src/database/server/models/topic.ts +43 -3
  92. package/src/database/server/models/user.ts +22 -0
  93. package/src/database/utils/genWhere.ts +39 -0
  94. package/src/features/ShareModal/ShareImage/index.tsx +11 -24
  95. package/src/features/ShareModal/ShareImage/type.ts +1 -6
  96. package/src/features/User/DataStatistics.tsx +21 -14
  97. package/src/features/User/UserPanel/PanelContent.tsx +12 -16
  98. package/src/features/User/UserPanel/useMenu.tsx +4 -6
  99. package/src/features/User/__tests__/PanelContent.test.tsx +4 -0
  100. package/src/features/User/__tests__/useMenu.test.tsx +1 -21
  101. package/src/hooks/useActiveTabKey.ts +34 -1
  102. package/src/{features/ShareModal/ShareImage → hooks}/useScreenshot.ts +51 -6
  103. package/src/locales/default/auth.ts +74 -2
  104. package/src/server/ld.test.ts +1 -1
  105. package/src/server/modules/AssistantStore/index.ts +3 -2
  106. package/src/server/routers/lambda/message.ts +35 -6
  107. package/src/server/routers/lambda/session.ts +17 -3
  108. package/src/server/routers/lambda/topic.ts +17 -3
  109. package/src/server/routers/lambda/user.ts +4 -0
  110. package/src/server/services/changelog/index.ts +1 -1
  111. package/src/services/message/_deprecated.ts +16 -0
  112. package/src/services/message/client.test.ts +0 -18
  113. package/src/services/message/client.ts +12 -9
  114. package/src/services/message/server.ts +12 -4
  115. package/src/services/message/type.ts +15 -3
  116. package/src/services/session/_deprecated.ts +5 -0
  117. package/src/services/session/client.ts +6 -2
  118. package/src/services/session/server.ts +6 -2
  119. package/src/services/session/type.ts +7 -1
  120. package/src/services/topic/_deprecated.ts +5 -0
  121. package/src/services/topic/client.ts +6 -2
  122. package/src/services/topic/server.ts +7 -1
  123. package/src/services/topic/type.ts +7 -2
  124. package/src/services/user/_deprecated.ts +4 -0
  125. package/src/services/user/client.ts +4 -0
  126. package/src/services/user/server.ts +4 -0
  127. package/src/services/user/type.ts +5 -0
  128. package/src/store/global/initialState.ts +6 -0
  129. package/src/store/user/slices/auth/action.test.ts +1 -33
  130. package/src/store/user/slices/auth/action.ts +0 -9
  131. package/src/store/user/slices/common/action.test.ts +2 -2
  132. package/src/types/message/index.ts +5 -0
  133. package/src/types/session/index.ts +8 -0
  134. package/src/types/topic/topic.ts +7 -0
  135. package/src/utils/format.ts +1 -1
  136. package/src/utils/time.ts +23 -0
  137. package/src/app/(main)/profile/[[...slugs]]/Client.tsx +0 -76
  138. package/src/components/Loading/BrandTextLoading/LobeChatText/SVG.tsx +0 -44
  139. package/src/components/Loading/BrandTextLoading/LobeChatText/index.tsx +0 -6
  140. package/src/components/Loading/BrandTextLoading/LobeChatText/style.css +0 -32
  141. package/src/hooks/useActiveSettingsKey.ts +0 -20
@@ -0,0 +1,39 @@
1
+ import dayjs, { Dayjs } from 'dayjs';
2
+ import { SQL } from 'drizzle-orm';
3
+ import { and, gte, lte } from 'drizzle-orm/expressions';
4
+
5
+ export const genWhere = (sqls: (SQL<any> | undefined)[]): SQL<any> | undefined => {
6
+ const where = sqls.filter(Boolean);
7
+ if (where.length > 1) return and(...where);
8
+ return where[0];
9
+ };
10
+
11
+ export const genStartDateWhere = (
12
+ date: string | undefined,
13
+ key: any,
14
+ format: (date: Dayjs) => any,
15
+ ): SQL | undefined => {
16
+ if (!date || !dayjs(date).isValid()) return;
17
+ return gte(key, format(dayjs(new Date(date))));
18
+ };
19
+
20
+ export const genEndDateWhere = (
21
+ date: string | undefined,
22
+ key: any,
23
+ format: (date: Dayjs) => any,
24
+ ): SQL | undefined => {
25
+ if (!date || !dayjs(date).isValid()) return;
26
+ return lte(key, format(dayjs(new Date(date)).add(1, 'day')));
27
+ };
28
+
29
+ export const genRangeWhere = (
30
+ range: [string, string] | undefined,
31
+ key: any,
32
+ format: (date: Dayjs) => any,
33
+ ): SQL | undefined => {
34
+ if (!range) return;
35
+ if (!dayjs(range[0]).isValid() && !dayjs(range[1]).isValid()) return;
36
+ if (!dayjs(range[0]).isValid()) return genEndDateWhere(range[1], key, format);
37
+ if (!dayjs(range[1]).isValid()) return genStartDateWhere(range[0], key, format);
38
+ return and(genStartDateWhere(range[0], key, format), genEndDateWhere(range[1], key, format));
39
+ };
@@ -1,34 +1,17 @@
1
1
  import { Form, type FormItemProps } from '@lobehub/ui';
2
- import { Button, Segmented, SegmentedProps, Switch } from 'antd';
2
+ import { Button, Segmented, Switch } from 'antd';
3
3
  import { memo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import { FORM_STYLE } from '@/const/layoutTokens';
8
8
  import { useIsMobile } from '@/hooks/useIsMobile';
9
+ import { ImageType, imageTypeOptions, useScreenshot } from '@/hooks/useScreenshot';
10
+ import { useSessionStore } from '@/store/session';
11
+ import { sessionMetaSelectors } from '@/store/session/selectors';
9
12
 
10
13
  import Preview from './Preview';
11
- import { FieldType, ImageType } from './type';
12
- import { useScreenshot } from './useScreenshot';
13
-
14
- export const imageTypeOptions: SegmentedProps['options'] = [
15
- {
16
- label: 'JPG',
17
- value: ImageType.JPG,
18
- },
19
- {
20
- label: 'PNG',
21
- value: ImageType.PNG,
22
- },
23
- {
24
- label: 'SVG',
25
- value: ImageType.SVG,
26
- },
27
- {
28
- label: 'WEBP',
29
- value: ImageType.WEBP,
30
- },
31
- ];
14
+ import { FieldType } from './type';
32
15
 
33
16
  const DEFAULT_FIELD_VALUE: FieldType = {
34
17
  imageType: ImageType.JPG,
@@ -39,9 +22,13 @@ const DEFAULT_FIELD_VALUE: FieldType = {
39
22
  };
40
23
 
41
24
  const ShareImage = memo(() => {
25
+ const currentAgentTitle = useSessionStore(sessionMetaSelectors.currentAgentTitle);
42
26
  const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
43
- const { t } = useTranslation('chat');
44
- const { loading, onDownload, title } = useScreenshot(fieldValue.imageType);
27
+ const { t } = useTranslation(['chat', 'common']);
28
+ const { loading, onDownload, title } = useScreenshot({
29
+ imageType: fieldValue.imageType,
30
+ title: currentAgentTitle,
31
+ });
45
32
 
46
33
  const settings: FormItemProps[] = [
47
34
  {
@@ -1,9 +1,4 @@
1
- export enum ImageType {
2
- JPG = 'jpg',
3
- PNG = 'png',
4
- SVG = 'svg',
5
- WEBP = 'webp',
6
- }
1
+ import { ImageType } from '@/hooks/useScreenshot';
7
2
 
8
3
  export type FieldType = {
9
4
  imageType: ImageType;
@@ -3,17 +3,18 @@
3
3
  import { Icon, Tooltip } from '@lobehub/ui';
4
4
  import { Badge } from 'antd';
5
5
  import { createStyles } from 'antd-style';
6
- import { isNumber } from 'lodash-es';
6
+ import { isNumber, isUndefined } from 'lodash-es';
7
7
  import { LoaderCircle } from 'lucide-react';
8
8
  import { memo, useMemo } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { Flexbox, FlexboxProps } from 'react-layout-kit';
11
- import useSWR from 'swr';
12
11
 
12
+ import { useClientDataSWR } from '@/libs/swr';
13
13
  import { messageService } from '@/services/message';
14
14
  import { sessionService } from '@/services/session';
15
15
  import { topicService } from '@/services/topic';
16
16
  import { useServerConfigStore } from '@/store/serverConfig';
17
+ import { today } from '@/utils/time';
17
18
 
18
19
  const useStyles = createStyles(({ css, token }) => ({
19
20
  card: css`
@@ -21,6 +22,10 @@ const useStyles = createStyles(({ css, token }) => ({
21
22
  padding-inline: 8px;
22
23
  background: ${token.colorFillTertiary};
23
24
  border-radius: ${token.borderRadius}px;
25
+
26
+ &:hover {
27
+ background: ${token.colorFillSecondary};
28
+ }
24
29
  `,
25
30
  count: css`
26
31
  font-size: 16px;
@@ -57,21 +62,23 @@ const formatNumber = (num: any) => {
57
62
  const DataStatistics = memo<Omit<FlexboxProps, 'children'>>(({ style, ...rest }) => {
58
63
  const mobile = useServerConfigStore((s) => s.isMobile);
59
64
  // sessions
60
- const { data: sessions, isLoading: sessionsLoading } = useSWR(
61
- 'count-sessions',
62
- sessionService.countSessions,
65
+ const { data: sessions, isLoading: sessionsLoading } = useClientDataSWR('count-sessions', () =>
66
+ sessionService.countSessions(),
63
67
  );
64
68
  // topics
65
- const { data: topics, isLoading: topicsLoading } = useSWR(
66
- 'count-topics',
67
- topicService.countTopics,
69
+ const { data: topics, isLoading: topicsLoading } = useClientDataSWR('count-topics', () =>
70
+ topicService.countTopics(),
68
71
  );
69
72
  // messages
70
- const { data: messages, isLoading: messagesLoading } = useSWR(
73
+ const { data: { messages, messagesToday } = {}, isLoading: messagesLoading } = useClientDataSWR(
71
74
  'count-messages',
72
- messageService.countMessages,
75
+ async () => ({
76
+ messages: await messageService.countMessages(),
77
+ messagesToday: await messageService.countMessages({
78
+ startDate: today().format('YYYY-MM-DD'),
79
+ }),
80
+ }),
73
81
  );
74
- const { data: messagesToday } = useSWR('today-messages', messageService.countTodayMessages);
75
82
 
76
83
  const { styles, theme } = useStyles();
77
84
  const { t } = useTranslation('common');
@@ -80,17 +87,17 @@ const DataStatistics = memo<Omit<FlexboxProps, 'children'>>(({ style, ...rest })
80
87
 
81
88
  const items = [
82
89
  {
83
- count: sessionsLoading ? loading : sessions,
90
+ count: sessionsLoading || isUndefined(sessions) ? loading : sessions,
84
91
  key: 'sessions',
85
92
  title: t('dataStatistics.sessions'),
86
93
  },
87
94
  {
88
- count: topicsLoading ? loading : topics,
95
+ count: topicsLoading || isUndefined(topics) ? loading : topics,
89
96
  key: 'topics',
90
97
  title: t('dataStatistics.topics'),
91
98
  },
92
99
  {
93
- count: messagesLoading ? loading : messages,
100
+ count: messagesLoading || isUndefined(messages) ? loading : messages,
94
101
  countToady: messagesToday,
95
102
  key: 'messages',
96
103
  title: t('dataStatistics.messages'),
@@ -1,9 +1,11 @@
1
+ import Link from 'next/link';
1
2
  import { useRouter } from 'next/navigation';
2
3
  import { memo } from 'react';
3
4
  import { Flexbox } from 'react-layout-kit';
4
5
 
5
6
  import BrandWatermark from '@/components/BrandWatermark';
6
7
  import Menu from '@/components/Menu';
8
+ import { isDeprecatedEdition } from '@/const/version';
7
9
  import { useUserStore } from '@/store/user';
8
10
  import { authSelectors } from '@/store/user/selectors';
9
11
 
@@ -17,21 +19,14 @@ import { useMenu } from './useMenu';
17
19
  const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
18
20
  const router = useRouter();
19
21
  const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
20
- const [openSignIn, signOut, openUserProfile, enableAuth, enabledNextAuth] = useUserStore((s) => [
22
+ const [openSignIn, signOut, enableAuth, enabledNextAuth] = useUserStore((s) => [
21
23
  s.openLogin,
22
24
  s.logout,
23
- s.openUserProfile,
24
25
  s.enableAuth(),
25
26
  s.enabledNextAuth,
26
27
  ]);
27
28
  const { mainItems, logoutItems } = useMenu();
28
29
 
29
- const handleOpenProfile = () => {
30
- if (!enableAuth) return;
31
- openUserProfile();
32
- closePopover();
33
- };
34
-
35
30
  const handleSignIn = () => {
36
31
  openSignIn();
37
32
  closePopover();
@@ -47,15 +42,16 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
47
42
 
48
43
  return (
49
44
  <Flexbox gap={2} style={{ minWidth: 300 }}>
50
- {!enableAuth ? (
51
- <>
52
- <UserInfo />
53
- <DataStatistics />
54
- </>
55
- ) : isLoginWithAuth ? (
45
+ {!enableAuth || (enableAuth && isLoginWithAuth) ? (
56
46
  <>
57
- <UserInfo onClick={handleOpenProfile} />
58
- <DataStatistics />
47
+ <Link href={'/profile'} style={{ color: 'inherit' }}>
48
+ <UserInfo />
49
+ </Link>
50
+ {!isDeprecatedEdition && (
51
+ <Link href={'/profile/stats'} style={{ color: 'inherit' }}>
52
+ <DataStatistics />
53
+ </Link>
54
+ )}
59
55
  </>
60
56
  ) : (
61
57
  <UserLoginOrSignup onClick={handleSignIn} />
@@ -68,19 +68,17 @@ export const useMenu = () => {
68
68
  const hasNewVersion = useNewVersion();
69
69
  const { t } = useTranslation(['common', 'setting', 'auth']);
70
70
  const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors);
71
- const [isLogin, isLoginWithAuth, isLoginWithClerk, openUserProfile] = useUserStore((s) => [
71
+ const [enableAuth, isLogin, isLoginWithAuth] = useUserStore((s) => [
72
+ authSelectors.enabledAuth(s),
72
73
  authSelectors.isLogin(s),
73
74
  authSelectors.isLoginWithAuth(s),
74
- authSelectors.isLoginWithClerk(s),
75
- s.openUserProfile,
76
75
  ]);
77
76
 
78
77
  const profile: MenuProps['items'] = [
79
78
  {
80
79
  icon: <Icon icon={CircleUserRound} />,
81
80
  key: 'profile',
82
- label: t('userPanel.profile'),
83
- onClick: () => openUserProfile(),
81
+ label: <Link href={'/profile'}>{t('userPanel.profile')}</Link>,
84
82
  },
85
83
  ];
86
84
 
@@ -227,7 +225,7 @@ export const useMenu = () => {
227
225
  {
228
226
  type: 'divider',
229
227
  },
230
- ...(isLoginWithClerk ? profile : []),
228
+ ...(!enableAuth || (enableAuth && isLoginWithAuth) ? profile : []),
231
229
  ...(isLogin ? settings : []),
232
230
  /* ↓ cloud slot ↓ */
233
231
 
@@ -61,6 +61,10 @@ vi.mock('../DataStatistics', () => ({
61
61
  default: vi.fn(() => <div>Mocked DataStatistics</div>),
62
62
  }));
63
63
 
64
+ vi.mock('@/const/version', () => ({
65
+ isDeprecatedEdition: false,
66
+ }));
67
+
64
68
  // 定义一个变量来存储 enableAuth 的值
65
69
  let enableAuth = true;
66
70
 
@@ -76,26 +76,6 @@ describe('useMenu', () => {
76
76
 
77
77
  const { result } = renderHook(() => useMenu(), { wrapper });
78
78
 
79
- act(() => {
80
- const { mainItems, logoutItems } = result.current;
81
- expect(mainItems?.some((item) => item?.key === 'profile')).toBe(false);
82
- expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
83
- expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
84
- expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
85
- expect(mainItems?.some((item) => item?.key === 'changelog')).toBe(true);
86
- expect(logoutItems.some((item) => item?.key === 'logout')).toBe(true);
87
- });
88
- });
89
-
90
- it('should provide correct menu items when user is logged in with Clerk', () => {
91
- act(() => {
92
- useUserStore.setState({ isSignedIn: true });
93
- });
94
- enableAuth = true;
95
- enableClerk = true;
96
-
97
- const { result } = renderHook(() => useMenu(), { wrapper });
98
-
99
79
  act(() => {
100
80
  const { mainItems, logoutItems } = result.current;
101
81
  expect(mainItems?.some((item) => item?.key === 'profile')).toBe(true);
@@ -117,7 +97,7 @@ describe('useMenu', () => {
117
97
 
118
98
  act(() => {
119
99
  const { mainItems, logoutItems } = result.current;
120
- expect(mainItems?.some((item) => item?.key === 'profile')).toBe(false);
100
+ expect(mainItems?.some((item) => item?.key === 'profile')).toBe(true);
121
101
  expect(mainItems?.some((item) => item?.key === 'setting')).toBe(true);
122
102
  expect(mainItems?.some((item) => item?.key === 'import')).toBe(true);
123
103
  expect(mainItems?.some((item) => item?.key === 'export')).toBe(true);
@@ -1,6 +1,7 @@
1
1
  import { usePathname } from 'next/navigation';
2
2
 
3
- import { SidebarTabKey } from '@/store/global/initialState';
3
+ import { useQuery } from '@/hooks/useQuery';
4
+ import { ProfileTabs, SettingsTabs, SidebarTabKey } from '@/store/global/initialState';
4
5
 
5
6
  /**
6
7
  * Returns the active tab key (chat/market/settings/...)
@@ -10,3 +11,35 @@ export const useActiveTabKey = () => {
10
11
 
11
12
  return pathname.split('/').find(Boolean)! as SidebarTabKey;
12
13
  };
14
+
15
+ /**
16
+ * Returns the active setting page key (common/sync/agent/...)
17
+ */
18
+ export const useActiveSettingsKey = () => {
19
+ const pathname = usePathname();
20
+ const { tab } = useQuery();
21
+
22
+ const tabs = pathname.split('/').at(-1);
23
+
24
+ if (tabs === 'settings') return SettingsTabs.Common;
25
+
26
+ if (tabs === 'modal') return tab as SettingsTabs;
27
+
28
+ return tabs as SettingsTabs;
29
+ };
30
+
31
+ /**
32
+ * Returns the active profile page key (profile/security/stats/...)
33
+ */
34
+ export const useActiveProfileKey = () => {
35
+ const pathname = usePathname();
36
+ const { tab } = useQuery();
37
+
38
+ const tabs = pathname.split('/').at(-1);
39
+
40
+ if (tabs === 'profile') return ProfileTabs.Profile;
41
+
42
+ if (tabs === 'modal') return tab as ProfileTabs;
43
+
44
+ return tabs as ProfileTabs;
45
+ };
@@ -1,16 +1,48 @@
1
+ import { SegmentedProps } from 'antd';
1
2
  import dayjs from 'dayjs';
2
3
  import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
3
4
  import { useCallback, useState } from 'react';
4
5
 
5
6
  import { BRANDING_NAME } from '@/const/branding';
6
- import { useSessionStore } from '@/store/session';
7
- import { sessionMetaSelectors } from '@/store/session/selectors';
8
7
 
9
- import { ImageType } from './type';
8
+ export enum ImageType {
9
+ JPG = 'jpg',
10
+ PNG = 'png',
11
+ SVG = 'svg',
12
+ WEBP = 'webp',
13
+ }
10
14
 
11
- export const useScreenshot = (imageType: ImageType) => {
15
+ export const imageTypeOptions: SegmentedProps['options'] = [
16
+ {
17
+ label: 'JPG',
18
+ value: ImageType.JPG,
19
+ },
20
+ {
21
+ label: 'PNG',
22
+ value: ImageType.PNG,
23
+ },
24
+ {
25
+ label: 'SVG',
26
+ value: ImageType.SVG,
27
+ },
28
+ {
29
+ label: 'WEBP',
30
+ value: ImageType.WEBP,
31
+ },
32
+ ];
33
+
34
+ export const useScreenshot = ({
35
+ imageType,
36
+ title = 'share',
37
+ id = '#preview',
38
+ width,
39
+ }: {
40
+ id?: string;
41
+ imageType: ImageType;
42
+ title?: string;
43
+ width?: number;
44
+ }) => {
12
45
  const [loading, setLoading] = useState(false);
13
- const title = useSessionStore(sessionMetaSelectors.currentAgentTitle);
14
46
 
15
47
  const handleDownload = useCallback(async () => {
16
48
  setLoading(true);
@@ -35,13 +67,26 @@ export const useScreenshot = (imageType: ImageType) => {
35
67
  }
36
68
  }
37
69
 
38
- const dataUrl = await screenshotFn(document.querySelector('#preview') as HTMLDivElement, {
70
+ const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
71
+ let copy: HTMLDivElement = dom;
72
+
73
+ if (width) {
74
+ copy = dom.cloneNode(true) as HTMLDivElement;
75
+ copy.style.width = `${width}px`;
76
+ document.body.append(copy);
77
+ }
78
+
79
+ const dataUrl = await screenshotFn(width ? copy : dom, {
39
80
  features: {
40
81
  // 不启用移除控制符,否则会导致 safari emoji 报错
41
82
  removeControlCharacter: false,
42
83
  },
43
84
  scale: 2,
85
+ width,
44
86
  });
87
+
88
+ if (width && copy) copy?.remove();
89
+
45
90
  const link = document.createElement('a');
46
91
  link.download = `${BRANDING_NAME}_${title}_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
47
92
  link.href = dataUrl;
@@ -1,8 +1,80 @@
1
1
  export default {
2
+ date: {
3
+ prevMonth: '上个月',
4
+ recent30Days: '最近30天',
5
+ },
6
+ header: {
7
+ desc: '管理您的账户信息。',
8
+ title: '账户',
9
+ },
10
+ heatmaps: {
11
+ legend: {
12
+ less: '不活跃',
13
+ more: '活跃',
14
+ },
15
+ months: {
16
+ apr: '四月',
17
+ aug: '八月',
18
+ dec: '十二月',
19
+ feb: '二月',
20
+ jan: '一月',
21
+ jul: '七月',
22
+ jun: '六月',
23
+ mar: '三月',
24
+ may: '五月',
25
+ nov: '十一月',
26
+ oct: '十月',
27
+ sep: '九月',
28
+ },
29
+ tooltip: '{{date}} 当日发送 {{count}} 条消息',
30
+ totalCount: '过去一年共发送 {{count}} 条消息',
31
+ },
2
32
  login: '登录',
3
33
  loginOrSignup: '登录 / 注册',
4
- profile: '个人资料',
5
- security: '安全',
34
+ profile: {
35
+ avatar: '头像',
36
+ email: '电子邮件地址',
37
+ username: '用户名',
38
+ },
6
39
  signout: '退出登录',
7
40
  signup: '注册',
41
+ stats: {
42
+ aiheatmaps: 'AI 指数',
43
+ assistants: '助手数',
44
+ assistantsRank: {
45
+ left: '助手名称',
46
+ right: '话题数',
47
+ title: '助手使用率',
48
+ },
49
+ createdAt: '用户创建于',
50
+ days: '天',
51
+ empty: {
52
+ desc: '请积累更多聊天数据后查看',
53
+ title: '暂无数据',
54
+ },
55
+ lastYearActivity: '过去一年活跃度',
56
+ messages: '消息数',
57
+ modelsRank: {
58
+ left: '模型名称',
59
+ right: '消息数',
60
+ title: '模型使用率',
61
+ },
62
+ share: {
63
+ title: '我的 AI 活跃指数',
64
+ },
65
+ topics: '话题数',
66
+ topicsRank: {
67
+ left: '话题名称',
68
+ right: '消息数',
69
+ title: '话题内容量',
70
+ },
71
+ updatedAt: '数据更新至',
72
+ welcome: '{{username}}, 这是你和 {{appName}} 相伴的第 <span>{{days}}</span> 天',
73
+ words: '累计字数',
74
+ },
75
+ tab: {
76
+ profile: '个人资料',
77
+ security: '安全',
78
+ stats: '数据统计',
79
+ },
8
80
  };
@@ -29,7 +29,7 @@ describe('Ld', () => {
29
29
 
30
30
  expect(org['@type']).toBe('Organization');
31
31
  expect(org.name).toBe('LobeHub');
32
- expect(org.url).toBe('https://lobehub.com/');
32
+ expect(org.url).toBe('https://lobehub.com');
33
33
  });
34
34
  });
35
35
 
@@ -36,7 +36,7 @@ export class AssistantStore {
36
36
  }
37
37
 
38
38
  if (!res.ok) {
39
- console.error('fetch agent index error:', await res.text());
39
+ console.warn('fetch agent index error:', await res.text());
40
40
  return [];
41
41
  }
42
42
 
@@ -55,7 +55,8 @@ export class AssistantStore {
55
55
 
56
56
  return data;
57
57
  } catch (e) {
58
- console.error('fetch agent index error:', e);
58
+ console.error('[AgentIndexFetchError] failed to fetch agent index, error detail:');
59
+ console.error(e);
59
60
 
60
61
  throw e;
61
62
  }
@@ -27,12 +27,33 @@ export const messageRouter = router({
27
27
  return { added: data.rowCount as number, ids: [], skips: [], success: true };
28
28
  }),
29
29
 
30
- count: messageProcedure.query(async ({ ctx }) => {
31
- return ctx.messageModel.count();
32
- }),
33
- countToday: messageProcedure.query(async ({ ctx }) => {
34
- return ctx.messageModel.countToday();
35
- }),
30
+ count: messageProcedure
31
+ .input(
32
+ z
33
+ .object({
34
+ endDate: z.string().optional(),
35
+ range: z.tuple([z.string(), z.string()]).optional(),
36
+ startDate: z.string().optional(),
37
+ })
38
+ .optional(),
39
+ )
40
+ .query(async ({ ctx, input }) => {
41
+ return ctx.messageModel.count(input);
42
+ }),
43
+
44
+ countWords: messageProcedure
45
+ .input(
46
+ z
47
+ .object({
48
+ endDate: z.string().optional(),
49
+ range: z.tuple([z.string(), z.string()]).optional(),
50
+ startDate: z.string().optional(),
51
+ })
52
+ .optional(),
53
+ )
54
+ .query(async ({ ctx, input }) => {
55
+ return ctx.messageModel.countWords(input);
56
+ }),
36
57
 
37
58
  createMessage: messageProcedure
38
59
  .input(z.object({}).passthrough().partial())
@@ -56,6 +77,10 @@ export const messageRouter = router({
56
77
  return ctx.messageModel.queryBySessionId(input.sessionId);
57
78
  }),
58
79
 
80
+ getHeatmaps: messageProcedure.query(async ({ ctx }) => {
81
+ return ctx.messageModel.getHeatmaps();
82
+ }),
83
+
59
84
  // TODO: 未来这部分方法也需要使用 authedProcedure
60
85
  getMessages: publicProcedure
61
86
  .input(
@@ -74,6 +99,10 @@ export const messageRouter = router({
74
99
  return messageModel.query(input, { postProcessUrl: (path) => getFullFileUrl(path) });
75
100
  }),
76
101
 
102
+ rankModels: messageProcedure.query(async ({ ctx }) => {
103
+ return ctx.messageModel.rankModels();
104
+ }),
105
+
77
106
  removeAllMessages: messageProcedure.mutation(async ({ ctx }) => {
78
107
  return ctx.messageModel.deleteAllMessages();
79
108
  }),
@@ -57,9 +57,19 @@ export const sessionRouter = router({
57
57
  return data?.id;
58
58
  }),
59
59
 
60
- countSessions: sessionProcedure.query(async ({ ctx }) => {
61
- return ctx.sessionModel.count();
62
- }),
60
+ countSessions: sessionProcedure
61
+ .input(
62
+ z
63
+ .object({
64
+ endDate: z.string().optional(),
65
+ range: z.tuple([z.string(), z.string()]).optional(),
66
+ startDate: z.string().optional(),
67
+ })
68
+ .optional(),
69
+ )
70
+ .query(async ({ ctx, input }) => {
71
+ return ctx.sessionModel.count(input);
72
+ }),
63
73
 
64
74
  createSession: sessionProcedure
65
75
  .input(
@@ -103,6 +113,10 @@ export const sessionRouter = router({
103
113
  return ctx.sessionModel.query({ current, pageSize });
104
114
  }),
105
115
 
116
+ rankSessions: sessionProcedure.input(z.number().optional()).query(async ({ ctx, input }) => {
117
+ return ctx.sessionModel.rank(input);
118
+ }),
119
+
106
120
  removeAllSessions: sessionProcedure.mutation(async ({ ctx }) => {
107
121
  return ctx.sessionModel.deleteAll();
108
122
  }),