@lobehub/chat 1.42.6 → 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 (140) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/changelog/v1.json +12 -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 +4 -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/loading.tsx +2 -2
  35. package/src/app/(main)/discover/loading.tsx +2 -8
  36. package/src/app/(main)/files/loading.tsx +2 -2
  37. package/src/app/(main)/profile/(home)/Client.tsx +53 -0
  38. package/src/app/(main)/profile/(home)/[[...slugs]]/page.tsx +38 -0
  39. package/src/app/(main)/profile/@category/default.tsx +9 -0
  40. package/src/app/(main)/profile/@category/features/CategoryContent.tsx +38 -0
  41. package/src/app/(main)/profile/_layout/Desktop/Header.tsx +85 -0
  42. package/src/app/(main)/profile/_layout/Desktop/SideBar.tsx +42 -0
  43. package/src/app/(main)/profile/_layout/Desktop/index.tsx +48 -0
  44. package/src/app/(main)/profile/_layout/Mobile/Header.tsx +23 -5
  45. package/src/app/(main)/profile/_layout/Mobile/index.tsx +12 -5
  46. package/src/app/(main)/profile/_layout/type.ts +6 -0
  47. package/src/app/(main)/profile/error.tsx +5 -0
  48. package/src/app/(main)/profile/features/ClerkProfile.tsx +72 -0
  49. package/src/app/(main)/profile/hooks/useCategory.tsx +51 -0
  50. package/src/app/(main)/profile/layout.tsx +7 -17
  51. package/src/app/(main)/profile/loading.tsx +2 -22
  52. package/src/app/(main)/profile/not-found.tsx +3 -0
  53. package/src/app/(main)/profile/security/page.tsx +34 -0
  54. package/src/app/(main)/profile/stats/Client.tsx +52 -0
  55. package/src/app/(main)/profile/stats/features/AiHeatmaps.tsx +130 -0
  56. package/src/app/(main)/profile/stats/features/AssistantsRank.tsx +115 -0
  57. package/src/app/(main)/profile/stats/features/ModelsRank.tsx +84 -0
  58. package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +159 -0
  59. package/src/app/(main)/profile/stats/features/ShareButton/ShareModal.tsx +87 -0
  60. package/src/app/(main)/profile/stats/features/ShareButton/TotalCard.tsx +39 -0
  61. package/src/app/(main)/profile/stats/features/ShareButton/index.tsx +26 -0
  62. package/src/app/(main)/profile/stats/features/TimeLabel.tsx +30 -0
  63. package/src/app/(main)/profile/stats/features/TopicsRank.tsx +103 -0
  64. package/src/app/(main)/profile/stats/features/TotalAssistants.tsx +56 -0
  65. package/src/app/(main)/profile/stats/features/TotalMessages.tsx +56 -0
  66. package/src/app/(main)/profile/stats/features/TotalTopics.tsx +53 -0
  67. package/src/app/(main)/profile/stats/features/TotalWords.tsx +54 -0
  68. package/src/app/(main)/profile/stats/features/Welcome.tsx +86 -0
  69. package/src/app/(main)/profile/{[[...slugs]] → stats}/page.tsx +4 -5
  70. package/src/app/(main)/repos/[id]/evals/dataset/page.tsx +2 -2
  71. package/src/app/(main)/repos/[id]/evals/evaluation/page.tsx +2 -2
  72. package/src/app/(main)/settings/@category/features/CategoryContent.tsx +1 -1
  73. package/src/app/(main)/settings/_layout/Desktop/index.tsx +1 -1
  74. package/src/app/(main)/settings/_layout/Mobile/Header.tsx +1 -1
  75. package/src/app/(main)/settings/_layout/Mobile/index.tsx +2 -0
  76. package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -17
  77. package/src/app/(main)/settings/loading.tsx +2 -2
  78. package/src/components/Loading/BrandTextLoading/index.tsx +2 -2
  79. package/src/components/Statistic/index.tsx +15 -0
  80. package/src/components/StatisticCard/TitleWithPercentage.tsx +80 -0
  81. package/src/components/StatisticCard/growthPercentage.tsx +8 -0
  82. package/src/components/StatisticCard/index.tsx +209 -0
  83. package/src/const/url.ts +3 -3
  84. package/src/database/server/models/__tests__/message.test.ts +346 -35
  85. package/src/database/server/models/__tests__/session.test.ts +185 -2
  86. package/src/database/server/models/__tests__/topic.test.ts +136 -0
  87. package/src/database/server/models/__tests__/user.test.ts +140 -1
  88. package/src/database/server/models/message.ts +109 -14
  89. package/src/database/server/models/session.ts +75 -4
  90. package/src/database/server/models/topic.ts +43 -3
  91. package/src/database/server/models/user.ts +22 -0
  92. package/src/database/utils/genWhere.ts +39 -0
  93. package/src/features/ShareModal/ShareImage/index.tsx +11 -24
  94. package/src/features/ShareModal/ShareImage/type.ts +1 -6
  95. package/src/features/User/DataStatistics.tsx +21 -14
  96. package/src/features/User/UserPanel/PanelContent.tsx +12 -16
  97. package/src/features/User/UserPanel/useMenu.tsx +4 -6
  98. package/src/features/User/__tests__/PanelContent.test.tsx +4 -0
  99. package/src/features/User/__tests__/useMenu.test.tsx +1 -21
  100. package/src/hooks/useActiveTabKey.ts +34 -1
  101. package/src/{features/ShareModal/ShareImage → hooks}/useScreenshot.ts +51 -6
  102. package/src/locales/default/auth.ts +74 -2
  103. package/src/server/ld.test.ts +1 -1
  104. package/src/server/modules/AssistantStore/index.ts +3 -2
  105. package/src/server/routers/lambda/message.ts +35 -6
  106. package/src/server/routers/lambda/session.ts +17 -3
  107. package/src/server/routers/lambda/topic.ts +17 -3
  108. package/src/server/routers/lambda/user.ts +4 -0
  109. package/src/server/services/changelog/index.ts +1 -1
  110. package/src/services/message/_deprecated.ts +16 -0
  111. package/src/services/message/client.test.ts +0 -18
  112. package/src/services/message/client.ts +12 -9
  113. package/src/services/message/server.ts +12 -4
  114. package/src/services/message/type.ts +15 -3
  115. package/src/services/session/_deprecated.ts +5 -0
  116. package/src/services/session/client.ts +6 -2
  117. package/src/services/session/server.ts +6 -2
  118. package/src/services/session/type.ts +7 -1
  119. package/src/services/topic/_deprecated.ts +5 -0
  120. package/src/services/topic/client.ts +6 -2
  121. package/src/services/topic/server.ts +7 -1
  122. package/src/services/topic/type.ts +7 -2
  123. package/src/services/user/_deprecated.ts +4 -0
  124. package/src/services/user/client.ts +4 -0
  125. package/src/services/user/server.ts +4 -0
  126. package/src/services/user/type.ts +5 -0
  127. package/src/store/global/initialState.ts +6 -0
  128. package/src/store/user/slices/auth/action.test.ts +1 -33
  129. package/src/store/user/slices/auth/action.ts +0 -9
  130. package/src/store/user/slices/common/action.test.ts +2 -2
  131. package/src/types/message/index.ts +5 -0
  132. package/src/types/session/index.ts +8 -0
  133. package/src/types/topic/topic.ts +7 -0
  134. package/src/utils/format.ts +1 -1
  135. package/src/utils/time.ts +23 -0
  136. package/src/app/(main)/profile/[[...slugs]]/Client.tsx +0 -76
  137. package/src/components/Loading/BrandTextLoading/LobeChatText/SVG.tsx +0 -44
  138. package/src/components/Loading/BrandTextLoading/LobeChatText/index.tsx +0 -6
  139. package/src/components/Loading/BrandTextLoading/LobeChatText/style.css +0 -32
  140. package/src/hooks/useActiveSettingsKey.ts +0 -20
