@lobehub/chat 0.120.2 → 0.120.4

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 (109) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +47 -22
  3. package/README.zh-CN.md +53 -26
  4. package/docs/Deployment/Upstream-Sync.md +8 -2
  5. package/docs/Deployment/Upstream-Sync.zh-CN.md +9 -3
  6. package/docs/Development/Add-New-Locale.md +9 -1
  7. package/docs/Development/Add-New-Locale.zh-CN.md +9 -1
  8. package/docs/Development/Contributing-Guidelines.md +83 -0
  9. package/docs/Development/Contributing-Guidelines.zh-CN.md +13 -0
  10. package/docs/Development/Internationalization-Implementation.md +125 -0
  11. package/docs/Development/Internationalization-Implementation.zh-CN.md +9 -1
  12. package/docs/Development/State-Management/State-Management-Intro.md +224 -0
  13. package/docs/Development/State-Management/{index.zh-CN.md → State-Management-Intro.zh-CN.md} +25 -28
  14. package/docs/Home.md +9 -6
  15. package/docs/_Sidebar.md +10 -7
  16. package/locales/ar/common.json +1 -0
  17. package/locales/ar/setting.json +1 -3
  18. package/locales/de-DE/common.json +1 -0
  19. package/locales/de-DE/setting.json +1 -3
  20. package/locales/en-US/common.json +1 -0
  21. package/locales/en-US/setting.json +1 -3
  22. package/locales/es-ES/common.json +1 -0
  23. package/locales/es-ES/setting.json +1 -3
  24. package/locales/fr-FR/common.json +1 -0
  25. package/locales/fr-FR/setting.json +1 -3
  26. package/locales/it-IT/common.json +1 -0
  27. package/locales/it-IT/setting.json +1 -3
  28. package/locales/ja-JP/common.json +1 -0
  29. package/locales/ja-JP/setting.json +1 -3
  30. package/locales/ko-KR/common.json +1 -0
  31. package/locales/ko-KR/setting.json +1 -3
  32. package/locales/nl-NL/common.json +1 -0
  33. package/locales/nl-NL/setting.json +1 -3
  34. package/locales/pl-PL/common.json +1 -0
  35. package/locales/pl-PL/setting.json +1 -3
  36. package/locales/pt-BR/common.json +1 -0
  37. package/locales/pt-BR/setting.json +1 -3
  38. package/locales/ru-RU/common.json +1 -0
  39. package/locales/ru-RU/setting.json +1 -3
  40. package/locales/tr-TR/common.json +1 -0
  41. package/locales/tr-TR/setting.json +1 -3
  42. package/locales/vi-VN/common.json +1 -0
  43. package/locales/vi-VN/setting.json +1 -3
  44. package/locales/zh-CN/common.json +1 -0
  45. package/locales/zh-CN/setting.json +1 -3
  46. package/locales/zh-TW/common.json +1 -0
  47. package/locales/zh-TW/setting.json +1 -3
  48. package/next.config.mjs +2 -2
  49. package/package.json +8 -7
  50. package/scripts/docsWorkflow/const.ts +1 -1
  51. package/scripts/docsWorkflow/index.ts +9 -3
  52. package/scripts/readmeWorkflow/syncAgentIndex.ts +2 -2
  53. package/scripts/readmeWorkflow/syncPluginIndex.ts +2 -2
  54. package/src/app/chat/(desktop)/features/ChatHeader.tsx +6 -6
  55. package/src/app/chat/{features → (desktop)/features/SideBar}/SystemRole/index.tsx +8 -5
  56. package/src/app/chat/(desktop)/features/{SideBar.tsx → SideBar/index.tsx} +2 -3
  57. package/src/app/chat/(mobile)/index.tsx +17 -4
  58. package/src/app/chat/(mobile)/mobile/page.tsx +30 -2
  59. package/src/app/chat/components/SessionHydration/index.tsx +44 -0
  60. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +6 -1
  61. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +0 -2
  62. package/src/app/chat/features/SessionListContent/List/index.tsx +7 -3
  63. package/src/app/chat/features/TopicListContent/Header.tsx +1 -1
  64. package/src/app/chat/page.tsx +7 -3
  65. package/src/app/home/Redirect.tsx +2 -2
  66. package/src/app/metadata.ts +6 -0
  67. package/src/app/settings/common/Common.tsx +5 -6
  68. package/src/app/welcome/features/Banner/index.tsx +6 -7
  69. package/src/components/StoreHydration/ChatHydration/index.tsx +33 -0
  70. package/src/config/server.ts +4 -0
  71. package/src/const/locale.ts +2 -2
  72. package/src/const/url.ts +1 -1
  73. package/src/database/models/__tests__/message.test.ts +3 -2
  74. package/src/database/models/message.ts +13 -9
  75. package/src/database/schemas/message.ts +1 -1
  76. package/src/features/ChatInput/ActionBar/Clear.tsx +0 -2
  77. package/src/features/ChatInput/useChatInput.ts +2 -6
  78. package/src/features/Conversation/hooks/useInitConversation.ts +0 -2
  79. package/src/features/Conversation/index.tsx +7 -2
  80. package/src/features/PluginDetailModal/index.tsx +0 -2
  81. package/src/features/PluginDevModal/index.tsx +0 -1
  82. package/src/features/SideBar/TopActions.tsx +5 -5
  83. package/src/layout/GlobalLayout/Locale.tsx +37 -10
  84. package/src/layout/GlobalLayout/StoreHydration.tsx +12 -11
  85. package/src/layout/GlobalLayout/index.tsx +1 -1
  86. package/src/locales/default/setting.ts +0 -2
  87. package/src/locales/resources.ts +9 -7
  88. package/src/store/chat/slices/message/action.ts +1 -1
  89. package/src/store/chat/store.ts +2 -34
  90. package/src/store/global/slices/common/action.ts +6 -0
  91. package/src/store/global/slices/common/initialState.ts +5 -0
  92. package/src/store/session/index.ts +0 -1
  93. package/src/store/session/initialState.ts +6 -2
  94. package/src/store/session/slices/agent/selectors.ts +3 -1
  95. package/src/store/session/slices/session/action.ts +13 -17
  96. package/src/store/session/slices/session/selectors/list.ts +7 -0
  97. package/src/store/session/store.ts +5 -46
  98. package/src/app/chat/(mobile)/layout.mobile.tsx +0 -17
  99. package/src/app/chat/(mobile)/mobile/index.tsx +0 -30
  100. package/src/app/chat/(mobile)/mobile/layout.mobile.tsx +0 -11
  101. package/src/store/session/hooks/index.ts +0 -4
  102. package/src/store/session/hooks/useEffectAfterHydrated.ts +0 -27
  103. package/src/store/session/hooks/useOnFinishHydrationSession.ts +0 -19
  104. package/src/store/session/hooks/useSessionChatInit.ts +0 -12
  105. package/src/store/session/hooks/useSessionHydrated.ts +0 -13
  106. /package/docs/Development/{index.md → Intro.md} +0 -0
  107. /package/docs/Development/{index.zh-CN.md → Intro.zh-CN.md} +0 -0
  108. /package/src/app/chat/{features → (desktop)/features/SideBar}/SystemRole/style.ts +0 -0
  109. /package/src/app/chat/{features → components}/SidebarHeader/index.tsx +0 -0
