@lobehub/chat 0.161.12 → 0.161.14

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.
@@ -3,55 +3,61 @@ description: 'Report an bug'
3
3
  title: '[Bug] '
4
4
  labels: ['🐛 Bug']
5
5
  body:
6
- - type: dropdown
6
+ - type: checkboxes
7
7
  attributes:
8
- label: '💻 Operating System'
8
+ label: '📦 Environment'
9
9
  options:
10
- - Windows
11
- - macOS
12
- - Ubuntu
13
- - Other Linux
14
- - iOS
15
- - Android
16
- - Other
10
+ - label: 'Official'
11
+ - label: 'Official Preview'
12
+ - label: 'Vercel / Zeabur / Sealos'
13
+ - label: 'Docker'
14
+ - label: 'Other'
17
15
  validations:
18
16
  required: true
19
- - type: dropdown
17
+ - type: input
20
18
  attributes:
21
- label: '📦 Environment'
19
+ label: '📌 Version'
20
+ validations:
21
+ required: true
22
+
23
+ - type: checkboxes
24
+ attributes:
25
+ label: '💻 Operating System'
22
26
  options:
23
- - Official Preview
24
- - Vercel / Zeabur / Sealos
25
- - Docker
26
- - Other
27
+ - label: 'Windows'
28
+ - label: 'macOS'
29
+ - label: 'Ubuntu'
30
+ - label: 'Other Linux'
31
+ - label: 'iOS'
32
+ - label: 'Android'
33
+ - label: 'Other'
27
34
  validations:
28
35
  required: true
29
-
30
- - type: dropdown
36
+ - type: checkboxes
31
37
  attributes:
32
38
  label: '🌐 Browser'
33
39
  options:
34
- - Chrome
35
- - Edge
36
- - Safari
37
- - Firefox
38
- - Other
40
+ - label: 'Chrome'
41
+ - label: 'Edge'
42
+ - label: 'Safari'
43
+ - label: 'Firefox'
44
+ - label: 'Other'
39
45
  validations:
40
46
  required: true
41
47
  - type: textarea
42
48
  attributes:
43
49
  label: '🐛 Bug Description'
44
- description: A clear and concise description of the bug.
50
+ description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
45
51
  validations:
46
52
  required: true
47
- - type: textarea
48
- attributes:
49
- label: '🚦 Expected Behavior'
50
- description: A clear and concise description of what you expected to happen.
51
53
  - type: textarea
52
54
  attributes:
53
55
  label: '📷 Recurrence Steps'
54
56
  description: A clear and concise description of how to recurrence.
57
+ - type: textarea
58
+ attributes:
59
+ label: '🚦 Expected Behavior'
60
+ description: A clear and concise description of what you expected to happen.
55
61
  - type: textarea
56
62
  attributes:
57
63
  label: '📝 Additional Information'
@@ -3,56 +3,61 @@ description: '反馈一个问题缺陷'
3
3
  title: '[Bug] '
4
4
  labels: ['🐛 Bug']
5
5
  body:
6
- - type: dropdown
6
+ - type: checkboxes
7
7
  attributes:
8
- label: '💻 系统环境'
8
+ label: '📦 部署环境'
9
9
  options:
10
- - Windows
11
- - macOS
12
- - Ubuntu
13
- - Other Linux
14
- - iOS
15
- - Android
16
- - Other
10
+ - label: 'Official'
11
+ - label: 'Official Preview'
12
+ - label: 'Vercel / Zeabur / Sealos'
13
+ - label: 'Docker'
14
+ - label: 'Other'
15
+ validations:
16
+ required: true
17
+ - type: input
18
+ attributes:
19
+ label: '📌 软件版本'
17
20
  validations:
18
21
  required: true
19
22
 
20
- - type: dropdown
23
+ - type: checkboxes
21
24
  attributes:
22
- label: '📦 部署环境'
25
+ label: '💻 系统环境'
23
26
  options:
24
- - Official Preview
25
- - Vercel / Zeabur / Sealos
26
- - Docker
27
- - Other
27
+ - label: 'Windows'
28
+ - label: 'macOS'
29
+ - label: 'Ubuntu'
30
+ - label: 'Other Linux'
31
+ - label: 'iOS'
32
+ - label: 'Android'
33
+ - label: 'Other'
28
34
  validations:
29
35
  required: true
30
-
31
- - type: dropdown
36
+ - type: checkboxes
32
37
  attributes:
33
38
  label: '🌐 浏览器'
34
39
  options:
35
- - Chrome
36
- - Edge
37
- - Safari
38
- - Firefox
39
- - Other
40
+ - label: 'Chrome'
41
+ - label: 'Edge'
42
+ - label: 'Safari'
43
+ - label: 'Firefox'
44
+ - label: 'Other'
40
45
  validations:
41
46
  required: true
42
47
  - type: textarea
43
48
  attributes:
44
49
  label: '🐛 问题描述'
45
- description: 请提供一个清晰且简洁的问题描述。
50
+ description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
46
51
  validations:
47
52
  required: true
48
- - type: textarea
49
- attributes:
50
- label: '🚦 期望结果'
51
- description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
52
53
  - type: textarea
