@lobehub/chat 1.39.3 → 1.40.1

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 (175) hide show
  1. package/.env.example +19 -8
  2. package/.eslintignore +1 -1
  3. package/CHANGELOG.md +58 -0
  4. package/changelog/v1.json +21 -0
  5. package/docs/.cdn.cache.json +25 -0
  6. package/docs/changelog/2023-09-09-plugin-system.mdx +1 -1
  7. package/docs/changelog/2023-09-09-plugin-system.zh-CN.mdx +1 -1
  8. package/docs/changelog/2024-09-20-artifacts.mdx +1 -1
  9. package/docs/changelog/2024-09-20-artifacts.zh-CN.mdx +1 -1
  10. package/docs/changelog/2024-10-27-pin-assistant.mdx +2 -2
  11. package/docs/changelog/2024-10-27-pin-assistant.zh-CN.mdx +2 -2
  12. package/docs/changelog/2024-11-06-share-text-json.mdx +2 -2
  13. package/docs/changelog/2024-11-06-share-text-json.zh-CN.mdx +2 -2
  14. package/docs/changelog/index.json +16 -16
  15. package/locales/ar/changelog.json +18 -0
  16. package/locales/ar/common.json +1 -0
  17. package/locales/ar/metadata.json +4 -0
  18. package/locales/bg-BG/changelog.json +18 -0
  19. package/locales/bg-BG/common.json +1 -0
  20. package/locales/bg-BG/metadata.json +4 -0
  21. package/locales/de-DE/changelog.json +18 -0
  22. package/locales/de-DE/common.json +1 -0
  23. package/locales/de-DE/metadata.json +4 -0
  24. package/locales/en-US/changelog.json +18 -0
  25. package/locales/en-US/common.json +1 -0
  26. package/locales/en-US/metadata.json +4 -0
  27. package/locales/es-ES/changelog.json +18 -0
  28. package/locales/es-ES/common.json +1 -0
  29. package/locales/es-ES/metadata.json +4 -0
  30. package/locales/fa-IR/changelog.json +18 -0
  31. package/locales/fa-IR/common.json +1 -0
  32. package/locales/fa-IR/metadata.json +4 -0
  33. package/locales/fr-FR/changelog.json +18 -0
  34. package/locales/fr-FR/common.json +1 -0
  35. package/locales/fr-FR/metadata.json +4 -0
  36. package/locales/it-IT/changelog.json +18 -0
  37. package/locales/it-IT/common.json +1 -0
  38. package/locales/it-IT/metadata.json +4 -0
  39. package/locales/ja-JP/changelog.json +18 -0
  40. package/locales/ja-JP/common.json +1 -0
  41. package/locales/ja-JP/metadata.json +4 -0
  42. package/locales/ko-KR/changelog.json +18 -0
  43. package/locales/ko-KR/common.json +1 -0
  44. package/locales/ko-KR/metadata.json +4 -0
  45. package/locales/nl-NL/changelog.json +18 -0
  46. package/locales/nl-NL/common.json +1 -0
  47. package/locales/nl-NL/metadata.json +4 -0
  48. package/locales/pl-PL/changelog.json +18 -0
  49. package/locales/pl-PL/common.json +1 -0
  50. package/locales/pl-PL/metadata.json +4 -0
  51. package/locales/pt-BR/changelog.json +18 -0
  52. package/locales/pt-BR/common.json +1 -0
  53. package/locales/pt-BR/metadata.json +4 -0
  54. package/locales/ru-RU/changelog.json +18 -0
  55. package/locales/ru-RU/common.json +1 -0
  56. package/locales/ru-RU/metadata.json +4 -0
  57. package/locales/tr-TR/changelog.json +18 -0
  58. package/locales/tr-TR/common.json +1 -0
  59. package/locales/tr-TR/metadata.json +4 -0
  60. package/locales/vi-VN/changelog.json +18 -0
  61. package/locales/vi-VN/common.json +1 -0
  62. package/locales/vi-VN/metadata.json +4 -0
  63. package/locales/zh-CN/changelog.json +18 -0
  64. package/locales/zh-CN/common.json +1 -0
  65. package/locales/zh-CN/metadata.json +4 -0
  66. package/locales/zh-TW/changelog.json +18 -0
  67. package/locales/zh-TW/common.json +1 -0
  68. package/locales/zh-TW/metadata.json +4 -0
  69. package/package.json +6 -1
  70. package/scripts/cdnWorkflow/index.ts +217 -0
  71. package/scripts/cdnWorkflow/optimized.ts +21 -0
  72. package/scripts/cdnWorkflow/s3/index.ts +120 -0
  73. package/scripts/cdnWorkflow/s3/types.ts +25 -0
  74. package/scripts/cdnWorkflow/s3/utils.ts +106 -0
  75. package/scripts/cdnWorkflow/uploader.ts +73 -0
  76. package/scripts/cdnWorkflow/utils.ts +93 -0
  77. package/src/app/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +25 -12
  78. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +19 -9
  79. package/src/app/(main)/_layout/Desktop.tsx +4 -1
  80. package/src/app/(main)/_layout/Mobile.tsx +2 -1
  81. package/src/app/(main)/changelog/_layout/Desktop.tsx +25 -0
  82. package/src/app/(main)/changelog/_layout/Mobile/Header.tsx +33 -0
  83. package/src/app/(main)/changelog/_layout/Mobile/index.tsx +21 -0
  84. package/src/app/(main)/changelog/error.tsx +5 -0
  85. package/src/app/(main)/changelog/features/GridLayout.tsx +22 -0
  86. package/src/app/(main)/changelog/features/Hero.tsx +40 -0
  87. package/src/app/(main)/changelog/features/Post.tsx +56 -0
  88. package/src/app/(main)/changelog/features/PublishedTime.tsx +50 -0
  89. package/src/app/(main)/changelog/features/VersionTag.tsx +27 -0
  90. package/src/app/(main)/changelog/layout.tsx +10 -0
  91. package/src/app/(main)/changelog/loading.tsx +3 -0
  92. package/src/app/(main)/changelog/modal/page.tsx +23 -0
  93. package/src/app/(main)/changelog/not-found.tsx +3 -0
  94. package/src/app/(main)/changelog/page.tsx +73 -0
  95. package/src/app/(main)/chat/(workspace)/_layout/Desktop/HotKeys.tsx +7 -0
  96. package/src/app/(main)/chat/(workspace)/page.tsx +9 -2
  97. package/src/app/(main)/settings/about/features/Version.tsx +2 -2
  98. package/src/app/@modal/(.)changelog/modal/features/Cover.tsx +48 -0
  99. package/src/app/@modal/(.)changelog/modal/features/Hero.tsx +29 -0
  100. package/src/app/@modal/(.)changelog/modal/features/Pagination.tsx +54 -0
  101. package/src/app/@modal/(.)changelog/modal/features/Post.tsx +57 -0
  102. package/src/app/@modal/(.)changelog/modal/features/PublishedTime.tsx +50 -0
  103. package/src/app/@modal/(.)changelog/modal/features/ReadDetail.tsx +94 -0
  104. package/src/app/@modal/(.)changelog/modal/features/UpdateChangelogStatus.tsx +21 -0
  105. package/src/app/@modal/(.)changelog/modal/features/VersionTag.tsx +27 -0
  106. package/src/app/@modal/(.)changelog/modal/layout.tsx +39 -0
  107. package/src/app/@modal/(.)changelog/modal/loading.tsx +10 -0
  108. package/src/app/@modal/(.)changelog/modal/page.tsx +37 -0
  109. package/src/app/@modal/(.)settings/modal/layout.tsx +19 -16
  110. package/src/app/@modal/_layout/ModalLayout.tsx +63 -0
  111. package/src/app/@modal/chat/(.)settings/modal/layout.tsx +20 -17
  112. package/src/app/@modal/layout.tsx +5 -69
  113. package/src/components/mdx/Image.tsx +50 -0
  114. package/src/components/mdx/index.tsx +2 -0
  115. package/src/const/hotkeys.ts +1 -0
  116. package/src/const/url.ts +1 -0
  117. package/src/features/ChangelogModal/index.tsx +22 -0
  118. package/src/features/User/UserPanel/useMenu.tsx +50 -46
  119. package/src/features/User/__tests__/useMenu.test.tsx +7 -6
  120. package/src/hooks/useInterceptingRoutes.ts +1 -6
  121. package/src/hooks/useShare.tsx +1 -0
  122. package/src/libs/agent-runtime/openai/index.ts +2 -0
  123. package/src/locales/default/changelog.ts +18 -0
  124. package/src/locales/default/common.ts +1 -0
  125. package/src/locales/default/index.ts +2 -0
  126. package/src/locales/default/metadata.ts +4 -0
  127. package/src/server/metadata.ts +5 -3
  128. package/src/server/routers/edge/appStatus.ts +3 -0
  129. package/src/server/routers/edge/index.ts +2 -0
  130. package/src/server/routers/lambda/agent.ts +1 -1
  131. package/src/server/services/changelog/index.test.ts +310 -0
  132. package/src/server/services/changelog/index.ts +196 -0
  133. package/src/server/services/discover/index.test.ts +0 -1
  134. package/src/server/sitemap.ts +4 -1
  135. package/src/services/__tests__/chat.test.ts +1 -1
  136. package/src/services/__tests__/global.test.ts +5 -2
  137. package/src/services/_auth.ts +1 -1
  138. package/src/services/agent.ts +25 -21
  139. package/src/services/chat.ts +2 -2
  140. package/src/services/file/ClientS3/index.ts +6 -6
  141. package/src/services/file/client.ts +14 -15
  142. package/src/services/file/server.ts +20 -25
  143. package/src/services/global.ts +2 -2
  144. package/src/services/import/client.ts +6 -5
  145. package/src/services/import/server.ts +6 -5
  146. package/src/services/import/type.ts +7 -0
  147. package/src/services/knowledgeBase.ts +19 -19
  148. package/src/services/message/_deprecated.ts +5 -0
  149. package/src/services/message/client.ts +52 -48
  150. package/src/services/message/server.ts +50 -53
  151. package/src/services/message/type.ts +2 -2
  152. package/src/services/plugin/client.ts +16 -22
  153. package/src/services/plugin/server.ts +15 -19
  154. package/src/services/rag.ts +18 -18
  155. package/src/services/ragEval.ts +29 -26
  156. package/src/services/session/_deprecated.ts +2 -2
  157. package/src/services/session/client.ts +55 -81
  158. package/src/services/session/server.ts +50 -74
  159. package/src/services/session/type.ts +4 -6
  160. package/src/services/share.ts +4 -4
  161. package/src/services/textToImage.ts +5 -2
  162. package/src/services/thread/client.ts +9 -15
  163. package/src/services/thread/server.ts +10 -15
  164. package/src/services/topic/client.ts +25 -25
  165. package/src/services/topic/server.ts +25 -42
  166. package/src/services/trace.ts +4 -4
  167. package/src/services/user/client.ts +13 -17
  168. package/src/services/user/server.ts +9 -13
  169. package/src/services/user/type.ts +1 -1
  170. package/src/store/chat/slices/message/reducer.ts +3 -2
  171. package/src/store/global/action.ts +27 -22
  172. package/src/store/global/initialState.ts +1 -0
  173. package/src/types/changelog.ts +6 -0
  174. package/src/types/message/index.ts +10 -8
  175. package/src/app/@modal/features/InterceptingContext.tsx +0 -9