@@ -94,10 +94,8 @@ const Actions = memo<ActionProps>(({ id, setOpen }) => {
94
94
  domEvent.stopPropagation();
95
95
 
96
96
  modal.confirm({
97
- cancelText: t('cancel'),
98
97
  centered: true,
99
98
  okButtonProps: { danger: true },
100
- okText: t('ok'),
101
99
  onOk: () => {
102
100
  removeSession(id);
103
101
  },
@@ -4,7 +4,8 @@ import { memo } from 'react';
4
4
  import LazyLoad from 'react-lazy-load';
5
5
 
6
6
  import { SESSION_CHAT_URL } from '@/const/url';
7
- import { useSessionHydrated, useSessionStore } from '@/store/session';
7
+ import { useSessionStore } from '@/store/session';
8
+ import { sessionSelectors } from '@/store/session/selectors';
8
9
  import { LobeAgentSession } from '@/types/session';
9
10
 
10
11
  import AddButton from './AddButton';
@@ -21,9 +22,12 @@ interface SessionListProps {
21
22
  dataSource: LobeAgentSession[];
22
23
  }
23
24
  const SessionList = memo<SessionListProps>(({ dataSource }) => {
24
- const [activeSession, switchSession] = useSessionStore((s) => [s.activeSession, s.switchSession]);
25
+ const [activeSession, switchSession, isInit] = useSessionStore((s) => [
26
+ s.activeSession,
27
+ s.switchSession,
28
+ sessionSelectors.isSessionListInit(s),
29
+ ]);
25
30
  const { styles } = useStyles();
26
- const isInit = useSessionHydrated();
27
31
 
28
32
  const { mobile } = useResponsive();
29
33
 
@@ -8,7 +8,7 @@ import { Flexbox } from 'react-layout-kit';
8
8
  import { useChatStore } from '@/store/chat';
9
9
  import { topicSelectors } from '@/store/chat/selectors';
10
10
 
11
- import SidebarHeader from '../SidebarHeader';
11
+ import SidebarHeader from '../../components/SidebarHeader';
12
12
  import TopicSearchBar from './TopicSearchBar';
13
13
 
14
14
  const Header = memo(() => {
@@ -2,6 +2,7 @@ import { isMobileDevice } from '@/utils/responsive';
2
2
 
3
3
  import DesktopPage from './(desktop)';
4
4
  import MobilePage from './(mobile)';
5
+ import SessionHydration from './components/SessionHydration';
5
6
  import Migration from './features/Migration';
6
7
 
7
8
  const Page = () => {
@@ -10,9 +11,12 @@ const Page = () => {
10
11
  const Page = mobile ? MobilePage : DesktopPage;
11
12
 
12
13
  return (
13
- <Migration>
14
- <Page />
15
- </Migration>
14
+ <>
15
+ <Migration>
16
+ <Page />
17
+ </Migration>
18
+ <SessionHydration />
19
+ </>
16
20
  );
17
21
  };
18
22
 
@@ -24,11 +24,11 @@ const Redirect = memo(() => {
24
24
  useGlobalStore.getState().switchSideBar(SidebarTabKey.Chat);
25
25
 
26
26
  if (hasData) {
27
- router.push('/chat');
27
+ router.replace('/chat');
28
28
 
29
29
  switchSession();
30
30
  } else {
31
- router.push('/welcome');
31
+ router.replace('/welcome');
32
32
  }
33
33
  });
34
34
  }, []);
@@ -1,9 +1,14 @@
1
1
  import { Metadata } from 'next';
2
2
 
3
+ import { getServerConfig } from '@/config/server';
4
+
3
5
  import pkg from '../../package.json';
4
6
 
5
7
  const title = 'LobeChat';
6
8
  const { description, homepage } = pkg;
9
+
10
+ const { METADATA_BASE_URL = 'https://chat-preview.lobehub.com/' } = getServerConfig();
11
+
7
12
  const metadata: Metadata = {
8
13
  appleWebApp: {
9
14
  statusBarStyle: 'black-translucent',
@@ -18,6 +23,7 @@ const metadata: Metadata = {
18
23
  'https://registry.npmmirror.com/@lobehub/assets-favicons/latest/files/assets/favicon.ico',
19
24
  },
20
25
  manifest: '/manifest.json',
26
+ metadataBase: new URL(METADATA_BASE_URL),
21
27
  openGraph: {
22
28
  description: description,
23
29
  images: [
@@ -49,10 +49,8 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
49
49
 
50
50
  const handleReset = useCallback(() => {
51
51
  modal.confirm({
52
- cancelText: t('cancel', { ns: 'common' }),
53
52
  centered: true,
54
53
  okButtonProps: { danger: true },
55
- okText: t('ok', { ns: 'common' }),
56
54
  onOk: () => {
57
55
  resetSettings();
58
56
  form.setFieldsValue(DEFAULT_SETTINGS);
@@ -63,12 +61,10 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
63
61
 
64
62
  const handleClear = useCallback(() => {
65
63
  modal.confirm({
66
- cancelText: t('cancel', { ns: 'common' }),
67
64
  centered: true,
68
65
  okButtonProps: {
69
66
  danger: true,
70
67
  },
71
- okText: t('ok', { ns: 'common' }),
72
68
  onOk: async () => {
73
69
  await clearSessions();
74
70
  await removeAllPlugins();
@@ -136,20 +132,23 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
136
132
  <SliderWithInput
137
133
  marks={{
138
134
  12: {
139
- label: t('settingTheme.fontSize.marks.small'),
135
+ label: 'A',
140
136
  style: {
137
+ fontSize: 12,
141
138
  marginTop: 4,
142
139
  },
143
140
  },
144
141
  14: {
145
142
  label: t('settingTheme.fontSize.marks.normal'),
146
143
  style: {
144
+ fontSize: 14,
147
145
  marginTop: 4,
148
146
  },
149
147
  },
150
148
  18: {
151
- label: t('settingTheme.fontSize.marks.large'),
149
+ label: 'A',
152
150
  style: {
151
+ fontSize: 18,
153
152
  marginTop: 4,
154
153
  },
155
154
  },
@@ -3,11 +3,13 @@
3
3
  import { Icon } from '@lobehub/ui';
4
4
  import { Button } from 'antd';
5
5
  import { SendHorizonal } from 'lucide-react';
6
+ import { useRouter } from 'next/navigation';
6
7
  import { memo } from 'react';
7
8
  import { useTranslation } from 'react-i18next';
8
9
  import { Flexbox } from 'react-layout-kit';
9
10
 
10
11
  import DataImporter from '@/features/DataImporter';
12
+ import { useGlobalStore } from '@/store/global';
11
13
  import { useSessionStore } from '@/store/session';
12
14
 
13
15
  import Hero from './Hero';
@@ -15,13 +17,10 @@ import { useStyles } from './style';
15
17
 
16
18
  const Banner = memo<{ mobile?: boolean }>(({ mobile }) => {
17
19
  const { t } = useTranslation('welcome');
20
+ const router = useRouter();
18
21
  const { styles } = useStyles();
19
- const [switchSession, switchBackToChat, router, isMobile] = useSessionStore((s) => [
20
- s.switchSession,
21
- s.switchBackToChat,
22
- s.router,
23
- s.isMobile,
24
- ]);
22
+ const [switchSession] = useSessionStore((s) => [s.switchSession]);
23
+ const [switchBackToChat, isMobile] = useGlobalStore((s) => [s.switchBackToChat, s.isMobile]);
25
24
 
26
25
  return (
27
26
  <>
@@ -46,7 +45,7 @@ const Banner = memo<{ mobile?: boolean }>(({ mobile }) => {
46
45
  </DataImporter>
47
46
  <Button
48
47
  block={mobile}
49
- onClick={() => (isMobile ? router?.push('/chat') : switchBackToChat())}
48
+ onClick={() => (isMobile ? router.push('/chat') : switchBackToChat())}
50
49
  size={'large'}
51
50
  type={'primary'}
52
51
  >
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { useQueryState } from 'nuqs';
4
+ import { memo, useEffect } from 'react';
5
+ import { createStoreUpdater } from 'zustand-utils';
6
+
7
+ import { useChatStore } from '@/store/chat';
8
+
9
+ // sync outside state to useChatStore
10
+ const ChatHydration = memo(() => {
11
+ const useStoreUpdater = createStoreUpdater(useChatStore);
12
+
13
+ // two-way bindings the topic params to chat store
14
+ const [topic, setTopic] = useQueryState('topic', { history: 'replace', throttleMs: 500 });
15
+ useStoreUpdater('activeTopicId', topic);
16
+
17
+ useEffect(() => {
18
+ const unsubscribe = useChatStore.subscribe(
19
+ (s) => s.activeTopicId,
20
+ (state) => {
21
+ setTopic(!state ? null : state);
22
+ },
23
+ );
24
+
25
+ return () => {
26
+ unsubscribe();
27
+ };
28
+ }, []);
29
+
30
+ return null;
31
+ });
32
+
33
+ export default ChatHydration;
@@ -16,6 +16,8 @@ declare global {
16
16
 
17
17
  IMGUR_CLIENT_ID?: string;
18
18
 
19
+ METADATA_BASE_URL?: string;
20
+
19
21
  AGENTS_INDEX_URL?: string;
20
22
 
21
23
  PLUGINS_INDEX_URL?: string;
@@ -51,6 +53,8 @@ export const getServerConfig = () => {
51
53
  OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
52
54
  OPENAI_FUNCTION_REGIONS: regions,
53
55
 
56
+ METADATA_BASE_URL: process.env.METADATA_BASE_URL,
57
+
54
58
  AZURE_API_KEY: process.env.AZURE_API_KEY,
55
59
  AZURE_API_VERSION: process.env.AZURE_API_VERSION,
56
60
  USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1',
@@ -1,4 +1,4 @@
1
- import { Locales, normalizeLocale, supportLocales } from '@/locales/resources';
1
+ import { normalizeLocale, supportLocales } from '@/locales/resources';
2
2
 
3
3
  export const DEFAULT_LANG = 'en-US';
4
4
  export const LOBE_LOCALE_COOKIE = 'LOBE_LOCALE';
@@ -7,6 +7,6 @@ export const LOBE_LOCALE_COOKIE = 'LOBE_LOCALE';
7
7
  * Check if the language is supported
8
8
  * @param locale
9
9
  */
10
- export const isLocaleNotSupport = (locale: Locales) => {
10
+ export const isLocaleNotSupport = (locale: string) => {
11
11
  return normalizeLocale(locale) === DEFAULT_LANG || !supportLocales.includes(locale);
12
12
  };
package/src/const/url.ts CHANGED
@@ -17,5 +17,5 @@ export const AGENTS_INDEX_GITHUB = 'https://github.com/lobehub/lobe-chat-agents'
17
17
  export const AGENTS_INDEX_GITHUB_ISSUE = urlJoin(AGENTS_INDEX_GITHUB, 'issues/new');
18
18
 
19
19
  export const SESSION_CHAT_URL = (id: string = INBOX_SESSION_ID, mobile?: boolean) =>
20
- mobile ? `/chat/mobile#session=${id}` : `/chat#session=${id}`;
20
+ mobile ? `/chat/mobile?session=${id}` : `/chat?session=${id}`;
21
21
  export const MANUAL_UPGRADE_URL = 'https://github.com/lobehub/lobe-chat/wiki/Upstream-Sync';
@@ -329,14 +329,15 @@ describe('MessageModel', () => {
329
329
  describe('duplicateMessages', () => {
330
330
  it('should duplicate messages and update parentId for copied messages', async () => {
331
331
  // 创建原始消息和父消息
332
- const parentMessageData: DB_Message = {
332
+ const parentMessageData: CreateMessageParams = {
333
333
  content: 'Parent message content',
334
334
  role: 'user',
335
335
  sessionId: 'session1',
336
+ topicId: undefined,
336
337
  };
337
338
  const parentMessage = await MessageModel.create(parentMessageData);
338
339
 
339
- const childMessageData: DB_Message = {
340
+ const childMessageData: CreateMessageParams = {
340
341
  content: 'Child message content',
341
342
  role: 'user',
342
343
  sessionId: 'session1',
@@ -46,14 +46,13 @@ class _MessageModel extends BaseModel {
46
46
  }: QueryMessageParams): Promise<ChatMessage[]> {
47
47
  const offset = current * pageSize;
48
48
 
49
- const query =
50
- topicId !== undefined
51
- ? // TODO: The query {"sessionId":"xxx","topicId":"xxx"} on messages would benefit of a compound index [sessionId+topicId]
52
- this.table.where({ sessionId, topicId }) // Use a compound index
53
- : this.table
54
- .where('sessionId')
55
- .equals(sessionId)
56
- .and((message) => !message.topicId);
49
+ const query = !!topicId
50
+ ? // TODO: The query {"sessionId":"xxx","topicId":"xxx"} on messages would benefit of a compound index [sessionId+topicId]
51
+ this.table.where({ sessionId, topicId }) // Use a compound index
52
+ : this.table
53
+ .where('sessionId')
54
+ .equals(sessionId)
55
+ .and((message) => !message.topicId);
57
56
 
58
57
  const dbMessages: DBModel<DB_Message>[] = await query
59
58
  .sortBy('createdAt')
@@ -221,7 +220,12 @@ class _MessageModel extends BaseModel {
221
220
  tts,
222
221
  ...item
223
222
  }: DBModel<DB_Message>): ChatMessage => {
224
- return { ...item, extra: { fromModel, translate, tts }, meta: {} };
223
+ return {
224
+ ...item,
225
+ extra: { fromModel, translate, tts },
226
+ meta: {},
227
+ topicId: item.topicId ?? undefined,
228
+ };
225
229
  };
226
230
  }
227
231
 
@@ -31,7 +31,7 @@ export const DB_MessageSchema = z.object({
31
31
  parentId: z.string().optional(),
32
32
  quotaId: z.string().optional(),
33
33
  sessionId: z.string(),
34
- topicId: z.string().optional(),
34
+ topicId: z.string().nullable().optional(),
35
35
  });
36
36
 
37
37
  export type DB_Message = z.infer<typeof DB_MessageSchema>;
@@ -22,9 +22,7 @@ const Clear = memo(() => {
22
22
 
23
23
  return (
24
24
  <Popconfirm
25
- cancelText={t('cancel', { ns: 'common' })}
26
25
  okButtonProps={{ danger: true }}
27
- okText={t('ok', { ns: 'common' })}
28
26
  onConfirm={() => {
29
27
  resetConversation();
30
28
  }}
@@ -1,7 +1,6 @@
1
1
  import { TextAreaRef } from 'antd/es/input/TextArea';
2
2
  import { useCallback, useRef, useState } from 'react';
3
3
 
4
- import { useIsMobile } from '@/hooks/useIsMobile';
5
4
  import { useChatStore } from '@/store/chat';
6
5
  import { useSessionStore } from '@/store/session';
7
6
  import { agentSelectors } from '@/store/session/selectors';
@@ -20,15 +19,12 @@ export const useChatInput = () => {
20
19
  s.updateInputMessage,
21
20
  s.stopGenerateMessage,
22
21
  ]);
23
- const mobile = useIsMobile();
24
22
 
25
23
  const handleSend = useCallback(() => {
26
24
  setExpand(false);
27
- if (mobile) {
28
- ref?.current?.blur();
29
- }
25
+
30
26
  onSend();
31
- }, [onSend, mobile]);
27
+ }, [onSend]);
32
28
 
33
29
  return {
34
30
  canUpload,
@@ -28,8 +28,6 @@ export const useInitConversation = () => {
28
28
  checkPluginsIsInstalled(plugins);
29
29
 
30
30
  useEffect(() => {
31
- useChatStore.persist.rehydrate();
32
-
33
31
  // // when activeId changed, switch topic to undefined
34
32
  const unsubscribe = useSessionStore.subscribe(
35
33
  (s) => s.activeId,
@@ -2,6 +2,7 @@ import { createStyles } from 'antd-style';
2
2
  import { ReactNode, memo } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
 
5
+ import ChatHydration from '@/components/StoreHydration/ChatHydration';
5
6
  import { useGlobalStore } from '@/store/global';
6
7
  import { settingsSelectors } from '@/store/global/selectors';
7
8
 
@@ -50,12 +51,16 @@ const Conversation = memo<ConversationProps>(({ chatInput, mobile }) => {
50
51
  const init = useInitConversation();
51
52
 
52
53
  return (
53
- // relative is required, ChatInput's absolute position needs it
54
- <Flexbox flex={1} style={{ position: 'relative' }}>
54
+ <Flexbox
55
+ flex={1}
56
+ // position: 'relative' is required, ChatInput's absolute position needs it
57
+ style={{ position: 'relative' }}
58
+ >
55
59
  <div className={styles}>
56
60
  {init ? <ChatList mobile={mobile} /> : <SkeletonList mobile={mobile} />}
57
61
  </div>
58
62
  {chatInput}
63
+ <ChatHydration />
59
64
  </Flexbox>
60
65
  );
61
66
  });
@@ -33,8 +33,6 @@ const PluginDetailModal = memo<PluginDetailModalProps>(
33
33
  return (
34
34
  <Modal
35
35
  allowFullscreen
36
- cancelText={t('cancel', { ns: 'common' })}
37
- okText={t('ok', { ns: 'common' })}
38
36
  onCancel={onClose}
39
37
  onOk={() => {
40
38
  onClose();
@@ -96,7 +96,6 @@ const DevModal = memo<DevModalProps>(
96
96
  >
97
97
  <Modal
98
98
  allowFullscreen
99
- cancelText={t('cancel', { ns: 'common' })}
100
99
  footer={footer}
101
100
  okText={t('dev.save')}
102
101
  onCancel={(e) => {
@@ -1,10 +1,10 @@
1
1
  import { ActionIcon } from '@lobehub/ui';
2
- import { Bot, MessageSquare } from 'lucide-react';
2
+ import { Compass, MessageSquare } from 'lucide-react';
3
3
  import Link from 'next/link';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
- import { GlobalStore } from '@/store/global';
7
+ import { GlobalStore, useGlobalStore } from '@/store/global';
8
8
  import { SidebarTabKey } from '@/store/global/initialState';
9
9
  import { useSessionStore } from '@/store/session';
10
10
 
@@ -15,7 +15,7 @@ export interface TopActionProps {
15
15
 
16
16
  const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
17
17
  const { t } = useTranslation('common');
18
- const switchBackToChat = useSessionStore((s) => s.switchBackToChat);
18
+ const switchBackToChat = useGlobalStore((s) => s.switchBackToChat);
19
19
 
20
20
  return (
21
21
  <>
@@ -23,7 +23,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
23
23
  href={'/chat'}
24
24
  onClick={(e) => {
25
25
  e.preventDefault();
26
- switchBackToChat();
26
+ switchBackToChat(useSessionStore.getState().activeId);
27
27
  setTab(SidebarTabKey.Chat);
28
28
  }}
29
29
  >
@@ -38,7 +38,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
38
38
  <Link href={'/market'}>
39
39
  <ActionIcon
40
40
  active={tab === SidebarTabKey.Market}
41
- icon={Bot}
41
+ icon={Compass}
42
42
  onClick={() => {
43
43
  setTab(SidebarTabKey.Market);
44
44
  }}
@@ -1,24 +1,39 @@
1
1
  import { ConfigProvider } from 'antd';
2
- import { PropsWithChildren, memo, useState } from 'react';
2
+ import { PropsWithChildren, memo, useEffect, useState } from 'react';
3
3
  import useSWR from 'swr';
4
4
 
5
5
  import { createI18nNext } from '@/locales/create';
6
+ import { normalizeLocale } from '@/locales/resources';
6
7
  import { useOnFinishHydrationGlobal } from '@/store/global';
7
8
  import { isOnServerSide } from '@/utils/env';
8
9
  import { switchLang } from '@/utils/switchLang';
9
10
 
11
+ const getAntdLocale = async (lang?: string) => {
12
+ let normalLang = normalizeLocale(lang);
13
+
14
+ // due to antd only have ar-EG locale, we need to convert ar to ar-EG
15
+ // refs: https://ant.design/docs/react/i18n
16
+
17
+ // And we don't want to handle it in `normalizeLocale` function
18
+ // because of other locale files are all `ar` not `ar-EG`
19
+ if (normalLang === 'ar') normalLang = 'ar-EG';
20
+
21
+ const { default: locale } = await import(`antd/locale/${normalLang.replace('-', '_')}.js`);
22
+
23
+ return locale;
24
+ };
25
+
10
26
  interface LocaleLayoutProps extends PropsWithChildren {
11
- lang?: string;
27
+ defaultLang?: string;
12
28
  }
13
29
 
14
- const Locale = memo<LocaleLayoutProps>(({ children, lang }) => {
15
- const { data: locale } = useSWR(
16
- lang,
17
- async () =>
18
- await import(`antd/locale/${lang?.includes('-') ? lang?.replace('-', '_') : 'en_US'}.js`),
19
- { revalidateOnFocus: false },
20
- );
21
- const [i18n] = useState(createI18nNext(lang));
30
+ const Locale = memo<LocaleLayoutProps>(({ children, defaultLang }) => {
31
+ const [i18n] = useState(createI18nNext(defaultLang));
32
+ const [lang, setLang] = useState(defaultLang);
33
+
34
+ const { data: locale } = useSWR(['antd-locale', lang], ([, key]) => getAntdLocale(key), {
35
+ revalidateOnFocus: false,
36
+ });
22
37
 
23
38
  // if run on server side, init i18n instance everytime
24
39
  if (isOnServerSide) {
@@ -38,6 +53,18 @@ const Locale = memo<LocaleLayoutProps>(({ children, lang }) => {
38
53
  }
39
54
  }, []);
40
55
 
56
+ // handle i18n instance language change
57
+ useEffect(() => {
58
+ const handleLang = (e: string) => {
59
+ setLang(e);
60
+ };
61
+
62
+ i18n.instance.on('languageChanged', handleLang);
63
+ return () => {
64
+ i18n.instance.off('languageChanged', handleLang);
65
+ };
66
+ }, [i18n]);
67
+
41
68
  return <ConfigProvider locale={locale}>{children}</ConfigProvider>;
42
69
  });
43
70
 
@@ -2,32 +2,32 @@ import { useResponsive } from 'antd-style';
2
2
  import { useRouter } from 'next/navigation';
3
3
  import { memo, useEffect } from 'react';
4
4
 
5
- import { useGlobalStore } from '@/store/global';
6
- import { useEffectAfterSessionHydrated, useSessionStore } from '@/store/session';
5
+ import { useEffectAfterGlobalHydrated, useGlobalStore } from '@/store/global';
7
6
 
8
7
  const StoreHydration = memo(() => {
9
- const router = useRouter();
10
-
11
8
  const useFetchGlobalConfig = useGlobalStore((s) => s.useFetchGlobalConfig);
9
+ useFetchGlobalConfig();
12
10
 
13
11
  useEffect(() => {
14
12
  // refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
15
- useSessionStore.persist.rehydrate();
16
13
  useGlobalStore.persist.rehydrate();
17
14
  }, []);
18
15
 
19
- useFetchGlobalConfig();
20
-
21
16
  const { mobile } = useResponsive();
22
-
23
- useEffectAfterSessionHydrated(
17
+ useEffectAfterGlobalHydrated(
24
18
  (store) => {
25
- store.setState({ isMobile: mobile });
19
+ const prevState = store.getState().isMobile;
20
+
21
+ if (prevState !== mobile) {
22
+ store.setState({ isMobile: mobile });
23
+ }
26
24
  },
27
25
  [mobile],
28
26
  );
29
27
 
30
- useEffectAfterSessionHydrated(
28
+ const router = useRouter();
29
+
30
+ useEffectAfterGlobalHydrated(
31
31
  (store) => {
32
32
  store.setState({ router });
33
33
  },
@@ -36,6 +36,7 @@ const StoreHydration = memo(() => {
36
36
 
37
37
  useEffect(() => {
38
38
  router.prefetch('/chat');
39
+ router.prefetch('/chat/settings');
39
40
  router.prefetch('/market');
40
41
  router.prefetch('/settings/common');
41
42
  router.prefetch('/settings/agent');
@@ -48,7 +48,7 @@ interface GlobalLayoutProps extends AppThemeProps {
48
48
 
49
49
  const GlobalLayout = ({ children, defaultLang, ...theme }: GlobalLayoutProps) => (
50
50
  <AppTheme {...theme}>
51
- <Locale lang={defaultLang}>
51
+ <Locale defaultLang={defaultLang}>
52
52
  <StoreHydration />
53
53
  <Container>{children}</Container>
54
54
  <DebugUI />
@@ -252,9 +252,7 @@ export default {
252
252
  fontSize: {
253
253
  desc: '聊天内容的字体大小',
254
254
  marks: {
255
- large: '大',
256
255
  normal: '标准',
257
- small: '小',
258
256
  },
259
257
  title: '字体大小',
260
258
  },
@@ -19,7 +19,9 @@ export const locales = [
19
19
  export type DefaultResources = typeof resources;
20
20
  export type Locales = (typeof locales)[number];
21
21
 
22
- export const normalizeLocale = (locale: string) => {
22
+ export const normalizeLocale = (locale?: string) => {
23
+ if (!locale) return 'en-US';
24
+
23
25
  switch (locale) {
24
26
  case 'zh-CN':
25
27
  case 'zh': {
@@ -43,8 +45,8 @@ type LocaleOptions = {
43
45
 
44
46
  export const localeOptions: LocaleOptions = [
45
47
  {
46
- label: 'العربية',
47
- value: 'ar',
48
+ label: 'English',
49
+ value: 'en-US',
48
50
  },
49
51
  {
50
52
  label: '简体中文',
@@ -54,10 +56,6 @@ export const localeOptions: LocaleOptions = [
54
56
  label: '繁體中文',
55
57
  value: 'zh-TW',
56
58
  },
57
- {
58
- label: 'English',
59
- value: 'en-US',
60
- },
61
59
  {
62
60
  label: '日本語',
63
61
  value: 'ja-JP',
@@ -74,6 +72,10 @@ export const localeOptions: LocaleOptions = [
74
72
  label: 'Español',
75
73
  value: 'es-ES',
76
74
  },
75
+ {
76
+ label: 'العربية',
77
+ value: 'ar',
78
+ },
77
79
  {
78
80
  label: 'Français',
79
81
  value: 'fr-FR',