@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.
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +12 -0
- package/docs/.cdn.cache.json +1 -0
- package/docs/changelog/2025-01-03-user-profile.mdx +27 -0
- package/docs/changelog/2025-01-03-user-profile.zh-CN.mdx +26 -0
- package/docs/self-hosting/advanced/auth/next-auth/wechat.mdx +3 -1
- package/docs/self-hosting/advanced/auth/next-auth/wechat.zh-CN.mdx +2 -2
- package/locales/ar/auth.json +76 -4
- package/locales/bg-BG/auth.json +75 -3
- package/locales/de-DE/auth.json +78 -6
- package/locales/en-US/auth.json +78 -6
- package/locales/es-ES/auth.json +75 -3
- package/locales/fa-IR/auth.json +77 -5
- package/locales/fr-FR/auth.json +78 -6
- package/locales/it-IT/auth.json +76 -4
- package/locales/ja-JP/auth.json +76 -4
- package/locales/ko-KR/auth.json +75 -3
- package/locales/nl-NL/auth.json +76 -4
- package/locales/pl-PL/auth.json +76 -4
- package/locales/pt-BR/auth.json +76 -4
- package/locales/ru-RU/auth.json +75 -3
- package/locales/tr-TR/auth.json +74 -3
- package/locales/vi-VN/auth.json +75 -3
- package/locales/zh-CN/auth.json +75 -3
- package/locales/zh-TW/auth.json +75 -3
- package/package.json +4 -3
- package/src/app/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +4 -0
- package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -46
- package/src/app/(main)/(mobile)/me/(home)/features/UserBanner.tsx +11 -14
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +6 -21
- package/src/app/(main)/(mobile)/me/profile/features/Category.tsx +38 -21
- package/src/app/(main)/(mobile)/me/profile/layout.tsx +0 -3
- package/src/app/(main)/(mobile)/me/profile/page.tsx +3 -3
- package/src/app/(main)/chat/loading.tsx +2 -2
- package/src/app/(main)/discover/loading.tsx +2 -8
- package/src/app/(main)/files/loading.tsx +2 -2
- package/src/app/(main)/profile/(home)/Client.tsx +53 -0
- package/src/app/(main)/profile/(home)/[[...slugs]]/page.tsx +38 -0
- package/src/app/(main)/profile/@category/default.tsx +9 -0
- package/src/app/(main)/profile/@category/features/CategoryContent.tsx +38 -0
- package/src/app/(main)/profile/_layout/Desktop/Header.tsx +85 -0
- package/src/app/(main)/profile/_layout/Desktop/SideBar.tsx +42 -0
- package/src/app/(main)/profile/_layout/Desktop/index.tsx +48 -0
- package/src/app/(main)/profile/_layout/Mobile/Header.tsx +23 -5
- package/src/app/(main)/profile/_layout/Mobile/index.tsx +12 -5
- package/src/app/(main)/profile/_layout/type.ts +6 -0
- package/src/app/(main)/profile/error.tsx +5 -0
- package/src/app/(main)/profile/features/ClerkProfile.tsx +72 -0
- package/src/app/(main)/profile/hooks/useCategory.tsx +51 -0
- package/src/app/(main)/profile/layout.tsx +7 -17
- package/src/app/(main)/profile/loading.tsx +2 -22
- package/src/app/(main)/profile/not-found.tsx +3 -0
- package/src/app/(main)/profile/security/page.tsx +34 -0
- package/src/app/(main)/profile/stats/Client.tsx +52 -0
- package/src/app/(main)/profile/stats/features/AiHeatmaps.tsx +130 -0
- package/src/app/(main)/profile/stats/features/AssistantsRank.tsx +115 -0
- package/src/app/(main)/profile/stats/features/ModelsRank.tsx +84 -0
- package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +159 -0
- package/src/app/(main)/profile/stats/features/ShareButton/ShareModal.tsx +87 -0
- package/src/app/(main)/profile/stats/features/ShareButton/TotalCard.tsx +39 -0
- package/src/app/(main)/profile/stats/features/ShareButton/index.tsx +26 -0
- package/src/app/(main)/profile/stats/features/TimeLabel.tsx +30 -0
- package/src/app/(main)/profile/stats/features/TopicsRank.tsx +103 -0
- package/src/app/(main)/profile/stats/features/TotalAssistants.tsx +56 -0
- package/src/app/(main)/profile/stats/features/TotalMessages.tsx +56 -0
- package/src/app/(main)/profile/stats/features/TotalTopics.tsx +53 -0
- package/src/app/(main)/profile/stats/features/TotalWords.tsx +54 -0
- package/src/app/(main)/profile/stats/features/Welcome.tsx +86 -0
- package/src/app/(main)/profile/{[[...slugs]] → stats}/page.tsx +4 -5
- package/src/app/(main)/repos/[id]/evals/dataset/page.tsx +2 -2
- package/src/app/(main)/repos/[id]/evals/evaluation/page.tsx +2 -2
- package/src/app/(main)/settings/@category/features/CategoryContent.tsx +1 -1
- package/src/app/(main)/settings/_layout/Desktop/index.tsx +1 -1
- package/src/app/(main)/settings/_layout/Mobile/Header.tsx +1 -1
- package/src/app/(main)/settings/_layout/Mobile/index.tsx +2 -0
- package/src/app/(main)/settings/common/features/Theme/index.tsx +2 -17
- package/src/app/(main)/settings/loading.tsx +2 -2
- package/src/components/Loading/BrandTextLoading/index.tsx +2 -2
- package/src/components/Statistic/index.tsx +15 -0
- package/src/components/StatisticCard/TitleWithPercentage.tsx +80 -0
- package/src/components/StatisticCard/growthPercentage.tsx +8 -0
- package/src/components/StatisticCard/index.tsx +209 -0
- package/src/const/url.ts +3 -3
- package/src/database/server/models/__tests__/message.test.ts +346 -35
- package/src/database/server/models/__tests__/session.test.ts +185 -2
- package/src/database/server/models/__tests__/topic.test.ts +136 -0
- package/src/database/server/models/__tests__/user.test.ts +140 -1
- package/src/database/server/models/message.ts +109 -14
- package/src/database/server/models/session.ts +75 -4
- package/src/database/server/models/topic.ts +43 -3
- package/src/database/server/models/user.ts +22 -0
- package/src/database/utils/genWhere.ts +39 -0
- package/src/features/ShareModal/ShareImage/index.tsx +11 -24
- package/src/features/ShareModal/ShareImage/type.ts +1 -6
- package/src/features/User/DataStatistics.tsx +21 -14
- package/src/features/User/UserPanel/PanelContent.tsx +12 -16
- package/src/features/User/UserPanel/useMenu.tsx +4 -6
- package/src/features/User/__tests__/PanelContent.test.tsx +4 -0
- package/src/features/User/__tests__/useMenu.test.tsx +1 -21
- package/src/hooks/useActiveTabKey.ts +34 -1
- package/src/{features/ShareModal/ShareImage → hooks}/useScreenshot.ts +51 -6
- package/src/locales/default/auth.ts +74 -2
- package/src/server/ld.test.ts +1 -1
- package/src/server/modules/AssistantStore/index.ts +3 -2
- package/src/server/routers/lambda/message.ts +35 -6
- package/src/server/routers/lambda/session.ts +17 -3
- package/src/server/routers/lambda/topic.ts +17 -3
- package/src/server/routers/lambda/user.ts +4 -0
- package/src/server/services/changelog/index.ts +1 -1
- package/src/services/message/_deprecated.ts +16 -0
- package/src/services/message/client.test.ts +0 -18
- package/src/services/message/client.ts +12 -9
- package/src/services/message/server.ts +12 -4
- package/src/services/message/type.ts +15 -3
- package/src/services/session/_deprecated.ts +5 -0
- package/src/services/session/client.ts +6 -2
- package/src/services/session/server.ts +6 -2
- package/src/services/session/type.ts +7 -1
- package/src/services/topic/_deprecated.ts +5 -0
- package/src/services/topic/client.ts +6 -2
- package/src/services/topic/server.ts +7 -1
- package/src/services/topic/type.ts +7 -2
- package/src/services/user/_deprecated.ts +4 -0
- package/src/services/user/client.ts +4 -0
- package/src/services/user/server.ts +4 -0
- package/src/services/user/type.ts +5 -0
- package/src/store/global/initialState.ts +6 -0
- package/src/store/user/slices/auth/action.test.ts +1 -33
- package/src/store/user/slices/auth/action.ts +0 -9
- package/src/store/user/slices/common/action.test.ts +2 -2
- package/src/types/message/index.ts +5 -0
- package/src/types/session/index.ts +8 -0
- package/src/types/topic/topic.ts +7 -0
- package/src/utils/format.ts +1 -1
- package/src/utils/time.ts +23 -0
- package/src/app/(main)/profile/[[...slugs]]/Client.tsx +0 -76
- package/src/components/Loading/BrandTextLoading/LobeChatText/SVG.tsx +0 -44
- package/src/components/Loading/BrandTextLoading/LobeChatText/index.tsx +0 -6
- package/src/components/Loading/BrandTextLoading/LobeChatText/style.css +0 -32
- 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 (
|
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(
|
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 (
|
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(
|
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,
|
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
|
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(
|
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
|
{
|
@@ -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 } =
|
61
|
-
|
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 } =
|
66
|
-
|
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 } =
|
73
|
+
const { data: { messages, messagesToday } = {}, isLoading: messagesLoading } = useClientDataSWR(
|
71
74
|
'count-messages',
|
72
|
-
|
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,
|
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
|
-
<
|
58
|
-
|
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
|
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
|
-
...(
|
228
|
+
...(!enableAuth || (enableAuth && isLoginWithAuth) ? profile : []),
|
231
229
|
...(isLogin ? settings : []),
|
232
230
|
/* ↓ cloud slot ↓ */
|
233
231
|
|
@@ -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(
|
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 {
|
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
|
-
|
8
|
+
export enum ImageType {
|
9
|
+
JPG = 'jpg',
|
10
|
+
PNG = 'png',
|
11
|
+
SVG = 'svg',
|
12
|
+
WEBP = 'webp',
|
13
|
+
}
|
10
14
|
|
11
|
-
export const
|
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
|
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;
|