@@ -1,10 +1,15 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
 
4
+ import { ServerConfigStoreProvider } from '@/store/serverConfig';
4
5
  import { useUserStore } from '@/store/user';
5
6
 
6
7
  import { useCategory } from '../features/useCategory';
7
8
 
9
+ const wrapper: React.JSXElementConstructor<{ children: React.ReactNode }> = ({ children }) => (
10
+ <ServerConfigStoreProvider>{children}</ServerConfigStoreProvider>
11
+ );
12
+
8
13
  // Mock dependencies
9
14
  vi.mock('next/navigation', () => ({
10
15
  useRouter: vi.fn(() => ({
@@ -24,7 +29,7 @@ vi.mock('../../settings/features/useCategory', () => ({
24
29
 
25
30
  // 定义一个变量来存储 enableAuth 的值
26
31
  let enableAuth = true;
27
- let enableClerk = false;
32
+ let enableClerk = true;
28
33
  // 模拟 @/const/auth 模块
29
34
  vi.mock('@/const/auth', () => ({
30
35
  get enableAuth() {
@@ -37,7 +42,7 @@ vi.mock('@/const/auth', () => ({
37
42
 
38
43
  afterEach(() => {
39
44
  enableAuth = true;
40
- enableClerk = false;
45
+ enableClerk = true;
41
46
  });
42
47
 
43
48
  // 目前对 enableAuth 的判定是在 useUserStore 中,所以需要 mock useUserStore
@@ -47,8 +52,10 @@ describe('useCategory', () => {
47
52
  act(() => {
48
53
  useUserStore.setState({ isSignedIn: true, enableAuth: () => true });
49
54
  });
55
+ enableAuth = true;
56
+ enableClerk = false;
50
57
 
51
- const { result } = renderHook(() => useCategory());
58
+ const { result } = renderHook(() => useCategory(), { wrapper });
52
59
 
53
60
  act(() => {
54
61
  const items = result.current;
@@ -57,7 +64,7 @@ describe('useCategory', () => {
57
64
  expect(items.some((item) => item.key === 'data')).toBe(true);
58
65
  expect(items.some((item) => item.key === 'docs')).toBe(true);
59
66
  expect(items.some((item) => item.key === 'feedback')).toBe(true);
60
- expect(items.some((item) => item.key === 'discord')).toBe(true);
67
+ expect(items.some((item) => item.key === 'changelog')).toBe(true);
61
68
  });
62
69
  });
63
70
 
@@ -65,9 +72,10 @@ describe('useCategory', () => {
65
72
  act(() => {
66
73
  useUserStore.setState({ isSignedIn: true });
67
74
  });
75
+ enableAuth = true;
68
76
  enableClerk = true;
69
77
 
70
- const { result } = renderHook(() => useCategory());
78
+ const { result } = renderHook(() => useCategory(), { wrapper });
71
79
 
72
80
  act(() => {
73
81
  const items = result.current;
@@ -76,16 +84,21 @@ describe('useCategory', () => {
76
84
  expect(items.some((item) => item.key === 'data')).toBe(true);
77
85
  expect(items.some((item) => item.key === 'docs')).toBe(true);
78
86
  expect(items.some((item) => item.key === 'feedback')).toBe(true);
79
- expect(items.some((item) => item.key === 'discord')).toBe(true);
87
+ expect(items.some((item) => item.key === 'changelog')).toBe(true);
80
88
  });
81
89
  });
82
90
 
83
91
  it('should return correct items when the user is logged in with NextAuth', () => {
84
92
  act(() => {
85
- useUserStore.setState({ isSignedIn: true, enableAuth: () => true, enabledNextAuth: true });
93
+ useUserStore.setState({
94
+ isSignedIn: true,
95
+ enableAuth: () => true,
96
+ enabledNextAuth: true,
97
+ });
86
98
  });
99
+ enableClerk = false;
87
100
 
88
- const { result } = renderHook(() => useCategory());
101
+ const { result } = renderHook(() => useCategory(), { wrapper });
89
102
 
90
103
  act(() => {
91
104
  const items = result.current;
@@ -95,7 +108,7 @@ describe('useCategory', () => {
95
108
  expect(items.some((item) => item.key === 'data')).toBe(true);
96
109
  expect(items.some((item) => item.key === 'docs')).toBe(true);
97
110
  expect(items.some((item) => item.key === 'feedback')).toBe(true);
98
- expect(items.some((item) => item.key === 'discord')).toBe(true);
111
+ expect(items.some((item) => item.key === 'changelog')).toBe(true);
99
112
  expect(items.some((item) => item.key === 'nextauthSignout')).toBe(true);
100
113
  });
101
114
  });
@@ -105,7 +118,7 @@ describe('useCategory', () => {
105
118
  useUserStore.setState({ isSignedIn: false, enableAuth: () => true });
106
119
  });
107
120
 
108
- const { result } = renderHook(() => useCategory());
121
+ const { result } = renderHook(() => useCategory(), { wrapper });
109
122
 
110
123
  act(() => {
111
124
  const items = result.current;
@@ -114,7 +127,7 @@ describe('useCategory', () => {
114
127
  expect(items.some((item) => item.key === 'data')).toBe(false);
115
128
  expect(items.some((item) => item.key === 'docs')).toBe(true);
116
129
  expect(items.some((item) => item.key === 'feedback')).toBe(true);
117
- expect(items.some((item) => item.key === 'discord')).toBe(true);
130
+ expect(items.some((item) => item.key === 'changelog')).toBe(true);
118
131
  expect(items.some((item) => item.key === 'nextauthSignout')).toBe(false);
119
132
  });
120
133
  });
@@ -125,7 +138,7 @@ describe('useCategory', () => {
125
138
  });
126
139
  enableClerk = false;
127
140
 
128
- const { result } = renderHook(() => useCategory());
141
+ const { result } = renderHook(() => useCategory(), { wrapper });
129
142
 
130
143
  act(() => {
131
144
  const items = result.current;
@@ -1,10 +1,11 @@
1
- import { DiscordIcon } from '@lobehub/ui';
2
1
  import {
3
2
  Book,
4
3
  CircleUserRound,
4
+ Cloudy,
5
5
  Database,
6
6
  Download,
7
7
  Feather,
8
+ FileClockIcon,
8
9
  LogOut,
9
10
  Settings2,
10
11
  } from 'lucide-react';
@@ -12,11 +13,13 @@ import { useRouter } from 'next/navigation';
12
13
  import { useTranslation } from 'react-i18next';
13
14
 
14
15
  import { CellProps } from '@/components/Cell';
15
- import { DISCORD, DOCUMENTS, FEEDBACK } from '@/const/url';
16
+ import { LOBE_CHAT_CLOUD } from '@/const/branding';
17
+ import { DOCUMENTS, FEEDBACK, OFFICIAL_URL, UTM_SOURCE } from '@/const/url';
16
18
  import { isServerMode } from '@/const/version';
17
19
  import { usePWAInstall } from '@/hooks/usePWAInstall';
20
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
18
21
  import { useUserStore } from '@/store/user';
19
- import { authSelectors } from '@/store/user/slices/auth/selectors';
22
+ import { authSelectors } from '@/store/user/selectors';
20
23
 
21
24
  import { useCategory as useSettingsCategory } from '../../settings/features/useCategory';
22
25
 
@@ -24,6 +27,7 @@ export const useCategory = () => {
24
27
  const router = useRouter();
25
28
  const { canInstall, install } = usePWAInstall();
26
29
  const { t } = useTranslation(['common', 'setting', 'auth']);
30
+ const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors);
27
31
  const [isLogin, isLoginWithAuth, isLoginWithClerk, enableAuth, signOut, isLoginWithNextAuth] =
28
32
  useUserStore((s) => [
29
33
  authSelectors.isLogin(s),
@@ -91,6 +95,12 @@ export const useCategory = () => {
91
95
  ];
92
96
 
93
97
  const helps: CellProps[] = [
98
+ showCloudPromotion && {
99
+ icon: Cloudy,
100
+ key: 'cloud',
101
+ label: t('userPanel.cloud', { name: LOBE_CHAT_CLOUD }),
102
+ onClick: () => window.open(`${OFFICIAL_URL}?utm_source=${UTM_SOURCE}`, '__blank'),
103
+ },
94
104
  {
95
105
  icon: Book,
96
106
  key: 'docs',
@@ -104,12 +114,12 @@ export const useCategory = () => {
104
114
  onClick: () => window.open(FEEDBACK, '__blank'),
105
115
  },
106
116
  {
107
- icon: DiscordIcon,
108
- key: 'discord',
109
- label: 'Discord',
110
- onClick: () => window.open(DISCORD, '__blank'),
117
+ icon: FileClockIcon,
118
+ key: 'changelog',
119
+ label: t('changelog'),
120
+ onClick: () => router.push('/changelog'),
111
121
  },
112
- ];
122
+ ].filter(Boolean) as CellProps[];
113
123
 
114
124
  const nextAuthSignOut: CellProps[] = [
115
125
  {
@@ -131,7 +141,7 @@ export const useCategory = () => {
131
141
  /* ↑ cloud slot ↑ */
132
142
  ...(canInstall ? pwa : []),
133
143
  ...(isLogin && !isServerMode ? data : []),
134
- ...helps,
144
+ ...(!hideDocs ? helps : []),
135
145
  ...(enableAuth && isLoginWithNextAuth ? nextAuthSignOut : []),
136
146
  ].filter(Boolean) as CellProps[];
137
147
 
@@ -1,15 +1,18 @@
1
1
  'use client';
2
2
 
3
3
  import { useTheme } from 'antd-style';
4
+ import dynamic from 'next/dynamic';
4
5
  import { memo } from 'react';
5
6
  import { Flexbox } from 'react-layout-kit';
6
7
 
7
- import CloudBanner, { BANNER_HEIGHT } from '@/features/AlertBanner/CloudBanner';
8
+ import { BANNER_HEIGHT } from '@/features/AlertBanner/CloudBanner';
8
9
  import { usePlatform } from '@/hooks/usePlatform';
9
10
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
10
11
 
11
12
  import { LayoutProps } from './type';
12
13
 
14
+ const CloudBanner = dynamic(() => import('@/features/AlertBanner/CloudBanner'));
15
+
13
16
  const Layout = memo<LayoutProps>(({ children, nav }) => {
14
17
  const { isPWA } = usePlatform();
15
18
  const theme = useTheme();
@@ -1,15 +1,16 @@
1
1
  'use client';
2
2
 
3
+ import dynamic from 'next/dynamic';
3
4
  import { usePathname } from 'next/navigation';
4
5
  import qs from 'query-string';
5
6
  import { memo } from 'react';
6
7
 
7
- import CloudBanner from '@/features/AlertBanner/CloudBanner';
8
8
  import { useQuery } from '@/hooks/useQuery';
9
9
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
10
10
 
11
11
  import { LayoutProps } from './type';
12
12
 
13
+ const CloudBanner = dynamic(() => import('@/features/AlertBanner/CloudBanner'));
13
14
  const MOBILE_NAV_ROUTES = new Set([
14
15
  '/chat',
15
16
  '/discover',
@@ -0,0 +1,25 @@
1
+ import { ReactNode } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import Hero from '../features/Hero';
5
+
6
+ type Props = { children: ReactNode };
7
+
8
+ const Layout = ({ children }: Props) => {
9
+ return (
10
+ <Flexbox
11
+ align={'center'}
12
+ style={{ height: '100%', overflowX: 'hidden', overflowY: 'auto' }}
13
+ width={'100%'}
14
+ >
15
+ <Flexbox gap={24} paddingBlock={24} paddingInline={16} style={{ width: 'min(100%, 1024px)' }}>
16
+ <Hero />
17
+ {children}
18
+ </Flexbox>
19
+ </Flexbox>
20
+ );
21
+ };
22
+
23
+ Layout.displayName = 'DesktopChangelogLayout';
24
+
25
+ export default Layout;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { MobileNavBar, MobileNavBarTitle } from '@lobehub/ui';
4
+ import { useRouter } from 'next/navigation';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { mobileHeaderSticky } from '@/styles/mobileHeader';
10
+
11
+ const Header = memo(() => {
12
+ const { t } = useTranslation('changelog');
13
+
14
+ const router = useRouter();
15
+ return (
16
+ <MobileNavBar
17
+ center={
18
+ <MobileNavBarTitle
19
+ title={
20
+ <Flexbox align={'center'} gap={4} horizontal>
21
+ {t('title')}
22
+ </Flexbox>
23
+ }
24
+ />
25
+ }
26
+ onBackClick={() => router.back()}
27
+ showBackButton
28
+ style={mobileHeaderSticky}
29
+ />
30
+ );
31
+ });
32
+
33
+ export default Header;
@@ -0,0 +1,21 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import MobileContentLayout from '@/components/server/MobileNavLayout';
4
+
5
+ import Hero from '../../features/Hero';
6
+ import Header from './Header';
7
+
8
+ type Props = { children: ReactNode };
9
+
10
+ const Layout = ({ children }: Props) => {
11
+ return (
12
+ <MobileContentLayout header={<Header />} padding={16}>
13
+ <Hero />
14
+ {children}
15
+ </MobileContentLayout>
16
+ );
17
+ };
18
+
19
+ Layout.displayName = 'MobileChangelogLayout';
20
+
21
+ export default Layout;
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+
5
+ export default dynamic(() => import('@/components/Error'));
@@ -0,0 +1,22 @@
1
+ import { FC, PropsWithChildren, ReactNode } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ const GridLayout: FC<PropsWithChildren<{ date?: ReactNode; mobile?: boolean }>> = ({
5
+ mobile,
6
+ children,
7
+ date,
8
+ }) => {
9
+ return (
10
+ <Flexbox horizontal={!mobile} wrap={'wrap'}>
11
+ <Flexbox flex={1} style={{ minWidth: 150, position: 'relative' }}>
12
+ {date}
13
+ </Flexbox>
14
+ <Flexbox flex={3} gap={16} style={{ minWidth: 'min(600px, 100%)', position: 'relative' }}>
15
+ {children}
16
+ </Flexbox>
17
+ {!mobile && <Flexbox flex={1} style={{ minWidth: 150, position: 'relative' }} />}
18
+ </Flexbox>
19
+ );
20
+ };
21
+
22
+ export default GridLayout;
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { useResponsive, useTheme } from 'antd-style';
4
+ import Link from 'next/link';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+ import urlJoin from 'url-join';
9
+
10
+ import { BRANDING_NAME } from '@/const/branding';
11
+ import { OFFICIAL_SITE, X } from '@/const/url';
12
+
13
+ import GridLayout from './GridLayout';
14
+
15
+ const Hero = memo(() => {
16
+ const { t } = useTranslation('changelog');
17
+ const theme = useTheme();
18
+ const { mobile } = useResponsive();
19
+ return (
20
+ <GridLayout>
21
+ <Flexbox gap={16} style={{ paddingTop: 32, zIndex: 1 }}>
22
+ <h1 style={{ fontSize: mobile ? 28 : 40, fontWeight: 'bold', margin: 0 }}>{t('title')}</h1>
23
+ <div style={{ fontSize: mobile ? 18 : 24, opacity: 0.6 }}>
24
+ {t('description', { appName: BRANDING_NAME })}
25
+ </div>
26
+ <Flexbox gap={8} horizontal style={{ fontSize: 16 }}>
27
+ <Link href={urlJoin(OFFICIAL_SITE, '/changelog/versions')} target={'_blank'}>
28
+ {t('actions.versions')}
29
+ </Link>
30
+ <div style={{ color: theme.colorInfo }}>·</div>
31
+ <Link href={X} target={'_blank'}>
32
+ {t('actions.followOnX')}
33
+ </Link>
34
+ </Flexbox>
35
+ </Flexbox>
36
+ </GridLayout>
37
+ );
38
+ });
39
+
40
+ export default Hero;
@@ -0,0 +1,56 @@
1
+ import { Typography } from '@lobehub/ui';
2
+ import { Divider } from 'antd';
3
+ import Link from 'next/link';
4
+ import urlJoin from 'url-join';
5
+
6
+ import { CustomMDX } from '@/components/mdx';
7
+ import Image from '@/components/mdx/Image';
8
+ import { OFFICIAL_SITE } from '@/const/url';
9
+ import { Locales } from '@/locales/resources';
10
+ import { ChangelogService } from '@/server/services/changelog';
11
+ import { ChangelogIndexItem } from '@/types/changelog';
12
+
13
+ import GridLayout from './GridLayout';
14
+ import PublishedTime from './PublishedTime';
15
+ import VersionTag from './VersionTag';
16
+
17
+ const Post = async ({
18
+ id,
19
+ mobile,
20
+ versionRange,
21
+ locale,
22
+ }: ChangelogIndexItem & { branch?: string; locale: Locales; mobile?: boolean }) => {
23
+ const changelogService = new ChangelogService();
24
+ const data = await changelogService.getPostById(id, { locale });
25
+
26
+ if (!data || !data.title) return null;
27
+
28
+ return (
29
+ <>
30
+ <Divider />
31
+ <GridLayout
32
+ date={
33
+ <PublishedTime
34
+ date={data.date.toISOString()}
35
+ style={{ lineHeight: mobile ? undefined : '60px' }}
36
+ template={'MMMM D, YYYY'}
37
+ />
38
+ }
39
+ mobile={mobile}
40
+ >
41
+ <Typography headerMultiple={mobile ? 0.2 : 0.3}>
42
+ <Link href={urlJoin(OFFICIAL_SITE, '/changelog', id)} style={{ color: 'inherit' }}>
43
+ <h1 id={id}>{data.rawTitle || data.title}</h1>
44
+ </Link>
45
+ <Image alt={data.title} src={data.image} />
46
+ <CustomMDX source={data.content} />
47
+ <Link href={urlJoin(OFFICIAL_SITE, '/changelog', id)} style={{ color: 'inherit' }}>
48
+ <VersionTag range={versionRange} />
49
+ </Link>
50
+ </Typography>
51
+ </GridLayout>
52
+ </>
53
+ );
54
+ };
55
+
56
+ export default Post;
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { createStyles } from 'antd-style';
4
+ import dayjs from 'dayjs';
5
+ import 'dayjs/locale/zh.js';
6
+ import { CSSProperties, FC } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ const useStyles = createStyles(({ css, token }) => {
10
+ return {
11
+ time: css`
12
+ margin-block: calc(var(--lobe-markdown-margin-multiple) * 1em);
13
+
14
+ font-size: 14px;
15
+ line-height: var(--lobe-markdown-line-height);
16
+ color: ${token.colorTextSecondary};
17
+ letter-spacing: 0.02em;
18
+ `,
19
+ };
20
+ });
21
+
22
+ interface PrivacyUpdatedProps {
23
+ className?: string;
24
+ date: string;
25
+ style?: CSSProperties;
26
+ template?: string;
27
+ }
28
+ const PublishedTime: FC<PrivacyUpdatedProps> = ({
29
+ date = new Date().toISOString(),
30
+ style,
31
+ className,
32
+ template = 'dddd, MMMM D YYYY',
33
+ }) => {
34
+ const { i18n } = useTranslation();
35
+ const { styles, cx } = useStyles();
36
+ const time = dayjs(date).locale(i18n.language).format(template);
37
+
38
+ return (
39
+ <time
40
+ aria-label={'published-date'}
41
+ className={cx(styles.time, className)}
42
+ dateTime={time}
43
+ style={style}
44
+ >
45
+ {time}
46
+ </time>
47
+ );
48
+ };
49
+
50
+ export default PublishedTime;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { Tag } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
5
+ import { memo } from 'react';
6
+
7
+ const useStyles = createStyles(({ token, css }) => {
8
+ return {
9
+ tag: css`
10
+ margin: 0;
11
+ padding-block: 4px;
12
+ padding-inline: 12px;
13
+
14
+ color: ${token.colorTextSecondary};
15
+
16
+ border-radius: 16px;
17
+ `,
18
+ };
19
+ });
20
+
21
+ const VersionTag = memo<{ range: string[] }>(({ range }) => {
22
+ const { styles } = useStyles();
23
+
24
+ return <Tag className={styles.tag}>{range.map((v) => 'v' + v).join(' ~ ')}</Tag>;
25
+ });
26
+
27
+ export default VersionTag;
@@ -0,0 +1,10 @@
1
+ import ServerLayout from '@/components/server/ServerLayout';
2
+
3
+ import Desktop from './_layout/Desktop';
4
+ import Mobile from './_layout/Mobile';
5
+
6
+ const MainLayout = ServerLayout({ Desktop, Mobile });
7
+
8
+ MainLayout.displayName = 'ChangelogLayout';
9
+
10
+ export default MainLayout;
@@ -0,0 +1,3 @@
1
+ import { Skeleton } from 'antd';
2
+
3
+ export default () => <Skeleton active paragraph={{ rows: 5 }} title={false} />;
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { useLayoutEffect } from 'react';
4
+
5
+ import { useQueryRoute } from '@/hooks/useQueryRoute';
6
+
7
+ /**
8
+ * @description: Changelog Modal (intercepting routes fallback when hard refresh)
9
+ * @example: /changelog/modal => /changelog
10
+ * @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942
11
+ */
12
+
13
+ const ChangelogModalFallback = () => {
14
+ const router = useQueryRoute();
15
+
16
+ useLayoutEffect(() => {
17
+ router.replace('/changelog');
18
+ }, []);
19
+
20
+ return null;
21
+ };
22
+
23
+ export default ChangelogModalFallback;
@@ -0,0 +1,3 @@
1
+ import dynamic from 'next/dynamic';
2
+
3
+ export default dynamic(() => import('@/components/404'));
@@ -0,0 +1,73 @@
1
+ import { Divider, Skeleton } from 'antd';
2
+ import { notFound } from 'next/navigation';
3
+ import { Fragment, Suspense } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+ import urlJoin from 'url-join';
6
+
7
+ import Pagination from '@/app/@modal/(.)changelog/modal/features/Pagination';
8
+ import StructuredData from '@/components/StructuredData';
9
+ import { serverFeatureFlags } from '@/config/featureFlags';
10
+ import { BRANDING_NAME } from '@/const/branding';
11
+ import { OFFICIAL_SITE } from '@/const/url';
12
+ import { ldModule } from '@/server/ld';
13
+ import { metadataModule } from '@/server/metadata';
14
+ import { ChangelogService } from '@/server/services/changelog';
15
+ import { translation } from '@/server/translation';
16
+ import { isMobileDevice } from '@/utils/server/responsive';
17
+
18
+ import GridLayout from './features/GridLayout';
19
+ import Post from './features/Post';
20
+
21
+ export const generateMetadata = async () => {
22
+ const { t } = await translation('metadata');
23
+ return metadataModule.generate({
24
+ canonical: urlJoin(OFFICIAL_SITE, 'changelog'),
25
+ description: t('changelog.description', { appName: BRANDING_NAME }),
26
+ title: t('changelog.title'),
27
+ url: '/changelog',
28
+ });
29
+ };
30
+
31
+ const Page = async () => {
32
+ const hideDocs = serverFeatureFlags().hideDocs;
33
+
34
+ if (hideDocs) return notFound();
35
+
36
+ const mobile = await isMobileDevice();
37
+ const { t, locale } = await translation('metadata');
38
+ const changelogService = new ChangelogService();
39
+ const data = await changelogService.getChangelogIndex();
40
+
41
+ const ld = ldModule.generate({
42
+ description: t('changelog.description', { appName: BRANDING_NAME }),
43
+ title: t('changelog.title', { appName: BRANDING_NAME }),
44
+ url: '/changelog',
45
+ });
46
+
47
+ return (
48
+ <>
49
+ <StructuredData ld={ld} />
50
+ <Flexbox gap={mobile ? 16 : 48}>
51
+ {data.map((item) => (
52
+ <Fragment key={item.id}>
53
+ <Suspense
54
+ fallback={
55
+ <GridLayout>
56
+ <Divider />
57
+ <Skeleton active paragraph={{ rows: 5 }} />
58
+ </GridLayout>
59
+ }
60
+ >
61
+ <Post locale={locale} mobile={mobile} {...item} />
62
+ </Suspense>
63
+ </Fragment>
64
+ ))}
65
+ </Flexbox>
66
+ <GridLayout>
67
+ <Pagination />
68
+ </GridLayout>
69
+ </>
70
+ );
71
+ };
72
+
73
+ export default Page;
@@ -4,6 +4,7 @@ import isEqual from 'fast-deep-equal';
4
4
  import { useHotkeys } from 'react-hotkeys-hook';
5
5
 
6
6
  import { HOTKEYS } from '@/const/hotkeys';
7
+ import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
7
8
  import { useChatStore } from '@/store/chat';
8
9
  import { chatSelectors } from '@/store/chat/selectors';
9
10
  import { useGlobalStore } from '@/store/global';
@@ -13,12 +14,18 @@ const HotKeys = () => {
13
14
  const lastMessage = useChatStore(chatSelectors.latestMessage, isEqual);
14
15
 
15
16
  const toggleZenMode = useGlobalStore((s) => s.toggleZenMode);
17
+ const openChatSettings = useOpenChatSettings();
16
18
 
17
19
  useHotkeys(HOTKEYS.zenMode, toggleZenMode, {
18
20
  enableOnFormTags: true,
19
21
  preventDefault: true,
20
22
  });
21
23
 
24
+ useHotkeys(HOTKEYS.chatSettings, () => openChatSettings(), {
25
+ enableOnFormTags: true,
26
+ preventDefault: true,
27
+ });
28
+
22
29
  useHotkeys(
23
30
  HOTKEYS.regenerate,
24
31
  () => {