@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
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { FormGroup, Grid } from '@lobehub/ui';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import { FORM_STYLE } from '@/const/layoutTokens';
9
+
10
+ import AiHeatmaps from './features/AiHeatmaps';
11
+ import AssistantsRank from './features/AssistantsRank';
12
+ import ModelsRank from './features/ModelsRank';
13
+ import ShareButton from './features/ShareButton';
14
+ import TopicsRank from './features/TopicsRank';
15
+ import TotalAssistants from './features/TotalAssistants';
16
+ import TotalMessages from './features/TotalMessages';
17
+ import TotalTopics from './features/TotalTopics';
18
+ import TotalWords from './features/TotalWords';
19
+ import Welcome from './features/Welcome';
20
+
21
+ const Client = memo<{ mobile?: boolean }>(({ mobile }) => {
22
+ const { t } = useTranslation('auth');
23
+
24
+ return (
25
+ <Flexbox gap={mobile ? 0 : 24}>
26
+ {mobile ? (
27
+ <Welcome mobile />
28
+ ) : (
29
+ <Flexbox align={'flex-start'} gap={16} horizontal justify={'space-between'}>
30
+ <Welcome />
31
+ <ShareButton />
32
+ </Flexbox>
33
+ )}
34
+ <FormGroup style={FORM_STYLE.style} title={t('tab.stats')} variant={'pure'}>
35
+ <Grid maxItemWidth={150} paddingBlock={16} rows={4}>
36
+ <TotalAssistants mobile={mobile} />
37
+ <TotalTopics mobile={mobile} />
38
+ <TotalMessages mobile={mobile} />
39
+ <TotalWords />
40
+ </Grid>
41
+ </FormGroup>
42
+ <AiHeatmaps mobile={mobile} />
43
+ <Grid gap={mobile ? 0 : 48} rows={3}>
44
+ <ModelsRank />
45
+ <AssistantsRank />
46
+ <TopicsRank />
47
+ </Grid>
48
+ </Flexbox>
49
+ );
50
+ });
51
+
52
+ export default Client;
@@ -0,0 +1,130 @@
1
+ import { Heatmaps, HeatmapsProps } from '@lobehub/charts';
2
+ import { FormGroup, Icon } from '@lobehub/ui';
3
+ import { Tag } from 'antd';
4
+ import { useTheme } from 'antd-style';
5
+ import { FlameIcon } from 'lucide-react';
6
+ import { readableColor } from 'polished';
7
+ import { memo } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
10
+
11
+ import { FORM_STYLE } from '@/const/layoutTokens';
12
+ import { useClientDataSWR } from '@/libs/swr';
13
+ import { messageService } from '@/services/message';
14
+
15
+ const AiHeatmaps = memo<Omit<HeatmapsProps, 'data'> & { inShare?: boolean; mobile?: boolean }>(
16
+ ({ inShare, mobile, ...rest }) => {
17
+ const { t } = useTranslation('auth');
18
+ const theme = useTheme();
19
+ const { data, isLoading } = useClientDataSWR('stats-heatmaps', async () =>
20
+ messageService.getHeatmaps(),
21
+ );
22
+
23
+ const days = data?.filter((item) => item.level > 0).length || '--';
24
+ const hotDays = data?.filter((item) => item.level >= 3).length || '--';
25
+
26
+ const content = (
27
+ <Heatmaps
28
+ blockMargin={mobile ? 3 : undefined}
29
+ blockRadius={mobile ? 2 : undefined}
30
+ blockSize={mobile ? 6 : 14}
31
+ data={data || []}
32
+ labels={{
33
+ legend: {
34
+ less: t('heatmaps.legend.less'),
35
+ more: t('heatmaps.legend.more'),
36
+ },
37
+ months: [
38
+ t('heatmaps.months.jan'),
39
+ t('heatmaps.months.feb'),
40
+ t('heatmaps.months.mar'),
41
+ t('heatmaps.months.apr'),
42
+ t('heatmaps.months.may'),
43
+ t('heatmaps.months.jun'),
44
+ t('heatmaps.months.jul'),
45
+ t('heatmaps.months.aug'),
46
+ t('heatmaps.months.sep'),
47
+ t('heatmaps.months.oct'),
48
+ t('heatmaps.months.nov'),
49
+ t('heatmaps.months.dec'),
50
+ ],
51
+ tooltip: t('heatmaps.tooltip'),
52
+ totalCount: t('heatmaps.totalCount'),
53
+ }}
54
+ loading={isLoading || !data}
55
+ maxLevel={4}
56
+ {...rest}
57
+ />
58
+ );
59
+
60
+ const fillColor = readableColor(theme.gold);
61
+ const tags = (
62
+ <Flexbox
63
+ gap={4}
64
+ horizontal
65
+ style={{
66
+ alignSelf: 'center',
67
+ flex: 'none',
68
+ zoom: 0.9,
69
+ }}
70
+ >
71
+ <Tag
72
+ bordered={false}
73
+ style={{
74
+ background: theme.colorText,
75
+ color: theme.colorBgLayout,
76
+ fontWeight: 500,
77
+ margin: 0,
78
+ }}
79
+ >
80
+ {[days, t('stats.days')].join(' ')}
81
+ </Tag>
82
+ <Tag
83
+ bordered={false}
84
+ color={'gold'}
85
+ icon={<Icon color={fillColor} fill={fillColor} icon={FlameIcon} />}
86
+ style={{
87
+ background: theme.gold,
88
+ color: fillColor,
89
+ fontWeight: 500,
90
+ margin: 0,
91
+ }}
92
+ >
93
+ {[hotDays, t('stats.days')].join(' ')}
94
+ </Tag>
95
+ </Flexbox>
96
+ );
97
+
98
+ if (inShare) {
99
+ return (
100
+ <Flexbox gap={4}>
101
+ <Flexbox align={'baseline'} gap={4} horizontal justify={'space-between'}>
102
+ <div
103
+ style={{
104
+ color: theme.colorTextDescription,
105
+ fontSize: 12,
106
+ }}
107
+ >
108
+ {t('stats.lastYearActivity')}
109
+ </div>
110
+ {tags}
111
+ </Flexbox>
112
+ {content}
113
+ </Flexbox>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <FormGroup
119
+ extra={tags}
120
+ style={FORM_STYLE.style}
121
+ title={t('stats.lastYearActivity')}
122
+ variant={'pure'}
123
+ >
124
+ <Flexbox paddingBlock={24}>{content}</Flexbox>
125
+ </FormGroup>
126
+ );
127
+ },
128
+ );
129
+
130
+ export default AiHeatmaps;
@@ -0,0 +1,115 @@
1
+ import { BarList } from '@lobehub/charts';
2
+ import { ActionIcon, Avatar, FormGroup, Modal } from '@lobehub/ui';
3
+ import { MaximizeIcon } from 'lucide-react';
4
+ import Link from 'next/link';
5
+ import { useRouter } from 'next/navigation';
6
+ import qs from 'query-string';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { Flexbox } from 'react-layout-kit';
10
+
11
+ import { FORM_STYLE } from '@/const/layoutTokens';
12
+ import { DEFAULT_AVATAR } from '@/const/meta';
13
+ import { INBOX_SESSION_ID } from '@/const/session';
14
+ import { useClientDataSWR } from '@/libs/swr';
15
+ import { sessionService } from '@/services/session';
16
+ import { SessionRankItem } from '@/types/session';
17
+
18
+ export const AssistantsRank = memo(() => {
19
+ const [open, setOpen] = useState(false);
20
+ const { t } = useTranslation(['auth', 'chat']);
21
+ const router = useRouter();
22
+ const { data, isLoading } = useClientDataSWR('rank-sessions', async () =>
23
+ sessionService.rankSessions(),
24
+ );
25
+
26
+ const showExtra = Boolean(data && data?.length > 5);
27
+
28
+ const mapData = (item: SessionRankItem) => {
29
+ const link = qs.stringifyUrl({
30
+ query: {
31
+ session: item.id,
32
+ },
33
+ url: '/chat',
34
+ });
35
+
36
+ return {
37
+ icon: (
38
+ <Avatar
39
+ alt={item.title || t('defaultAgent', { ns: 'chat' })}
40
+ avatar={item.avatar || DEFAULT_AVATAR}
41
+ background={item.backgroundColor || undefined}
42
+ size={28}
43
+ style={{
44
+ backdropFilter: 'blur(8px)',
45
+ }}
46
+ />
47
+ ),
48
+ link,
49
+ name: (
50
+ <Link href={link} style={{ color: 'inherit' }}>
51
+ {item.title
52
+ ? item.id === INBOX_SESSION_ID
53
+ ? t('inbox.title', { ns: 'chat' })
54
+ : item.title
55
+ : t('defaultAgent', { ns: 'chat' })}
56
+ </Link>
57
+ ),
58
+ value: item.count,
59
+ };
60
+ };
61
+
62
+ return (
63
+ <>
64
+ <FormGroup
65
+ extra={
66
+ showExtra && (
67
+ <ActionIcon
68
+ icon={MaximizeIcon}
69
+ onClick={() => setOpen(true)}
70
+ size={{ blockSize: 28, fontSize: 20 }}
71
+ />
72
+ )
73
+ }
74
+ style={FORM_STYLE.style}
75
+ title={t('stats.assistantsRank.title')}
76
+ variant={'pure'}
77
+ >
78
+ <Flexbox paddingBlock={16}>
79
+ <BarList
80
+ data={data?.slice(0, 5).map((item) => mapData(item)) || []}
81
+ height={220}
82
+ leftLabel={t('stats.assistantsRank.left')}
83
+ loading={isLoading || !data}
84
+ noDataText={{
85
+ desc: t('stats.empty.desc'),
86
+ title: t('stats.empty.title'),
87
+ }}
88
+ onValueChange={(item) => router.push(item.link)}
89
+ rightLabel={t('stats.assistantsRank.right')}
90
+ />
91
+ </Flexbox>
92
+ </FormGroup>
93
+ {showExtra && (
94
+ <Modal
95
+ footer={null}
96
+ loading={isLoading || !data}
97
+ onCancel={() => setOpen(false)}
98
+ open={open}
99
+ title={t('stats.assistantsRank.title')}
100
+ >
101
+ <BarList
102
+ data={data?.map((item) => mapData(item)) || []}
103
+ height={340}
104
+ leftLabel={t('stats.assistantsRank.left')}
105
+ loading={isLoading || !data}
106
+ onValueChange={(item) => router.push(item.link)}
107
+ rightLabel={t('stats.assistantsRank.right')}
108
+ />
109
+ </Modal>
110
+ )}
111
+ </>
112
+ );
113
+ });
114
+
115
+ export default AssistantsRank;
@@ -0,0 +1,84 @@
1
+ import { BarList } from '@lobehub/charts';
2
+ import { ModelIcon } from '@lobehub/icons';
3
+ import { ActionIcon, FormGroup, Modal } from '@lobehub/ui';
4
+ import { MaximizeIcon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { FORM_STYLE } from '@/const/layoutTokens';
10
+ import { useClientDataSWR } from '@/libs/swr';
11
+ import { messageService } from '@/services/message';
12
+ import { ModelRankItem } from '@/types/message';
13
+
14
+ export const TopicsRank = memo(() => {
15
+ const [open, setOpen] = useState(false);
16
+ const { t } = useTranslation('auth');
17
+ const { data, isLoading } = useClientDataSWR('rank-models', async () =>
18
+ messageService.rankModels(),
19
+ );
20
+
21
+ const showExtra = Boolean(data && data?.length > 5);
22
+
23
+ const mapData = (item: ModelRankItem) => {
24
+ return {
25
+ icon: <ModelIcon model={item.id as string} size={24} />,
26
+ id: item.id,
27
+
28
+ name: item.id,
29
+ value: item.count,
30
+ };
31
+ };
32
+
33
+ return (
34
+ <>
35
+ <FormGroup
36
+ extra={
37
+ showExtra ? (
38
+ <ActionIcon
39
+ icon={MaximizeIcon}
40
+ onClick={() => setOpen(true)}
41
+ size={{ blockSize: 28, fontSize: 20 }}
42
+ />
43
+ ) : undefined
44
+ }
45
+ style={FORM_STYLE.style}
46
+ title={t('stats.modelsRank.title')}
47
+ variant={'pure'}
48
+ >
49
+ <Flexbox horizontal paddingBlock={16}>
50
+ <BarList
51
+ data={data?.slice(0, 5).map((item) => mapData(item)) || []}
52
+ height={220}
53
+ leftLabel={t('stats.modelsRank.left')}
54
+ loading={isLoading || !data}
55
+ noDataText={{
56
+ desc: t('stats.empty.desc'),
57
+ title: t('stats.empty.title'),
58
+ }}
59
+ rightLabel={t('stats.modelsRank.right')}
60
+ />
61
+ </Flexbox>
62
+ </FormGroup>
63
+ {showExtra && (
64
+ <Modal
65
+ footer={null}
66
+ loading={isLoading || !data}
67
+ onCancel={() => setOpen(false)}
68
+ open={open}
69
+ title={t('stats.modelsRank.title')}
70
+ >
71
+ <BarList
72
+ data={data?.map((item) => mapData(item)) || []}
73
+ height={340}
74
+ leftLabel={t('stats.assistantsRank.left')}
75
+ loading={isLoading || !data}
76
+ rightLabel={t('stats.assistantsRank.right')}
77
+ />
78
+ </Modal>
79
+ )}
80
+ </>
81
+ );
82
+ });
83
+
84
+ export default TopicsRank;
@@ -0,0 +1,159 @@
1
+ import { Github } from '@lobehub/icons';
2
+ import { Grid } from '@lobehub/ui';
3
+ import { createStyles } from 'antd-style';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Center, Flexbox } from 'react-layout-kit';
7
+
8
+ import { ProductLogo } from '@/components/Branding';
9
+ import { OFFICIAL_URL, imageUrl } from '@/const/url';
10
+ import { isServerMode } from '@/const/version';
11
+ import UserAvatar from '@/features/User/UserAvatar';
12
+
13
+ import TotalMessages from '..//TotalMessages';
14
+ import TotalWords from '..//TotalWords';
15
+ import AiHeatmaps from '../AiHeatmaps';
16
+
17
+ const useStyles = createStyles(({ css, token, stylish, cx, responsive }) => ({
18
+ avatar: css`
19
+ box-sizing: content-box;
20
+ background: ${token.colorText};
21
+ border: 4px solid ${token.colorBgLayout};
22
+ `,
23
+ background: css`
24
+ position: relative;
25
+
26
+ width: 100%;
27
+ padding: 24px;
28
+
29
+ background-color: ${token.colorBgLayout};
30
+ background-image: url(${imageUrl('screenshot_background.webp')});
31
+ background-position: center;
32
+ background-size: 120% 120%;
33
+ `,
34
+
35
+ container: css`
36
+ position: relative;
37
+
38
+ overflow: hidden;
39
+
40
+ width: 100%;
41
+
42
+ background: ${token.colorBgLayout};
43
+ border: 1px solid ${token.colorBorder};
44
+ border-radius: ${token.borderRadiusLG * 2}px;
45
+ box-shadow: ${token.boxShadow};
46
+ `,
47
+ decs: css`
48
+ font-size: 12px;
49
+ color: ${token.colorTextDescription};
50
+ `,
51
+ footer: css`
52
+ font-size: 12px;
53
+ color: ${token.colorTextDescription};
54
+ `,
55
+ heatmaps: css`
56
+ .legend-month,
57
+ footer {
58
+ display: none;
59
+ }
60
+ `,
61
+ preview: cx(
62
+ stylish.noScrollbar,
63
+ css`
64
+ overflow: hidden scroll;
65
+
66
+ width: 100%;
67
+ max-height: 70dvh;
68
+
69
+ background: ${token.colorBgLayout};
70
+ border: 1px solid ${token.colorBorder};
71
+ border-radius: ${token.borderRadiusLG}px;
72
+
73
+ * {
74
+ pointer-events: none;
75
+
76
+ ::-webkit-scrollbar {
77
+ width: 0 !important;
78
+ height: 0 !important;
79
+ }
80
+ }
81
+
82
+ ${responsive.mobile} {
83
+ max-height: 40dvh;
84
+ }
85
+ `,
86
+ ),
87
+ title: css`
88
+ font-size: 24px;
89
+ font-weight: bold;
90
+ text-align: center;
91
+ `,
92
+ }));
93
+
94
+ const Preview = memo(() => {
95
+ const { styles } = useStyles();
96
+ const { t } = useTranslation('auth');
97
+ const isOfficial = !isServerMode && OFFICIAL_URL.includes(location.host);
98
+
99
+ return (
100
+ <div className={styles.preview}>
101
+ <div className={styles.background} id={'preview'}>
102
+ <Center className={styles.container} gap={12} padding={24}>
103
+ <ProductLogo size={24} type={'text'} />
104
+ <div className={styles.title}>{t('stats.share.title')}</div>
105
+ <Flexbox align={'center'} horizontal>
106
+ <UserAvatar
107
+ className={styles.avatar}
108
+ size={48}
109
+ style={{
110
+ marginRight: -12,
111
+ zIndex: 2,
112
+ }}
113
+ />
114
+ <Center
115
+ className={styles.avatar}
116
+ height={48}
117
+ style={{
118
+ borderRadius: '50%',
119
+ zIndex: 1,
120
+ }}
121
+ width={48}
122
+ >
123
+ <ProductLogo size={40} />
124
+ </Center>
125
+ </Flexbox>
126
+ <Flexbox gap={12} paddingBlock={12} width={'100%'}>
127
+ <AiHeatmaps
128
+ blockMargin={2}
129
+ blockRadius={1}
130
+ blockSize={4.5}
131
+ className={styles.heatmaps}
132
+ inShare
133
+ style={{
134
+ marginTop: -12,
135
+ }}
136
+ width={'100%'}
137
+ />
138
+ <Grid gap={8} maxItemWidth={100} rows={2} width={'100%'}>
139
+ <TotalMessages inShare />
140
+ <TotalWords inShare />
141
+ </Grid>
142
+ </Flexbox>
143
+ <div className={styles.footer}>
144
+ {isOfficial ? (
145
+ OFFICIAL_URL
146
+ ) : (
147
+ <Flexbox align={'center'} gap={8} horizontal>
148
+ <Github size={16} />
149
+ <span>lobehub/lobe-chat</span>
150
+ </Flexbox>
151
+ )}
152
+ </div>
153
+ </Center>
154
+ </div>
155
+ </div>
156
+ );
157
+ });
158
+
159
+ export default Preview;
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import { type FormItemProps, FormModal, FormModalProps } from '@lobehub/ui';
4
+ import { Segmented, Skeleton } from 'antd';
5
+ import { createStyles } from 'antd-style';
6
+ import dynamic from 'next/dynamic';
7
+ import { memo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import { ImageType, imageTypeOptions, useScreenshot } from '@/hooks/useScreenshot';
11
+
12
+ const Preview = dynamic(() => import('./Preview'), {
13
+ loading: () => (
14
+ <Skeleton.Button
15
+ active
16
+ block
17
+ size={'large'}
18
+ style={{
19
+ height: 400,
20
+ width: '100%',
21
+ }}
22
+ />
23
+ ),
24
+ });
25
+
26
+ const useStyles = createStyles(({ css, prefixCls }) => ({
27
+ preview: css`
28
+ .${prefixCls}-form-item-label {
29
+ display: none;
30
+ }
31
+ `,
32
+ }));
33
+
34
+ type FieldType = {
35
+ imageType: ImageType;
36
+ };
37
+
38
+ const DEFAULT_FIELD_VALUE: FieldType = {
39
+ imageType: ImageType.JPG,
40
+ };
41
+
42
+ const ShareModal = memo<FormModalProps & { mobile?: boolean }>(({ open, onCancel, mobile }) => {
43
+ const { t } = useTranslation(['chat', 'common']);
44
+ const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
45
+ const { styles } = useStyles();
46
+ const { loading, onDownload } = useScreenshot({
47
+ imageType: fieldValue.imageType,
48
+ title: 'stats',
49
+ width: mobile ? 440 : undefined,
50
+ });
51
+
52
+ const items: FormItemProps[] = [
53
+ {
54
+ children: <Preview />,
55
+ className: styles.preview,
56
+ divider: false,
57
+ minWidth: '100%',
58
+ },
59
+ {
60
+ children: <Segmented options={imageTypeOptions} />,
61
+ divider: false,
62
+ label: t('shareModal.imageType'),
63
+ minWidth: undefined,
64
+ name: 'imageType',
65
+ },
66
+ ];
67
+
68
+ return (
69
+ <FormModal
70
+ allowFullscreen
71
+ footer={null}
72
+ initialValues={DEFAULT_FIELD_VALUE}
73
+ items={items}
74
+ itemsType={'flat'}
75
+ onCancel={onCancel}
76
+ onFinish={onDownload}
77
+ onValuesChange={(_, v) => setFieldValue(v)}
78
+ open={open}
79
+ submitLoading={loading}
80
+ submitText={t('shareModal.download')}
81
+ title={t('share', { ns: 'common' })}
82
+ width={480}
83
+ />
84
+ );
85
+ });
86
+
87
+ export default ShareModal;
@@ -0,0 +1,39 @@
1
+ import { useTheme } from 'antd-style';
2
+ import { memo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ interface TotalCardProps {
6
+ count: string | number;
7
+ title: string;
8
+ }
9
+
10
+ const TotalCard = memo<TotalCardProps>(({ title, count }) => {
11
+ const theme = useTheme();
12
+ return (
13
+ <Flexbox
14
+ padding={12}
15
+ style={{
16
+ background: theme.isDarkMode ? theme.colorFillTertiary : theme.colorFillQuaternary,
17
+ borderRadius: theme.borderRadiusLG,
18
+ }}
19
+ >
20
+ <div
21
+ style={{
22
+ fontSize: 13,
23
+ }}
24
+ >
25
+ {title}
26
+ </div>
27
+ <div
28
+ style={{
29
+ fontSize: 20,
30
+ fontWeight: 'bold',
31
+ }}
32
+ >
33
+ {count}
34
+ </div>
35
+ </Flexbox>
36
+ );
37
+ });
38
+
39
+ export default TotalCard;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+
3
+ import { ActionIcon } from '@lobehub/ui';
4
+ import { Share2Icon } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+
7
+ import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
8
+
9
+ import ShareModal from './ShareModal';
10
+
11
+ const ShareButton = memo<{ mobile?: boolean }>(({ mobile }) => {
12
+ const [open, setOpen] = useState(false);
13
+
14
+ return (
15
+ <>
16
+ <ActionIcon
17
+ icon={Share2Icon}
18
+ onClick={() => setOpen(true)}
19
+ size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
20
+ />
21
+ <ShareModal mobile={mobile} onCancel={() => setOpen(false)} open={open} />
22
+ </>
23
+ );
24
+ });
25
+
26
+ export default ShareButton;