53
54
  attributes:
54
55
  label: '📷 复现步骤'
55
56
  description: 请提供一个清晰且简洁的描述,说明如何复现问题。
57
+ - type: textarea
58
+ attributes:
59
+ label: '🚦 期望结果'
60
+ description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
56
61
  - type: textarea
57
62
  attributes:
58
63
  label: '📝 补充信息'
package/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.161.14](https://github.com/lobehub/lobe-chat/compare/v0.161.13...v0.161.14)
6
+
7
+ <sup>Released on **2024-05-24**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor the global app status and fix PWA installer.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Refactor the global app status and fix PWA installer, closes [#2637](https://github.com/lobehub/lobe-chat/issues/2637) ([1f70305](https://github.com/lobehub/lobe-chat/commit/1f70305))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 0.161.13](https://github.com/lobehub/lobe-chat/compare/v0.161.12...v0.161.13)
31
+
32
+ <sup>Released on **2024-05-24**</sup>
33
+
34
+ <br/>
35
+
36
+ <details>
37
+ <summary><kbd>Improvements and Fixes</kbd></summary>
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 0.161.12](https://github.com/lobehub/lobe-chat/compare/v0.161.11...v0.161.12)
6
48
 
7
49
  <sup>Released on **2024-05-23**</sup>
@@ -15,6 +15,13 @@ When deploying LobeChat, a rich set of environment variables related to model se
15
15
 
16
16
  ## OpenAI
17
17
 
18
+ ### `ENABLED_OPENAI`
19
+
20
+ - Type:Optional
21
+ - Description:Enables OpenAI as a model provider by default, turns off the OpenAI service when set to `0`
22
+ - Default:`1`
23
+ - Example:`0`
24
+
18
25
  ### `OPENAI_API_KEY`
19
26
 
20
27
  - Type: Required
@@ -189,6 +196,13 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
189
196
 
190
197
  ## Ollama
191
198
 
199
+ ### `ENABLED_OLLAMA`
200
+
201
+ - Type:Optional
202
+ - Description:Enables Ollama as a model provider by default, turns off the Ollama service when set to `0`
203
+ - Default:`1`
204
+ - Example:`0`
205
+
192
206
  ### `OLLAMA_PROXY_URL`
193
207
 
194
208
  - Type: Required
@@ -15,6 +15,13 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
15
15
 
16
16
  ## OpenAI
17
17
 
18
+ ### `ENABLED_OPENAI`
19
+
20
+ - 类型:可选
21
+ - 描述:默认启用 OpenAI 作为模型供应商,当设为 0 时关闭 OpenAI 服务
22
+ - 默认值:`1`
23
+ - 示例:`0`
24
+
18
25
  ### `OPENAI_API_KEY`
19
26
 
20
27
  - 类型:必选
@@ -187,6 +194,13 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
187
194
 
188
195
  ## Ollama
189
196
 
197
+ ### `ENABLED_OLLAMA`
198
+
199
+ - 类型:可选
200
+ - 描述:默认启用 Ollama 作为模型供应商,当设为 0 时关闭 Ollama 服务
201
+ - 默认值:`1`
202
+ - 示例:`0`
203
+
190
204
  ### `OLLAMA_PROXY_URL`
191
205
 
192
206
  - 类型:必选
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.161.12",
3
+ "version": "0.161.14",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -95,10 +95,10 @@
95
95
  "@google/generative-ai": "^0.11.3",
96
96
  "@icons-pack/react-simple-icons": "^9.5.0",
97
97
  "@khmyznikov/pwa-install": "^0.3.9",
98
- "@lobehub/chat-plugin-sdk": "latest",
99
- "@lobehub/chat-plugins-gateway": "latest",
98
+ "@lobehub/chat-plugin-sdk": "^1.32.3",
99
+ "@lobehub/chat-plugins-gateway": "^1.9.0",
100
100
  "@lobehub/icons": "^1.22.0",
101
- "@lobehub/tts": "latest",
101
+ "@lobehub/tts": "^1.24.1",
102
102
  "@lobehub/ui": "^1.139.0",
103
103
  "@microsoft/fetch-event-source": "^2.0.1",
104
104
  "@next/third-parties": "^14.2.3",
@@ -158,6 +158,7 @@
158
158
  "remark": "^14.0.3",
159
159
  "remark-gfm": "^3.0.1",
160
160
  "remark-html": "^15.0.2",
161
+ "resolve-accept-language": "^3.1.4",
161
162
  "rtl-detect": "^1.1.2",
162
163
  "semver": "^7.6.2",
163
164
  "sharp": "^0.33.4",
@@ -10,6 +10,7 @@ import {
10
10
  HEADER_HEIGHT,
11
11
  } from '@/const/layoutTokens';
12
12
  import { useGlobalStore } from '@/store/global';
13
+ import { systemStatusSelectors } from '@/store/global/selectors';
13
14
 
14
15
  import Footer from './Footer';
15
16
  import Head from './Header';
@@ -19,8 +20,8 @@ const DesktopChatInput = memo(() => {
19
20
  const [expand, setExpand] = useState<boolean>(false);
20
21
 
21
22
  const [inputHeight, updatePreference] = useGlobalStore((s) => [
22
- s.preference.inputHeight,
23
- s.updatePreference,
23
+ systemStatusSelectors.inputHeight(s),
24
+ s.updateSystemStatus,
24
25
  ]);
25
26
 
26
27
  return (
@@ -15,6 +15,7 @@ import { useAgentStore } from '@/store/agent';
15
15
  import { agentSelectors } from '@/store/agent/selectors';
16
16
  import { useGlobalStore } from '@/store/global';
17
17
  import { ChatSettingsTabs } from '@/store/global/initialState';
18
+ import { systemStatusSelectors } from '@/store/global/selectors';
18
19
  import { useSessionStore } from '@/store/session';
19
20
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
20
21
 
@@ -35,7 +36,7 @@ const SystemRole = memo(() => {
35
36
  ]);
36
37
 
37
38
  const [showSystemRole, toggleSystemRole] = useGlobalStore((s) => [
38
- s.preference.showSystemRole,
39
+ systemStatusSelectors.showSystemRole(s),
39
40
  s.toggleSystemRole,
40
41
  ]);
41
42
 
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
9
9
  import { useGlobalStore } from '@/store/global';
10
+ import { systemStatusSelectors } from '@/store/global/selectors';
10
11
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
11
12
 
12
13
  import SettingButton from '../../../features/SettingButton';
@@ -16,7 +17,7 @@ const HeaderAction = memo(() => {
16
17
  const { t } = useTranslation('chat');
17
18
 
18
19
  const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
19
- s.preference.showChatSideBar,
20
+ systemStatusSelectors.showChatSideBar(s),
20
21
  s.toggleChatSideBar,
21
22
  ]);
22
23
 
@@ -7,13 +7,14 @@ import { Suspense, memo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
- import { useInitAgentConfig } from '@/app/(main)/chat/(workspace)/_layout/useInitAgentConfig';
11
10
  import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
12
11
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
13
12
  import { useGlobalStore } from '@/store/global';
13
+ import { systemStatusSelectors } from '@/store/global/selectors';
14
14
  import { useSessionStore } from '@/store/session';
15
15
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
16
16
 
17
+ import { useInitAgentConfig } from '../../useInitAgentConfig';
17
18
  import Tags from './Tags';
18
19
 
19
20
  const Main = memo(() => {
@@ -34,7 +35,8 @@ const Main = memo(() => {
34
35
 
35
36
  const displayTitle = isInbox ? t('inbox.title') : title;
36
37
  const displayDesc = isInbox ? t('inbox.desc') : description;
37
- const showSessionPanel = useGlobalStore((s) => s.preference.showSessionPanel);
38
+ const showSessionPanel = useGlobalStore(systemStatusSelectors.showSessionPanel);
39
+ const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
38
40
 
39
41
  return !init ? (
40
42
  <Flexbox horizontal>
@@ -52,10 +54,9 @@ const Main = memo(() => {
52
54
  aria-label={t('agentsAndConversations')}
53
55
  icon={showSessionPanel ? PanelLeftClose : PanelLeftOpen}
54
56
  onClick={() => {
55
- const currentShowSessionPanel = useGlobalStore.getState().preference.showSessionPanel;
56
- useGlobalStore.getState().updatePreference({
57
- sessionsWidth: currentShowSessionPanel ? 0 : 320,
58
- showSessionPanel: !currentShowSessionPanel,
57
+ updateSystemStatus({
58
+ sessionsWidth: showSessionPanel ? 0 : 320,
59
+ showSessionPanel: !showSessionPanel,
59
60
  });
60
61
  }}
61
62
  size={DESKTOP_HEADER_ICON_SIZE}
@@ -8,6 +8,7 @@ import { PropsWithChildren, memo, useEffect, useState } from 'react';
8
8
  import SafeSpacing from '@/components/SafeSpacing';
9
9
  import { CHAT_SIDEBAR_WIDTH } from '@/const/layoutTokens';
10
10
  import { useGlobalStore } from '@/store/global';
11
+ import { systemStatusSelectors } from '@/store/global/selectors';
11
12
 
12
13
  const useStyles = createStyles(({ css, token }) => ({
13
14
  content: css`
@@ -28,10 +29,10 @@ const TopicPanel = memo(({ children }: PropsWithChildren) => {
28
29
  const { styles } = useStyles();
29
30
  const { md = true, lg = true } = useResponsive();
30
31
  const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
31
- s.preference.showChatSideBar,
32
+ systemStatusSelectors.showChatSideBar(s),
32
33
  s.toggleChatSideBar,
33
- s.isPreferenceInit,
34
34
  ]);
35
+
35
36
  const [cacheExpand, setCacheExpand] = useState<boolean>(Boolean(showAgentSettings));
36
37
 
37
38
  const handleExpand = (expand: boolean) => {
@@ -5,12 +5,13 @@ import { PropsWithChildren, memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { useGlobalStore } from '@/store/global';
8
+ import { systemStatusSelectors } from '@/store/global/selectors';
8
9
 
9
10
  import { useWorkspaceModal } from '../../features/useWorkspaceModal';
10
11
 
11
12
  const Topics = memo(({ children }: PropsWithChildren) => {
12
13
  const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
13
- s.preference.mobileShowTopic,
14
+ systemStatusSelectors.mobileShowTopic(s),
14
15
  s.toggleMobileTopic,
15
16
  ]);
16
17
  const [open, setOpen] = useWorkspaceModal(showAgentSettings, toggleConfig);
@@ -4,7 +4,7 @@ import { memo, useMemo, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useGlobalStore } from '@/store/global';
7
- import { preferenceSelectors } from '@/store/global/selectors';
7
+ import { systemStatusSelectors } from '@/store/global/selectors';
8
8
  import { useSessionStore } from '@/store/session';
9
9
  import { sessionSelectors } from '@/store/session/selectors';
10
10
  import { SessionDefaultGroup } from '@/types/session';
@@ -30,9 +30,9 @@ const DefaultMode = memo(() => {
30
30
  const customSessionGroups = useSessionStore(sessionSelectors.customSessionGroups, isEqual);
31
31
  const pinnedSessions = useSessionStore(sessionSelectors.pinnedSessions, isEqual);
32
32
 
33
- const [sessionGroupKeys, updatePreference] = useGlobalStore((s) => [
34
- preferenceSelectors.sessionGroupKeys(s),
35
- s.updatePreference,
33
+ const [sessionGroupKeys, updateSystemStatus] = useGlobalStore((s) => [
34
+ systemStatusSelectors.sessionGroupKeys(s),
35
+ s.updateSystemStatus,
36
36
  ]);
37
37
 
38
38
  const items = useMemo(
@@ -80,7 +80,7 @@ const DefaultMode = memo(() => {
80
80
  onChange={(keys) => {
81
81
  const expandSessionGroupKeys = typeof keys === 'string' ? [keys] : keys;
82
82
 
83
- updatePreference({ expandSessionGroupKeys });
83
+ updateSystemStatus({ expandSessionGroupKeys });
84
84
  }}
85
85
  />
86
86
  {activeGroupId && (
@@ -7,6 +7,7 @@ import { PropsWithChildren, memo, useEffect, useState } from 'react';
7
7
 
8
8
  import { FOLDER_WIDTH } from '@/const/layoutTokens';
9
9
  import { useGlobalStore } from '@/store/global';
10
+ import { systemStatusSelectors } from '@/store/global/selectors';
10
11
 
11
12
  export const useStyles = createStyles(({ css, token }) => ({
12
13
  panel: css`
@@ -21,10 +22,11 @@ const SessionPanel = memo<PropsWithChildren>(({ children }) => {
21
22
 
22
23
  const { styles } = useStyles();
23
24
  const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [
24
- s.preference.sessionsWidth,
25
- s.preference.showSessionPanel,
26
- s.updatePreference,
25
+ systemStatusSelectors.sessionWidth(s),
26
+ systemStatusSelectors.showSessionPanel(s),
27
+ s.updateSystemStatus,
27
28
  ]);
29
+
28
30
  const [cacheExpand, setCacheExpand] = useState<boolean>(Boolean(sessionExpandable));
29
31
  const [tmpWidth, setWidth] = useState(sessionsWidth);
30
32
  if (tmpWidth !== sessionsWidth) setWidth(sessionsWidth);
@@ -1,11 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  import dynamic from 'next/dynamic';
4
- import { memo } from 'react';
4
+ import { memo, useEffect, useLayoutEffect } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { PWA_INSTALL_ID } from '@/const/layoutTokens';
8
+ import { usePWAInstall } from '@/hooks/usePWAInstall';
8
9
  import { usePlatform } from '@/hooks/usePlatform';
10
+ import { useGlobalStore } from '@/store/global';
11
+ import { systemStatusSelectors } from '@/store/global/selectors';
12
+ import { useUserStore } from '@/store/user';
9
13
 
10
14
  // @ts-ignore
11
15
  const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), {
@@ -15,6 +19,52 @@ const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.
15
19
  const PWAInstall = memo(() => {
16
20
  const { t } = useTranslation('metadata');
17
21
  const { isPWA } = usePlatform();
22
+
23
+ const { install, canInstall } = usePWAInstall();
24
+
25
+ const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide);
26
+ const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [
27
+ systemStatusSelectors.hidePWAInstaller(s),
28
+ s.updateSystemStatus,
29
+ ]);
30
+
31
+ // we need to make the pwa installer hidden by default
32
+ useLayoutEffect(() => {
33
+ sessionStorage.setItem('pwa-hide-install', 'true');
34
+ }, []);
35
+
36
+ const pwaInstall =
37
+ // eslint-disable-next-line unicorn/prefer-query-selector
38
+ typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID);
39
+
40
+ // add an event listener to control the user close installer action
41
+ useEffect(() => {
42
+ if (!pwaInstall) return;
43
+
44
+ const handler = (e: Event) => {
45
+ const event = e as CustomEvent;
46
+
47
+ // it means user hide installer
48
+ if (event.detail.message === 'dismissed') {
49
+ updateSystemStatus({ hidePWAInstaller: true });
50
+ }
51
+ };
52
+
53
+ pwaInstall.addEventListener('pwa-user-choice-result-event', handler);
54
+ return () => {
55
+ pwaInstall.removeEventListener('pwa-user-choice-result-event', handler);
56
+ };
57
+ }, [pwaInstall]);
58
+
59
+ // trigger the PWA guide on demand
60
+ useEffect(() => {
61
+ if (!canInstall || hidePWAInstaller) return;
62
+
63
+ if (isShowPWAGuide) {
64
+ install();
65
+ }
66
+ }, [canInstall, hidePWAInstaller, isShowPWAGuide]);
67
+
18
68
  if (isPWA) return null;
19
69
  return <PWA description={t('chat.description')} id={PWA_INSTALL_ID} />;
20
70
  });
@@ -24,11 +24,11 @@ const StoreInitialization = memo(() => {
24
24
 
25
25
  const { serverConfig } = useServerConfigStore();
26
26
 
27
- const useInitGlobalPreference = useGlobalStore((s) => s.useInitGlobalPreference);
27
+ const useInitSystemStatus = useGlobalStore((s) => s.useInitSystemStatus);
28
28
 
29
29
  const useFetchDefaultAgentConfig = useAgentStore((s) => s.useFetchDefaultAgentConfig);
30
30
  // init the system preference
31
- useInitGlobalPreference();
31
+ useInitSystemStatus();
32
32
  useFetchDefaultAgentConfig();
33
33
 
34
34
  useInitUserState(isLogin, serverConfig, {
@@ -1,6 +1,7 @@
1
1
  import dynamic from 'next/dynamic';
2
- import { cookies } from 'next/headers';
3
- import { FC, ReactNode } from 'react';
2
+ import { cookies, headers } from 'next/headers';
3
+ import { FC, PropsWithChildren } from 'react';
4
+ import { resolveAcceptLanguage } from 'resolve-accept-language';
4
5
 
5
6
  import { getDebugConfig } from '@/config/debug';
6
7
  import { getServerFeatureFlagsValue } from '@/config/featureFlags';
@@ -10,6 +11,7 @@ import {
10
11
  LOBE_THEME_NEUTRAL_COLOR,
11
12
  LOBE_THEME_PRIMARY_COLOR,
12
13
  } from '@/const/theme';
14
+ import { locales } from '@/locales/resources';
13
15
  import { getServerGlobalConfig } from '@/server/globalConfig';
14
16
  import { ServerConfigStoreProvider } from '@/store/serverConfig';
15
17
  import { getAntdLocale } from '@/utils/locale';
@@ -31,11 +33,27 @@ if (process.env.NODE_ENV === 'development') {
31
33
  }
32
34
  }
33
35
 
34
- interface GlobalLayoutProps {
35
- children: ReactNode;
36
- }
36
+ const parserFallbackLang = () => {
37
+ /**
38
+ * The arguments are as follows:
39
+ *
40
+ * 1) The HTTP accept-language header.
41
+ * 2) The available locales (they must contain the default locale).
42
+ * 3) The default locale.
43
+ */
44
+ let fallbackLang: string = resolveAcceptLanguage(
45
+ headers().get('accept-language') || '',
46
+ // Invalid locale identifier 'ar'. A valid locale should follow the BCP 47 'language-country' format.
47
+ locales.map((locale) => (locale === 'ar' ? 'ar-EG' : locale)),
48
+ 'en-US',
49
+ );
50
+ // if match the ar-EG then fallback to ar
51
+ if (fallbackLang === 'ar-EG') fallbackLang = 'ar';
52
+
53
+ return fallbackLang;
54
+ };
37
55
 
38
- const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
56
+ const GlobalLayout = async ({ children }: PropsWithChildren) => {
39
57
  // get default theme config to use with ssr
40
58
  const cookieStore = cookies();
41
59
  const appearance = cookieStore.get(LOBE_THEME_APPEARANCE);
@@ -44,7 +62,13 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
44
62
 
45
63
  // get default locale config to use with ssr
46
64
  const defaultLang = cookieStore.get(LOBE_LOCALE_COOKIE);
47
- const antdLocale = await getAntdLocale(defaultLang?.value);
65
+ const fallbackLang = parserFallbackLang();
66
+
67
+ // if it's a new user, there's no cookie
68
+ // So we need to use the fallback language parsed by accept-language
69
+ const userLocale = defaultLang?.value || fallbackLang;
70
+
71
+ const antdLocale = await getAntdLocale(userLocale);
48
72
 
49
73
  // get default feature flags to use with ssr
50
74
  const serverFeatureFlags = getServerFeatureFlagsValue();
@@ -52,7 +76,7 @@ const GlobalLayout = async ({ children }: GlobalLayoutProps) => {
52
76
  const isMobile = isMobileDevice();
53
77
  return (
54
78
  <StyleRegistry>
55
- <Locale antdLocale={antdLocale} defaultLang={defaultLang?.value}>
79
+ <Locale antdLocale={antdLocale} defaultLang={userLocale}>
56
80
  <AppTheme
57
81
  defaultAppearance={appearance?.value}
58
82
  defaultNeutralColor={neutralColor?.value as any}
@@ -34,11 +34,11 @@ describe('createPreferenceSlice', () => {
34
34
  const { result } = renderHook(() => useGlobalStore());
35
35
 
36
36
  act(() => {
37
- useGlobalStore.getState().updatePreference({ showChatSideBar: false });
37
+ useGlobalStore.getState().updateSystemStatus({ showChatSideBar: false });
38
38
  result.current.toggleChatSideBar();
39
39
  });
40
40
 
41
- expect(result.current.preference.showChatSideBar).toBe(true);
41
+ expect(result.current.status.showChatSideBar).toBe(true);
42
42
  });
43
43
  });
44
44
 
@@ -48,10 +48,11 @@ describe('createPreferenceSlice', () => {
48
48
  const groupId = 'group-id';
49
49
 
50
50
  act(() => {
51
+ useGlobalStore.setState({ isStatusInit: true });
51
52
  result.current.toggleExpandSessionGroup(groupId, true);
52
53
  });
53
54
 
54
- expect(result.current.preference.expandSessionGroupKeys).toContain(groupId);
55
+ expect(result.current.status.expandSessionGroupKeys).toContain(groupId);
55
56
  });
56
57
  });
57
58
 
@@ -60,10 +61,11 @@ describe('createPreferenceSlice', () => {
60
61
  const { result } = renderHook(() => useGlobalStore());
61
62
 
62
63
  act(() => {
64
+ useGlobalStore.setState({ isStatusInit: true });
63
65
  result.current.toggleMobileTopic();
64
66
  });
65
67
 
66
- expect(result.current.preference.mobileShowTopic).toBe(true);
68
+ expect(result.current.status.mobileShowTopic).toBe(true);
67
69
  });
68
70
  });
69
71
 
@@ -72,23 +74,24 @@ describe('createPreferenceSlice', () => {
72
74
  const { result } = renderHook(() => useGlobalStore());
73
75
 
74
76
  act(() => {
77
+ useGlobalStore.setState({ isStatusInit: true });
75
78
  result.current.toggleSystemRole(true);
76
79
  });
77
80
 
78
- expect(result.current.preference.showSystemRole).toBe(true);
81
+ expect(result.current.status.showSystemRole).toBe(true);
79
82
  });
80
83
  });
81
84
 
82
85
  describe('updatePreference', () => {
83
- it('should update preference', () => {
86
+ it('should update status', () => {
84
87
  const { result } = renderHook(() => useGlobalStore());
85
- const preference = { inputHeight: 200 };
88
+ const status = { inputHeight: 200 };
86
89
 
87
90
  act(() => {
88
- result.current.updatePreference(preference);
91
+ result.current.updateSystemStatus(status);
89
92
  });
90
93
 
91
- expect(result.current.preference.inputHeight).toEqual(200);
94
+ expect(result.current.status.inputHeight).toEqual(200);
92
95
  });
93
96
  });
94
97
 
@@ -144,13 +147,12 @@ describe('createPreferenceSlice', () => {
144
147
  });
145
148
 
146
149
  describe('useInitGlobalPreference', () => {
147
- it('should init global preference if there is empty object', async () => {
148
- vi.spyOn(
149
- useGlobalStore.getState().preferenceStorage,
150
- 'getFromLocalStorage',
151
- ).mockReturnValueOnce({} as any);
150
+ it('should init global status if there is empty object', async () => {
151
+ vi.spyOn(useGlobalStore.getState().statusStorage, 'getFromLocalStorage').mockReturnValueOnce(
152
+ {} as any,
153
+ );
152
154
 
153
- const { result } = renderHook(() => useGlobalStore().useInitGlobalPreference(), {
155
+ const { result } = renderHook(() => useGlobalStore().useInitSystemStatus(), {
154
156
  wrapper: withSWR,
155
157
  });
156
158
 
@@ -158,17 +160,16 @@ describe('createPreferenceSlice', () => {
158
160
  expect(result.current.data).toEqual({});
159
161
  });
160
162
 
161
- expect(useGlobalStore.getState().preference).toEqual(initialState.preference);
163
+ expect(useGlobalStore.getState().status).toEqual(initialState.status);
162
164
  });
163
165
 
164
166
  it('should update with data', async () => {
165
167
  const { result } = renderHook(() => useGlobalStore());
166
- vi.spyOn(
167
- useGlobalStore.getState().preferenceStorage,
168
- 'getFromLocalStorage',
169
- ).mockReturnValueOnce({ inputHeight: 300 } as any);
168
+ vi.spyOn(useGlobalStore.getState().statusStorage, 'getFromLocalStorage').mockReturnValueOnce({
169
+ inputHeight: 300,
170
+ } as any);
170
171
 
171
- const { result: hooks } = renderHook(() => result.current.useInitGlobalPreference(), {
172
+ const { result: hooks } = renderHook(() => result.current.useInitSystemStatus(), {
172
173
  wrapper: withSWR,
173
174
  });
174
175
 
@@ -176,7 +177,7 @@ describe('createPreferenceSlice', () => {
176
177
  expect(hooks.current.data).toEqual({ inputHeight: 300 });
177
178
  });
178
179
 
179
- expect(result.current.preference.inputHeight).toEqual(300);
180
+ expect(result.current.status.inputHeight).toEqual(300);
180
181
  });
181
182
  });
182
183
  });
@@ -13,9 +13,9 @@ import type { GlobalStore } from '@/store/global/index';
13
13
  import { merge } from '@/utils/merge';
14
14
  import { setNamespace } from '@/utils/storeDebug';
15
15
 
16
- import type { GlobalPreference } from './initialState';
16
+ import type { SystemStatus } from './initialState';
17
17
 
18
- const n = setNamespace('preference');
18
+ const n = setNamespace('g');
19
19
 
20
20
  /**
21
21
  * 设置操作
@@ -26,9 +26,9 @@ export interface GlobalStoreAction {
26
26
  toggleExpandSessionGroup: (id: string, expand: boolean) => void;
27
27
  toggleMobileTopic: (visible?: boolean) => void;
28
28
  toggleSystemRole: (visible?: boolean) => void;
29
- updatePreference: (preference: Partial<GlobalPreference>, action?: any) => void;
29
+ updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
30
30
  useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
31
- useInitGlobalPreference: () => SWRResponse;
31
+ useInitSystemStatus: () => SWRResponse;
32
32
  }
33
33
 
34
34
  export const globalActionSlice: StateCreator<
@@ -42,13 +42,13 @@ export const globalActionSlice: StateCreator<
42
42
  },
43
43
  toggleChatSideBar: (newValue) => {
44
44
  const showChatSideBar =
45
- typeof newValue === 'boolean' ? newValue : !get().preference.showChatSideBar;
45
+ typeof newValue === 'boolean' ? newValue : !get().status.showChatSideBar;
46
46
 
47
- get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue));
47
+ get().updateSystemStatus({ showChatSideBar }, n('toggleAgentPanel', newValue));
48
48
  },
49
49
  toggleExpandSessionGroup: (id, expand) => {
50
- const { preference } = get();
51
- const nextExpandSessionGroup = produce(preference.expandSessionGroupKeys, (draft: string[]) => {
50
+ const { status } = get();
51
+ const nextExpandSessionGroup = produce(status.expandSessionGroupKeys, (draft: string[]) => {
52
52
  if (expand) {
53
53
  if (draft.includes(id)) return;
54
54
  draft.push(id);
@@ -57,26 +57,29 @@ export const globalActionSlice: StateCreator<
57
57
  if (index !== -1) draft.splice(index, 1);
58
58
  }
59
59
  });
60
- get().updatePreference({ expandSessionGroupKeys: nextExpandSessionGroup });
60
+ get().updateSystemStatus({ expandSessionGroupKeys: nextExpandSessionGroup });
61
61
  },
62
62
  toggleMobileTopic: (newValue) => {
63
63
  const mobileShowTopic =
64
- typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
64
+ typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
65
65
 
66
- get().updatePreference({ mobileShowTopic }, n('toggleMobileTopic', newValue));
66
+ get().updateSystemStatus({ mobileShowTopic }, n('toggleMobileTopic', newValue));
67
67
  },
68
68
  toggleSystemRole: (newValue) => {
69
- const showSystemRole =
70
- typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
69
+ const showSystemRole = typeof newValue === 'boolean' ? newValue : !get().status.mobileShowTopic;
71
70
 
72
- get().updatePreference({ showSystemRole }, n('toggleMobileTopic', newValue));
71
+ get().updateSystemStatus({ showSystemRole }, n('toggleMobileTopic', newValue));
73
72
  },
74
- updatePreference: (preference, action) => {
75
- const nextPreference = merge(get().preference, preference);
73
+ updateSystemStatus: (status, action) => {
74
+ // Status cannot be modified when it is not initialized
75
+ if (!get().isStatusInit) return;
76
76
 
77
- set({ preference: nextPreference }, false, action || n('updatePreference'));
77
+ const nextStatus = merge(get().status, status);
78
+ if (isEqual(get().status, nextStatus)) return;
78
79
 
79
- get().preferenceStorage.saveToLocalStorage(nextPreference);
80
+ set({ status: nextStatus }, false, action || n('updateSystemStatus'));
81
+
82
+ get().statusStorage.saveToLocalStorage(nextStatus);
80
83
  },
81
84
 
82
85
  useCheckLatestVersion: (enabledCheck = true) =>
@@ -89,19 +92,15 @@ export const globalActionSlice: StateCreator<
89
92
  },
90
93
  }),
91
94
 
92
- useInitGlobalPreference: () =>
93
- useClientDataSWR<GlobalPreference>(
94
- 'initGlobalPreference',
95
- () => get().preferenceStorage.getFromLocalStorage(),
95
+ useInitSystemStatus: () =>
96
+ useClientDataSWR<SystemStatus>(
97
+ 'initSystemStatus',
98
+ () => get().statusStorage.getFromLocalStorage(),
96
99
  {
97
- onSuccess: (preference) => {
98
- const nextPreference = merge(get().preference, preference);
99
-
100
- set({ isPreferenceInit: true });
101
-
102
- if (isEqual(get().preference, nextPreference)) return;
100
+ onSuccess: (status) => {
101
+ set({ isStatusInit: true }, false, 'setStatusInit');
103
102
 
104
- set({ preference: nextPreference }, false, n('initPreference'));
103
+ get().updateSystemStatus(status, 'initSystemStatus');
105
104
  },
106
105
  },
107
106
  ),
@@ -29,9 +29,10 @@ export enum SettingsTabs {
29
29
  TTS = 'tts',
30
30
  }
31
31
 
32
- export interface GlobalPreference {
32
+ export interface SystemStatus {
33
33
  // which sessionGroup should expand
34
34
  expandSessionGroupKeys: string[];
35
+ hidePWAInstaller?: boolean;
35
36
  inputHeight: number;
36
37
  mobileShowTopic?: boolean;
37
38
  sessionsWidth: number;
@@ -40,37 +41,32 @@ export interface GlobalPreference {
40
41
  showSystemRole?: boolean;
41
42
  }
42
43
 
43
- export interface GlobalPreferenceState {
44
- /**
45
- * the user preference, which only store in local storage
46
- */
47
- preference: GlobalPreference;
48
- preferenceStorage: AsyncLocalStorage<GlobalPreference>;
49
- }
50
-
51
- export interface GlobalCommonState {
44
+ export interface GlobalState {
52
45
  hasNewVersion?: boolean;
53
46
  isMobile?: boolean;
54
- isPreferenceInit?: boolean;
47
+ isStatusInit?: boolean;
55
48
  latestVersion?: string;
56
49
  router?: AppRouterInstance;
57
50
  sidebarKey: SidebarTabKey;
51
+ status: SystemStatus;
52
+ statusStorage: AsyncLocalStorage<SystemStatus>;
58
53
  }
59
54
 
60
- export type GlobalState = GlobalCommonState & GlobalPreferenceState;
55
+ export const INITIAL_STATUS = {
56
+ expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
57
+ hidePWAInstaller: false,
58
+ inputHeight: 200,
59
+ mobileShowTopic: false,
60
+ sessionsWidth: 320,
61
+ showChatSideBar: true,
62
+ showSessionPanel: true,
63
+ showSystemRole: false,
64
+ } satisfies SystemStatus;
61
65
 
62
66
  export const initialState: GlobalState = {
63
67
  isMobile: false,
64
- isPreferenceInit: false,
65
- preference: {
66
- expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
67
- inputHeight: 200,
68
- mobileShowTopic: false,
69
- sessionsWidth: 320,
70
- showChatSideBar: true,
71
- showSessionPanel: true,
72
- showSystemRole: false,
73
- },
74
- preferenceStorage: new AsyncLocalStorage('LOBE_GLOBAL_PREFERENCE'),
68
+ isStatusInit: false,
75
69
  sidebarKey: SidebarTabKey.Chat,
70
+ status: INITIAL_STATUS,
71
+ statusStorage: new AsyncLocalStorage('LOBE_SYSTEM_STATUS'),
76
72
  };
@@ -1,9 +1,26 @@
1
1
  import { GlobalStore } from '@/store/global';
2
- import { SessionDefaultGroup } from '@/types/session';
2
+
3
+ import { INITIAL_STATUS } from './initialState';
3
4
 
4
5
  const sessionGroupKeys = (s: GlobalStore): string[] =>
5
- s.preference.expandSessionGroupKeys || [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default];
6
+ s.status.expandSessionGroupKeys || INITIAL_STATUS.expandSessionGroupKeys;
7
+
8
+ const showSystemRole = (s: GlobalStore) => s.status.showSystemRole;
9
+ const mobileShowTopic = (s: GlobalStore) => s.status.mobileShowTopic;
10
+ const showChatSideBar = (s: GlobalStore) => s.status.showChatSideBar;
11
+ const showSessionPanel = (s: GlobalStore) => s.status.showSessionPanel;
12
+ const hidePWAInstaller = (s: GlobalStore) => s.status.hidePWAInstaller;
13
+
14
+ const sessionWidth = (s: GlobalStore) => s.status.sessionsWidth;
15
+ const inputHeight = (s: GlobalStore) => s.status.inputHeight;
6
16
 
7
- export const preferenceSelectors = {
17
+ export const systemStatusSelectors = {
18
+ hidePWAInstaller,
19
+ inputHeight,
20
+ mobileShowTopic,
8
21
  sessionGroupKeys,
22
+ sessionWidth,
23
+ showChatSideBar,
24
+ showSessionPanel,
25
+ showSystemRole,
9
26
  };
@@ -2,7 +2,7 @@ const PREV_KEY = 'LOBE_GLOBAL';
2
2
 
3
3
  // LOBE_PREFERENCE for userStore
4
4
  // LOBE_GLOBAL_PREFERENCE for globalStore
5
- type StorageKey = 'LOBE_PREFERENCE' | 'LOBE_GLOBAL_PREFERENCE';
5
+ type StorageKey = 'LOBE_PREFERENCE' | 'LOBE_SYSTEM_STATUS';
6
6
 
7
7
  export class AsyncLocalStorage<State> {
8
8
  private storageKey: StorageKey;