@lobehub/chat 0.40.7 → 0.41.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.
package/.eslintrc.js CHANGED
@@ -7,5 +7,7 @@ config.rules['unicorn/prefer-type-error'] = 0;
7
7
  config.rules['unicorn/prefer-logical-operator-over-ternary'] = 0;
8
8
  config.rules['unicorn/no-null'] = 0;
9
9
  config.rules['unicorn/no-typeof-undefined'] = 0;
10
+ config.rules['unicorn/explicit-length-check'] = 0;
11
+ config.rules['unicorn/prefer-code-point'] = 0;
10
12
 
11
13
  module.exports = config;
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.41.1](https://github.com/lobehub/lobe-chat/compare/v0.41.0...v0.41.1)
6
+
7
+ <sup>Released on **2023-08-10**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: 重构 settings store 代码写法.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: 重构 settings store 代码写法 ([4b6f917](https://github.com/lobehub/lobe-chat/commit/4b6f917))
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.41.0](https://github.com/lobehub/lobe-chat/compare/v0.40.7...v0.41.0)
31
+
32
+ <sup>Released on **2023-08-10**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: 支持持久化隐藏 Topic 功能.
37
+
38
+ #### 💄 Styles
39
+
40
+ - **misc**: 优化第一次水合逻辑.
41
+
42
+ <br/>
43
+
44
+ <details>
45
+ <summary><kbd>Improvements and Fixes</kbd></summary>
46
+
47
+ #### What's improved
48
+
49
+ - **misc**: 支持持久化隐藏 Topic 功能 ([9ea2778](https://github.com/lobehub/lobe-chat/commit/9ea2778))
50
+
51
+ #### Styles
52
+
53
+ - **misc**: 优化第一次水合逻辑 ([fefae61](https://github.com/lobehub/lobe-chat/commit/fefae61))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ### [Version 0.40.7](https://github.com/lobehub/lobe-chat/compare/v0.40.6...v0.40.7)
6
64
 
7
65
  <sup>Released on **2023-08-10**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.40.7",
3
+ "version": "0.41.1",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -93,6 +93,7 @@
93
93
  "serpapi": "^2",
94
94
  "swr": "^2",
95
95
  "ts-md5": "^1",
96
+ "use-merge-value": "^1",
96
97
  "uuid": "^9",
97
98
  "zustand": "^4.4",
98
99
  "zustand-utils": "^1"
@@ -1,34 +1,60 @@
1
1
  import { ActionIcon, DivProps } from '@lobehub/ui';
2
2
  import { X } from 'lucide-react';
3
- import { memo, useState } from 'react';
3
+ import Image from 'next/image';
4
+ import { memo } from 'react';
5
+ import useMergeState from 'use-merge-value';
4
6
 
5
7
  import { useStyles } from './style';
6
8
 
7
9
  interface EmptyProps extends DivProps {
10
+ alt: string;
8
11
  cover: string;
12
+ defaultVisible?: boolean;
9
13
  desc: string;
14
+ height?: number;
15
+ onVisibleChange?: (visible: boolean) => void;
10
16
  title: string;
17
+ visible?: boolean;
18
+ width?: number;
11
19
  }
12
20
 
13
- const Empty = memo<EmptyProps>(({ cover, title, desc, ...props }) => {
14
- const [visiable, setVisiable] = useState(true);
15
- const { styles } = useStyles();
16
- if (!visiable) return null;
17
- return (
18
- <div className={styles.container} {...props}>
19
- <ActionIcon
20
- className={styles.close}
21
- icon={X}
22
- onClick={() => setVisiable(false)}
23
- size={{ blockSize: 24, fontSize: 16 }}
24
- />
25
- {cover && <img alt="empty" src={cover} width="100%" />}
26
- <div className={styles.content}>
27
- {title && <h3>{title}</h3>}
28
- {desc && <p className={styles.desc}>{desc}</p>}
21
+ const Empty = memo<EmptyProps>(
22
+ ({
23
+ cover,
24
+ visible,
25
+ defaultVisible,
26
+ onVisibleChange,
27
+ alt,
28
+ title,
29
+ desc,
30
+ width,
31
+ height,
32
+ ...props
33
+ }) => {
34
+ const [value, setValue] = useMergeState(true, {
35
+ defaultValue: defaultVisible,
36
+ onChange: onVisibleChange,
37
+ value: visible,
38
+ });
39
+
40
+ const { styles } = useStyles();
41
+ if (!value) return null;
42
+ return (
43
+ <div className={styles.container} {...props}>
44
+ <ActionIcon
45
+ className={styles.close}
46
+ icon={X}
47
+ onClick={() => setValue(false)}
48
+ size={{ blockSize: 24, fontSize: 16 }}
49
+ />
50
+ {cover && <Image alt={alt} height={height} src={cover} width={width} />}
51
+ <div className={styles.content}>
52
+ {title && <h3>{title}</h3>}
53
+ {desc && <p className={styles.desc}>{desc}</p>}
54
+ </div>
29
55
  </div>
30
- </div>
31
- );
32
- });
56
+ );
57
+ },
58
+ );
33
59
 
34
60
  export default Empty;
@@ -5,12 +5,7 @@ import { useTranslation } from 'react-i18next';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
  import { shallow } from 'zustand/shallow';
7
7
 
8
- import {
9
- agentSelectors,
10
- chatSelectors,
11
- useSessionHydrated,
12
- useSessionStore,
13
- } from '@/store/session';
8
+ import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
14
9
  import { ChatMessage } from '@/types/chatMessage';
15
10
  import { ErrorType } from '@/types/fetch';
16
11
  import { isFunctionMessage } from '@/utils/message';
@@ -34,9 +29,8 @@ const renderErrorMessage: RenderErrorMessage = (error, message) => {
34
29
 
35
30
  const List = () => {
36
31
  const { t } = useTranslation('common');
37
- const hydrated = useSessionHydrated();
32
+
38
33
  const data = useSessionStore(chatSelectors.currentChats, isEqual);
39
- console.log(hydrated, data);
40
34
  const [displayMode, chatLoadingId, deleteMessage, resendMessage, dispatchMessage] =
41
35
  useSessionStore(
42
36
  (s) => [
@@ -1,26 +1,41 @@
1
1
  import { useThemeMode } from 'antd-style';
2
+ import isEqual from 'fast-deep-equal';
2
3
  import { useTranslation } from 'react-i18next';
3
4
  import { Flexbox } from 'react-layout-kit';
4
5
  import { shallow } from 'zustand/shallow';
5
6
 
6
7
  import Empty from '@/components/Empty';
7
8
  import { topicSelectors, useSessionStore } from '@/store/session';
9
+ import { useSettings } from '@/store/settings';
8
10
 
9
11
  import TopicItem from './TopicItem';
10
12
 
11
13
  export const Topic = () => {
12
- const topics = useSessionStore(topicSelectors.currentTopics);
14
+ const topics = useSessionStore(topicSelectors.currentTopics, isEqual);
13
15
  const { isDarkMode } = useThemeMode();
14
16
  const [activeTopicId] = useSessionStore((s) => [s.activeTopicId], shallow);
15
17
  const { t } = useTranslation('empty');
18
+
19
+ const [visible, updateGuideState] = useSettings(
20
+ (s) => [s.guide?.topic, s.updateGuideState],
21
+ shallow,
22
+ );
23
+
16
24
  return (
17
25
  <Flexbox gap={2}>
18
26
  {topics?.length === 0 && (
19
27
  <Empty
28
+ alt={t('topic.desc')}
20
29
  cover={`/images/empty_topic_${isDarkMode ? 'dark' : 'light'}.webp`}
21
30
  desc={t('topic.desc')}
31
+ height={120}
32
+ onVisibleChange={(visible) => {
33
+ updateGuideState({ topic: visible });
34
+ }}
22
35
  style={{ marginBottom: 6 }}
23
36
  title={t('topic.title')}
37
+ visible={visible}
38
+ width={200}
24
39
  />
25
40
  )}
26
41
 
@@ -3,12 +3,15 @@ import { useEffect, useState } from 'react';
3
3
  import { useSessionStore } from '@/store/session';
4
4
 
5
5
  export const useSessionHydrated = () => {
6
- const [isInit, setInit] = useState(false);
6
+ // 根据 sessions 是否有值来判断是否已经初始化
7
+ const hasInited = !!Object.values(useSessionStore.getState().sessions).length;
8
+
9
+ const [isInit, setInit] = useState(hasInited);
7
10
 
8
11
  useEffect(() => {
9
12
  const hasRehydrated = useSessionStore.persist.hasHydrated();
10
13
 
11
- if (hasRehydrated) {
14
+ if (hasRehydrated && !isInit) {
12
15
  setInit(true);
13
16
  }
14
17
  }, []);
@@ -10,6 +10,8 @@ import { AgentAction, createAgentSlice } from './slices/agentConfig';
10
10
  import { ChatAction, createChatSlice } from './slices/chat';
11
11
  import { SessionAction, createSessionSlice } from './slices/session';
12
12
 
13
+ // =============== 聚合 createStoreFn ============ //
14
+
13
15
  export type SessionStore = SessionAction & AgentAction & ChatAction & SessionStoreState;
14
16
 
15
17
  const createStore: StateCreator<SessionStore, [['zustand/devtools', never]]> = (...parameters) => ({
@@ -19,6 +21,8 @@ const createStore: StateCreator<SessionStore, [['zustand/devtools', never]]> = (
19
21
  ...createChatSlice(...parameters),
20
22
  });
21
23
 
24
+ // =============== persist 本地缓存中间件配置 ============ //
25
+
22
26
  type SessionPersist = Pick<SessionStore, 'sessions'>;
23
27
 
24
28
  const LOBE_CHAT = 'LOBE_CHAT';
@@ -36,6 +40,8 @@ const persistOptions: PersistOptions<SessionStore, SessionPersist> = {
36
40
  // version: Migration.targetVersion,
37
41
  };
38
42
 
43
+ // =============== 实装 useStore ============ //
44
+
39
45
  export const useSessionStore = createWithEqualityFn<SessionStore>()(
40
46
  persist(
41
47
  devtools(createStore, {
@@ -1,27 +1,3 @@
1
- import equal from 'fast-deep-equal';
2
- import { type PersistOptions, devtools, persist } from 'zustand/middleware';
3
- import { createWithEqualityFn } from 'zustand/traditional';
4
-
5
- import { isDev } from '@/utils/env';
6
-
7
- import { type SettingsStore, createStore } from './store';
8
-
9
- export const LOBE_SETTINGS = 'LOBE_SETTINGS';
10
-
11
- const persistOptions: PersistOptions<SettingsStore> = {
12
- name: LOBE_SETTINGS,
13
- skipHydration: true,
14
- };
15
-
16
- export const useSettings = createWithEqualityFn<SettingsStore>()(
17
- persist(
18
- devtools(createStore, {
19
- name: LOBE_SETTINGS + (isDev ? '_DEV' : ''),
20
- }),
21
- persistOptions,
22
- ),
23
- equal,
24
- );
25
-
26
1
  export * from './selectors';
27
2
  export type { SettingsStore } from './store';
3
+ export { useSettings } from './store';
@@ -3,7 +3,13 @@ import type { GlobalSettings } from '@/types/settings';
3
3
 
4
4
  export type SidebarTabKey = 'chat' | 'market' | 'settings';
5
5
 
6
+ export interface Guide {
7
+ // Topic 引导
8
+ topic?: boolean;
9
+ }
10
+
6
11
  export interface AppSettingsState {
12
+ guide?: Guide;
7
13
  inputHeight: number;
8
14
  sessionExpandable?: boolean;
9
15
  sessionsWidth: number;
@@ -13,6 +19,7 @@ export interface AppSettingsState {
13
19
  }
14
20
 
15
21
  export const initialState: AppSettingsState = {
22
+ guide: {},
16
23
  inputHeight: 200,
17
24
  sessionExpandable: true,
18
25
  sessionsWidth: 320,
@@ -1,77 +1,37 @@
1
- import { ThemeMode } from 'antd-style';
2
1
  import { produce } from 'immer';
3
2
  import { merge } from 'lodash-es';
4
3
  import type { StateCreator } from 'zustand/vanilla';
5
4
 
6
- import { DEFAULT_AGENT, DEFAULT_BASE_SETTINGS, DEFAULT_SETTINGS } from '@/const/settings';
5
+ import { DEFAULT_AGENT } from '@/const/settings';
7
6
  import { MetaData } from '@/types/meta';
8
7
  import type { LobeAgentSession } from '@/types/session';
9
8
  import { LobeAgentConfig } from '@/types/session';
10
9
  import type { GlobalSettings } from '@/types/settings';
11
10
  import { setNamespace } from '@/utils/storeDebug';
12
11
 
13
- import type { SidebarTabKey } from './initialState';
14
- import { AppSettingsState } from './initialState';
15
- import type { SettingsStore } from './store';
12
+ import type { SettingsStore } from '../store';
16
13
 
17
14
  const t = setNamespace('settings');
18
15
 
19
16
  /**
20
17
  * 设置操作
21
18
  */
22
- export interface SettingsAction {
23
- importAppSettings: (settings: GlobalSettings) => void;
19
+ export interface AgentAction {
24
20
  resetAgentConfig: () => void;
25
21
  resetAgentMeta: () => void;
26
- resetBaseSettings: () => void;
27
22
  resetDefaultAgent: () => void;
28
- /**
29
- * 重置设置
30
- */
31
- resetSettings: () => void;
32
23
  setAgentConfig: (config: Partial<LobeAgentSession['config']>) => void;
33
24
  setAgentMeta: (meta: Partial<LobeAgentSession['meta']>) => void;
34
- /**
35
- * 设置全局设置
36
- * @param settings - 全局设置
37
- */
38
- setAppSettings: (settings: AppSettingsState) => void;
39
- /**
40
- * 设置部分配置设置
41
- * @param settings - 部分配置设置
42
- */
43
- setSettings: (settings: Partial<GlobalSettings>) => void;
44
- /**
45
- * 设置主题模式
46
- * @param themeMode - 主题模式
47
- */
48
- setThemeMode: (themeMode: ThemeMode) => void;
49
- /**
50
- * 切换侧边栏选项
51
- * @param key - 选中的侧边栏选项
52
- */
53
- switchSideBar: (key: SidebarTabKey) => void;
54
25
  toggleAgentPanel: (visible?: boolean) => void;
55
26
  toggleAgentPlugin: (pluginId: string) => void;
56
27
  }
57
28
 
58
- export const createSettings: StateCreator<
29
+ export const createAgentSlice: StateCreator<
59
30
  SettingsStore,
60
31
  [['zustand/devtools', never]],
61
32
  [],
62
- SettingsAction
33
+ AgentAction
63
34
  > = (set, get) => ({
64
- importAppSettings: (importAppSettings) => {
65
- const { setSettings } = get();
66
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
67
- const { OPENAI_API_KEY: _, password: __, ...settings } = importAppSettings;
68
-
69
- setSettings({
70
- ...settings,
71
- // 如果用户存在用户头像,那么不做导入
72
- avatar: !get().settings.avatar ? settings.avatar : get().settings.avatar,
73
- });
74
- },
75
35
  resetAgentConfig: () => {
76
36
  const settings = produce(get().settings, (draft: GlobalSettings) => {
77
37
  draft.defaultAgent.config = DEFAULT_AGENT.config;
@@ -84,20 +44,12 @@ export const createSettings: StateCreator<
84
44
  });
85
45
  set({ settings }, false, t('resetAgentMeta'));
86
46
  },
87
- resetBaseSettings: () => {
88
- const settings = get().settings;
89
-
90
- set({ settings: { ...settings, ...DEFAULT_BASE_SETTINGS } }, false, t('resetBaseSettings'));
91
- },
92
47
  resetDefaultAgent: () => {
93
48
  const settings = produce(get().settings, (draft: GlobalSettings) => {
94
49
  draft.defaultAgent = DEFAULT_AGENT;
95
50
  });
96
51
  set({ settings }, false, t('resetDefaultAgent'));
97
52
  },
98
- resetSettings: () => {
99
- set({ settings: DEFAULT_SETTINGS }, false, t('resetSettings'));
100
- },
101
53
  setAgentConfig: (config) => {
102
54
  const settings = produce(get().settings, (draft: GlobalSettings) => {
103
55
  const oldConfig = draft.defaultAgent.config as LobeAgentConfig;
@@ -114,19 +66,6 @@ export const createSettings: StateCreator<
114
66
 
115
67
  set({ settings }, false, t('setAgentMeta', meta));
116
68
  },
117
- setAppSettings: (settings) => {
118
- set({ ...settings }, false, t('setAppSettings', settings));
119
- },
120
- setSettings: (settings) => {
121
- const oldSetting = get().settings;
122
- set({ settings: merge(oldSetting, settings) }, false, t('setSettings', settings));
123
- },
124
- setThemeMode: (themeMode) => {
125
- get().setSettings({ themeMode });
126
- },
127
- switchSideBar: (key) => {
128
- set({ sidebarKey: key }, false, t('switchSideBar', key));
129
- },
130
69
  toggleAgentPanel: (newValue) => {
131
70
  const showAgentConfig = typeof newValue === 'boolean' ? newValue : !get().showAgentConfig;
132
71
 
@@ -0,0 +1,96 @@
1
+ import { ThemeMode } from 'antd-style';
2
+ import { produce } from 'immer';
3
+ import { merge } from 'lodash-es';
4
+ import type { StateCreator } from 'zustand/vanilla';
5
+
6
+ import { DEFAULT_AGENT, DEFAULT_BASE_SETTINGS, DEFAULT_SETTINGS } from '@/const/settings';
7
+ import type { GlobalSettings } from '@/types/settings';
8
+ import { setNamespace } from '@/utils/storeDebug';
9
+
10
+ import type { Guide, SidebarTabKey } from '../initialState';
11
+ import { AppSettingsState } from '../initialState';
12
+ import type { SettingsStore } from '../store';
13
+
14
+ const t = setNamespace('settings');
15
+
16
+ /**
17
+ * 设置操作
18
+ */
19
+ export interface CommonAction {
20
+ importAppSettings: (settings: GlobalSettings) => void;
21
+ resetBaseSettings: () => void;
22
+ /**
23
+ * 重置设置
24
+ */
25
+ resetSettings: () => void;
26
+ /**
27
+ * 设置全局设置
28
+ * @param settings - 全局设置
29
+ */
30
+ setAppSettings: (settings: AppSettingsState) => void;
31
+ /**
32
+ * 设置部分配置设置
33
+ * @param settings - 部分配置设置
34
+ */
35
+ setSettings: (settings: Partial<GlobalSettings>) => void;
36
+ /**
37
+ * 设置主题模式
38
+ * @param themeMode - 主题模式
39
+ */
40
+ setThemeMode: (themeMode: ThemeMode) => void;
41
+ /**
42
+ * 切换侧边栏选项
43
+ * @param key - 选中的侧边栏选项
44
+ */
45
+ switchSideBar: (key: SidebarTabKey) => void;
46
+ updateGuideState: (guide: Partial<Guide>) => void;
47
+ }
48
+
49
+ export const createCommonSlice: StateCreator<
50
+ SettingsStore,
51
+ [['zustand/devtools', never]],
52
+ [],
53
+ CommonAction
54
+ > = (set, get) => ({
55
+ importAppSettings: (importAppSettings) => {
56
+ const { setSettings } = get();
57
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
58
+ const { OPENAI_API_KEY: _, password: __, ...settings } = importAppSettings;
59
+
60
+ setSettings({
61
+ ...settings,
62
+ // 如果用户存在用户头像,那么不做导入
63
+ avatar: !get().settings.avatar ? settings.avatar : get().settings.avatar,
64
+ });
65
+ },
66
+ resetBaseSettings: () => {
67
+ const settings = get().settings;
68
+
69
+ set({ settings: { ...settings, ...DEFAULT_BASE_SETTINGS } }, false, t('resetBaseSettings'));
70
+ },
71
+ resetDefaultAgent: () => {
72
+ const settings = produce(get().settings, (draft: GlobalSettings) => {
73
+ draft.defaultAgent = DEFAULT_AGENT;
74
+ });
75
+ set({ settings }, false, t('resetDefaultAgent'));
76
+ },
77
+ resetSettings: () => {
78
+ set({ settings: DEFAULT_SETTINGS }, false, t('resetSettings'));
79
+ },
80
+ setAppSettings: (settings) => {
81
+ set({ ...settings }, false, t('setAppSettings', settings));
82
+ },
83
+ setSettings: (settings) => {
84
+ const oldSetting = get().settings;
85
+ set({ settings: merge(oldSetting, settings) }, false, t('setSettings', settings));
86
+ },
87
+ setThemeMode: (themeMode) => {
88
+ get().setSettings({ themeMode });
89
+ },
90
+ switchSideBar: (key) => {
91
+ set({ sidebarKey: key }, false, t('switchSideBar', key));
92
+ },
93
+ updateGuideState: (guide) => {
94
+ set({ guide: { ...get().guide, ...guide } });
95
+ },
96
+ });
@@ -1,13 +1,43 @@
1
+ import equal from 'fast-deep-equal';
2
+ import { PersistOptions, devtools, persist } from 'zustand/middleware';
3
+ import { createWithEqualityFn } from 'zustand/traditional';
1
4
  import { StateCreator } from 'zustand/vanilla';
2
5
 
3
- import { type SettingsAction, createSettings } from './action';
6
+ import { isDev } from '@/utils/env';
7
+
4
8
  import { type AppSettingsState, initialState } from './initialState';
9
+ import { type AgentAction, createAgentSlice } from './slices/agent';
10
+ import { type CommonAction, createCommonSlice } from './slices/common';
11
+
12
+ // =============== 聚合 createStoreFn ============ //
5
13
 
6
- export type SettingsStore = SettingsAction & AppSettingsState;
14
+ export type SettingsStore = CommonAction & AppSettingsState & AgentAction;
7
15
 
8
- export const createStore: StateCreator<SettingsStore, [['zustand/devtools', never]]> = (
16
+ const createStore: StateCreator<SettingsStore, [['zustand/devtools', never]]> = (
9
17
  ...parameters
10
18
  ) => ({
11
19
  ...initialState,
12
- ...createSettings(...parameters),
20
+ ...createCommonSlice(...parameters),
21
+ ...createAgentSlice(...parameters),
13
22
  });
23
+
24
+ // =============== persist 本地缓存中间件配置 ============ //
25
+
26
+ const LOBE_SETTINGS = 'LOBE_SETTINGS';
27
+
28
+ const persistOptions: PersistOptions<SettingsStore> = {
29
+ name: LOBE_SETTINGS,
30
+ skipHydration: true,
31
+ };
32
+
33
+ // =============== 实装 useStore ============ //
34
+
35
+ export const useSettings = createWithEqualityFn<SettingsStore>()(
36
+ persist(
37
+ devtools(createStore, {
38
+ name: LOBE_SETTINGS + (isDev ? '_DEV' : ''),
39
+ }),
40
+ persistOptions,
41
+ ),
42
+ equal,
43
+ );