@@ -1,13 +1,20 @@
1
1
  import { Column, count, sql } from 'drizzle-orm';
2
- import { and, asc, desc, eq, inArray, like, not, or } from 'drizzle-orm/expressions';
2
+ import { and, asc, desc, eq, gt, inArray, isNull, like, not, or } from 'drizzle-orm/expressions';
3
3
 
4
4
  import { appEnv } from '@/config/app';
5
+ import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
5
6
  import { INBOX_SESSION_ID } from '@/const/session';
6
7
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
7
8
  import { LobeChatDatabase } from '@/database/type';
9
+ import {
10
+ genEndDateWhere,
11
+ genRangeWhere,
12
+ genStartDateWhere,
13
+ genWhere,
14
+ } from '@/database/utils/genWhere';
8
15
  import { idGenerator } from '@/database/utils/idGenerator';
9
16
  import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
10
- import { ChatSessionList, LobeAgentSession } from '@/types/session';
17
+ import { ChatSessionList, LobeAgentSession, SessionRankItem } from '@/types/session';
11
18
  import { merge } from '@/utils/merge';
12
19
 
13
20
  import {
@@ -19,6 +26,7 @@ import {
19
26
  agentsToSessions,
20
27
  sessionGroups,
21
28
  sessions,
29
+ topics,
22
30
  } from '../../schemas';
23
31
 
24
32
  export class SessionModel {
@@ -84,17 +92,80 @@ export class SessionModel {
84
92
  return { ...result, agent: (result?.agentsToSessions?.[0] as any)?.agent } as any;
85
93
  };
86
94
 
87
- count = async (): Promise<number> => {
95
+ count = async (params?: {
96
+ endDate?: string;
97
+ range?: [string, string];
98
+ startDate?: string;
99
+ }): Promise<number> => {
88
100
  const result = await this.db
89
101
  .select({
90
102
  count: count(sessions.id),
91
103
  })
92
104
  .from(sessions)
93
- .where(eq(sessions.userId, this.userId));
105
+ .where(
106
+ genWhere([
107
+ eq(sessions.userId, this.userId),
108
+ params?.range
109
+ ? genRangeWhere(params.range, sessions.createdAt, (date) => date.toDate())
110
+ : undefined,
111
+ params?.endDate
112
+ ? genEndDateWhere(params.endDate, sessions.createdAt, (date) => date.toDate())
113
+ : undefined,
114
+ params?.startDate
115
+ ? genStartDateWhere(params.startDate, sessions.createdAt, (date) => date.toDate())
116
+ : undefined,
117
+ ]),
118
+ );
94
119
 
95
120
  return result[0].count;
96
121
  };
97
122
 
123
+ _rank = async (limit: number = 10): Promise<SessionRankItem[]> => {
124
+ return this.db
125
+ .select({
126
+ avatar: agents.avatar,
127
+ backgroundColor: agents.backgroundColor,
128
+ count: count(topics.id).as('count'),
129
+ id: sessions.id,
130
+ title: agents.title,
131
+ })
132
+ .from(sessions)
133
+ .leftJoin(topics, eq(sessions.id, topics.sessionId))
134
+ .leftJoin(agentsToSessions, eq(sessions.id, agentsToSessions.sessionId))
135
+ .leftJoin(agents, eq(agentsToSessions.agentId, agents.id))
136
+ .groupBy(sessions.id, agentsToSessions.agentId, agents.id)
137
+ .having(({ count }) => gt(count, 0))
138
+ .orderBy(desc(sql`count`))
139
+ .limit(limit);
140
+ };
141
+
142
+ // TODO: 未来将 Inbox id 入库后可以直接使用 _rank 方法
143
+ rank = async (limit: number = 10): Promise<SessionRankItem[]> => {
144
+ const inboxResult = await this.db
145
+ .select({
146
+ count: count(topics.id).as('count'),
147
+ })
148
+ .from(topics)
149
+ .where(isNull(topics.sessionId));
150
+
151
+ const inboxCount = inboxResult[0].count;
152
+
153
+ if (!inboxCount || inboxCount === 0) return this._rank(limit);
154
+
155
+ const result = await this._rank(limit ? limit - 1 : undefined);
156
+
157
+ return [
158
+ {
159
+ avatar: DEFAULT_INBOX_AVATAR,
160
+ backgroundColor: null,
161
+ count: inboxCount,
162
+ id: INBOX_SESSION_ID,
163
+ title: 'inbox.title',
164
+ },
165
+ ...result,
166
+ ].sort((a, b) => b.count - a.count);
167
+ };
168
+
98
169
  hasMoreThanN = async (n: number): Promise<boolean> => {
99
170
  const result = await this.db
100
171
  .select({ id: sessions.id })
@@ -1,8 +1,15 @@
1
1
  import { Column, count, sql } from 'drizzle-orm';
2
- import { and, desc, eq, exists, inArray, isNull, like, or } from 'drizzle-orm/expressions';
2
+ import { and, desc, eq, exists, gt, inArray, isNull, like, or } from 'drizzle-orm/expressions';
3
3
 
4
4
  import { LobeChatDatabase } from '@/database/type';
5
+ import {
6
+ genEndDateWhere,
7
+ genRangeWhere,
8
+ genStartDateWhere,
9
+ genWhere,
10
+ } from '@/database/utils/genWhere';
5
11
  import { idGenerator } from '@/database/utils/idGenerator';
12
+ import { TopicRankItem } from '@/types/topic';
6
13
 
7
14
  import { NewMessage, TopicItem, messages, topics } from '../../schemas';
8
15
 
@@ -92,17 +99,50 @@ export class TopicModel {
92
99
  });
93
100
  };
94
101
 
95
- count = async (): Promise<number> => {
102
+ count = async (params?: {
103
+ endDate?: string;
104
+ range?: [string, string];
105
+ startDate?: string;
106
+ }): Promise<number> => {
96
107
  const result = await this.db
97
108
  .select({
98
109
  count: count(topics.id),
99
110
  })
100
111
  .from(topics)
101
- .where(eq(topics.userId, this.userId));
112
+ .where(
113
+ genWhere([
114
+ eq(topics.userId, this.userId),
115
+ params?.range
116
+ ? genRangeWhere(params.range, topics.createdAt, (date) => date.toDate())
117
+ : undefined,
118
+ params?.endDate
119
+ ? genEndDateWhere(params.endDate, topics.createdAt, (date) => date.toDate())
120
+ : undefined,
121
+ params?.startDate
122
+ ? genStartDateWhere(params.startDate, topics.createdAt, (date) => date.toDate())
123
+ : undefined,
124
+ ]),
125
+ );
102
126
 
103
127
  return result[0].count;
104
128
  };
105
129
 
130
+ rank = async (limit: number = 10): Promise<TopicRankItem[]> => {
131
+ return this.db
132
+ .select({
133
+ count: count(messages.id).as('count'),
134
+ id: topics.id,
135
+ sessionId: topics.sessionId,
136
+ title: topics.title,
137
+ })
138
+ .from(topics)
139
+ .leftJoin(messages, eq(topics.id, messages.topicId))
140
+ .groupBy(topics.id)
141
+ .orderBy(desc(sql`count`))
142
+ .having(({ count }) => gt(count, 0))
143
+ .limit(limit);
144
+ };
145
+
106
146
  // **************** Create *************** //
107
147
 
108
148
  create = async (
@@ -1,4 +1,5 @@
1
1
  import { TRPCError } from '@trpc/server';
2
+ import dayjs from 'dayjs';
2
3
  import { eq } from 'drizzle-orm/expressions';
3
4
  import { DeepPartial } from 'utility-types';
4
5
 
@@ -6,6 +7,7 @@ import { LobeChatDatabase } from '@/database/type';
6
7
  import { UserGuide, UserPreference } from '@/types/user';
7
8
  import { UserKeyVaults, UserSettings } from '@/types/user/settings';
8
9
  import { merge } from '@/utils/merge';
10
+ import { today } from '@/utils/time';
9
11
 
10
12
  import { NewUser, UserItem, UserSettingsItem, userSettings, users } from '../../schemas';
11
13
  import { SessionModel } from './session';
@@ -30,6 +32,26 @@ export class UserModel {
30
32
  this.db = db;
31
33
  }
32
34
 
35
+ getUserRegistrationDuration = async (): Promise<{
36
+ createdAt: string;
37
+ duration: number;
38
+ updatedAt: string;
39
+ }> => {
40
+ const user = await this.db.query.users.findFirst({ where: eq(users.id, this.userId) });
41
+ if (!user)
42
+ return {
43
+ createdAt: today().format('YYYY-MM-DD'),
44
+ duration: 1,
45
+ updatedAt: today().format('YYYY-MM-DD'),
46
+ };
47
+
48
+ return {
49
+ createdAt: dayjs(user.createdAt).format('YYYY-MM-DD'),
50
+ duration: dayjs().diff(dayjs(user.createdAt), 'day') + 1,
51
+ updatedAt: today().format('YYYY-MM-DD'),
52
+ };
53
+ };
54
+
33
55
  getUserState = async (decryptor: DecryptUserKeyVaults) => {
34
56
  const result = await this.db
35
57
  .select({
@@ -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;