@lobehub/chat 0.106.0 → 0.107.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 (214) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/en_US/common.json +19 -0
  3. package/locales/en_US/migration.json +45 -0
  4. package/locales/en_US/setting.json +7 -7
  5. package/locales/ja_JP/common.json +19 -0
  6. package/locales/ja_JP/migration.json +45 -0
  7. package/locales/ja_JP/setting.json +2 -2
  8. package/locales/ko_KR/common.json +19 -0
  9. package/locales/ko_KR/migration.json +45 -0
  10. package/locales/ko_KR/setting.json +2 -2
  11. package/locales/ru_RU/common.json +19 -0
  12. package/locales/ru_RU/migration.json +45 -0
  13. package/locales/ru_RU/setting.json +2 -2
  14. package/locales/zh_CN/common.json +19 -0
  15. package/locales/zh_CN/migration.json +45 -0
  16. package/locales/zh_CN/setting.json +1 -1
  17. package/locales/zh_TW/common.json +19 -0
  18. package/locales/zh_TW/migration.json +45 -0
  19. package/locales/zh_TW/setting.json +2 -2
  20. package/package.json +2 -1
  21. package/src/app/api/openai/chat/createChatCompletion.test.ts +4 -4
  22. package/src/app/api/openai/chat/createChatCompletion.ts +4 -10
  23. package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
  24. package/src/app/api/openai/errorResponse.test.ts +1 -1
  25. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +2 -1
  26. package/src/app/chat/(mobile)/features/ChatInput/SendButton.tsx +2 -2
  27. package/src/app/chat/features/ChatHeader/ShareButton/Inner.tsx +2 -5
  28. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +2 -2
  29. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +2 -2
  30. package/src/app/chat/features/ChatInput/ActionBar/Token/TokenTag.tsx +8 -2
  31. package/src/app/chat/features/ChatInput/InputAreaInner/index.tsx +2 -2
  32. package/src/app/chat/features/ChatInput/STT/index.tsx +2 -2
  33. package/src/app/chat/features/ChatInput/Topic/index.tsx +2 -2
  34. package/src/app/chat/features/ChatInput/useSend.ts +3 -3
  35. package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +2 -2
  36. package/src/app/chat/features/Conversation/ChatList/Error/ApiKeyForm.tsx +2 -2
  37. package/src/app/chat/features/Conversation/ChatList/Error/InvalidAccess.tsx +2 -2
  38. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +2 -2
  39. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.test.tsx +4 -4
  40. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +30 -31
  41. package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +2 -2
  42. package/src/app/chat/features/Conversation/ChatList/Extras/Translate.tsx +2 -2
  43. package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +8 -5
  44. package/src/app/chat/features/Conversation/ChatList/Extras/index.ts +3 -3
  45. package/src/app/chat/features/Conversation/ChatList/Extras/type.ts +5 -0
  46. package/src/app/chat/features/Conversation/ChatList/Messages/Assistant.tsx +10 -6
  47. package/src/app/chat/features/Conversation/ChatList/Messages/Default.tsx +7 -3
  48. package/src/app/chat/features/Conversation/ChatList/Messages/Function.tsx +7 -6
  49. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +7 -7
  50. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +6 -6
  51. package/src/app/chat/features/Conversation/ChatList/index.tsx +16 -24
  52. package/src/app/chat/features/Conversation/ScrollAnchor/index.tsx +2 -2
  53. package/src/app/chat/features/Conversation/index.tsx +3 -3
  54. package/src/app/chat/features/Conversation/useInitConversation.ts +43 -0
  55. package/src/app/chat/features/Migration/ExportConfigButton.tsx +33 -0
  56. package/src/app/chat/features/Migration/Failed.tsx +115 -0
  57. package/src/app/chat/features/Migration/Modal.tsx +80 -0
  58. package/src/app/chat/features/Migration/Start.tsx +106 -0
  59. package/src/app/chat/features/Migration/UpgradeButton.tsx +74 -0
  60. package/src/app/chat/features/Migration/const.ts +15 -0
  61. package/src/app/chat/features/Migration/index.tsx +44 -0
  62. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +54 -0
  63. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +5 -4
  64. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +22 -28
  65. package/src/app/chat/features/SessionListContent/SearchMode.tsx +15 -0
  66. package/src/app/chat/features/SessionListContent/index.tsx +4 -44
  67. package/src/app/chat/features/SessionSearchBar/index.tsx +11 -3
  68. package/src/app/chat/features/TopicListContent/Header.tsx +4 -4
  69. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +11 -9
  70. package/src/app/chat/features/TopicListContent/Topic/TopicItem.tsx +15 -13
  71. package/src/app/chat/features/TopicListContent/Topic/index.tsx +9 -16
  72. package/src/app/chat/features/TopicListContent/TopicSearchBar/index.tsx +13 -4
  73. package/src/app/chat/features/TopicListContent/index.tsx +1 -2
  74. package/src/app/chat/page.tsx +6 -1
  75. package/src/app/chat/settings/features/HeaderContent.tsx +3 -3
  76. package/src/app/home/Redirect.tsx +13 -7
  77. package/src/app/settings/(mobile)/features/ExtraList.tsx +12 -4
  78. package/src/app/settings/common/Common.tsx +16 -4
  79. package/src/app/welcome/features/Banner/index.tsx +9 -10
  80. package/src/chains/summaryTitle.ts +5 -2
  81. package/src/components/DataStyleModal/index.tsx +64 -0
  82. package/src/config/client.ts +5 -0
  83. package/src/const/session.ts +21 -0
  84. package/src/const/settings.ts +1 -1
  85. package/src/database/core/__tests__/db.test.ts +8 -5
  86. package/src/database/core/__tests__/model.test.ts +3 -3
  87. package/src/database/core/db.ts +38 -10
  88. package/src/database/core/index.ts +1 -0
  89. package/src/database/core/model.ts +121 -10
  90. package/src/database/core/schemas.ts +22 -0
  91. package/src/database/core/types/db.ts +13 -0
  92. package/src/database/models/__DEBUG.ts +119 -0
  93. package/src/database/models/{file.test.ts → __tests__/file.test.ts} +19 -6
  94. package/src/database/models/__tests__/message.test.ts +314 -0
  95. package/src/database/models/__tests__/session.test.ts +117 -0
  96. package/src/database/models/__tests__/topic.test.ts +250 -0
  97. package/src/database/models/file.ts +9 -5
  98. package/src/database/models/message.ts +186 -0
  99. package/src/database/models/session.ts +160 -0
  100. package/src/database/models/topic.ts +196 -0
  101. package/src/{types/database → database/schemas}/files.ts +2 -2
  102. package/src/database/schemas/message.ts +37 -0
  103. package/src/database/schemas/session.ts +56 -0
  104. package/src/database/schemas/topic.ts +12 -0
  105. package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +1 -1
  106. package/src/features/AgentSetting/store/action.ts +1 -1
  107. package/src/features/AgentSetting/store/initialState.ts +1 -1
  108. package/src/features/AgentSetting/store/reducers/config.ts +1 -1
  109. package/src/features/DataImporter/index.tsx +240 -0
  110. package/src/features/DebugUI/index.tsx +34 -0
  111. package/src/features/SideBar/BottomActions.tsx +8 -14
  112. package/src/hooks/useImportConfig.ts +13 -30
  113. package/src/hooks/useTTS.ts +1 -1
  114. package/src/layout/GlobalLayout/index.tsx +16 -1
  115. package/src/locales/default/common.ts +19 -0
  116. package/src/locales/default/migration.ts +47 -0
  117. package/src/locales/default/setting.ts +1 -1
  118. package/src/locales/resources/en_US.ts +2 -0
  119. package/src/locales/resources/ja_JP.ts +2 -0
  120. package/src/locales/resources/ko_KR.ts +2 -0
  121. package/src/locales/resources/ru_RU.ts +2 -0
  122. package/src/locales/resources/zh_CN.ts +2 -0
  123. package/src/locales/resources/zh_TW.ts +2 -0
  124. package/src/migrations/FromV0ToV1.ts +2 -4
  125. package/src/migrations/FromV1ToV2/fixtures/input-v1-session.json +191 -0
  126. package/src/migrations/FromV1ToV2/fixtures/output-v2.json +202 -0
  127. package/src/migrations/FromV1ToV2/index.ts +82 -0
  128. package/src/migrations/FromV1ToV2/migrations.test.ts +224 -0
  129. package/src/migrations/FromV1ToV2/types/v1.ts +78 -0
  130. package/src/migrations/FromV1ToV2/types/v2.ts +52 -0
  131. package/src/{utils → migrations}/VersionController.test.ts +1 -3
  132. package/src/{utils → migrations}/VersionController.ts +5 -2
  133. package/src/migrations/index.ts +17 -3
  134. package/src/services/__tests__/file.test.ts +3 -3
  135. package/src/services/chat.ts +66 -5
  136. package/src/services/config.ts +184 -0
  137. package/src/services/debug.ts +13 -0
  138. package/src/services/file.ts +6 -2
  139. package/src/services/github.ts +25 -0
  140. package/src/services/message.ts +79 -0
  141. package/src/services/session.ts +63 -0
  142. package/src/services/topic.ts +56 -0
  143. package/src/store/chat/actions/enhance.ts +95 -0
  144. package/src/store/{session/slices/chat → chat}/actions/message.ts +225 -247
  145. package/src/store/{session/slices/chat → chat}/actions/plugin.ts +29 -34
  146. package/src/store/{session/slices/chat → chat}/actions/share.ts +9 -13
  147. package/src/store/chat/actions/topic.ts +197 -0
  148. package/src/store/chat/helpers.ts +23 -0
  149. package/src/store/chat/index.ts +3 -0
  150. package/src/store/chat/initialState.ts +48 -0
  151. package/src/store/chat/reducers/message.test.ts +83 -0
  152. package/src/store/chat/reducers/message.ts +83 -0
  153. package/src/store/chat/selectors/__tests__/topic.test.ts +43 -0
  154. package/src/store/chat/selectors/chat.ts +119 -0
  155. package/src/store/chat/selectors/index.ts +2 -0
  156. package/src/store/chat/selectors/topic.ts +28 -0
  157. package/src/store/chat/store.ts +63 -0
  158. package/src/store/files/slices/images/action.ts +6 -4
  159. package/src/store/global/store.ts +1 -1
  160. package/src/store/middleware/createHyperStorage/index.ts +40 -24
  161. package/src/store/middleware/createHyperStorage/keyMapper.ts +8 -2
  162. package/src/store/middleware/createHyperStorage/type.ts +10 -8
  163. package/src/store/plugin/slices/plugin/action.ts +1 -1
  164. package/src/store/plugin/store.ts +2 -4
  165. package/src/store/session/helpers.ts +1 -1
  166. package/src/store/session/hooks/useSessionHydrated.ts +6 -10
  167. package/src/store/session/initialState.ts +26 -7
  168. package/src/store/session/selectors.ts +1 -2
  169. package/src/store/session/slices/agent/action.ts +26 -35
  170. package/src/store/session/slices/agent/selectors.ts +32 -37
  171. package/src/store/session/slices/session/action.ts +81 -88
  172. package/src/store/session/slices/session/helpers.ts +19 -0
  173. package/src/store/session/slices/session/initialState.ts +7 -10
  174. package/src/store/session/slices/session/selectors/export.ts +12 -9
  175. package/src/store/session/slices/session/selectors/index.ts +2 -35
  176. package/src/store/session/slices/session/selectors/list.test.ts +25 -203
  177. package/src/store/session/slices/session/selectors/list.ts +40 -78
  178. package/src/store/session/store.ts +11 -17
  179. package/src/types/agent/index.ts +61 -0
  180. package/src/types/chatMessage.ts +11 -20
  181. package/src/types/exportConfig.ts +7 -2
  182. package/src/types/files.ts +2 -2
  183. package/src/types/llm.ts +1 -1
  184. package/src/types/meta.ts +24 -12
  185. package/src/types/openai/chat.ts +9 -1
  186. package/src/types/openai/functionCall.ts +4 -0
  187. package/src/types/session.ts +10 -94
  188. package/src/types/translate.ts +2 -1
  189. package/src/utils/config.ts +5 -11
  190. package/src/utils/fetch.ts +8 -2
  191. package/src/app/chat/features/Conversation/usePluginsInit.ts +0 -14
  192. package/src/database/core/schema.ts +0 -9
  193. package/src/helpers/export.ts +0 -69
  194. package/src/hooks/useExportConfig.ts +0 -6
  195. package/src/store/session/slices/chat/actions/index.ts +0 -32
  196. package/src/store/session/slices/chat/actions/topic.ts +0 -171
  197. package/src/store/session/slices/chat/actions/translate.ts +0 -113
  198. package/src/store/session/slices/chat/helpers.ts +0 -9
  199. package/src/store/session/slices/chat/initialState.ts +0 -13
  200. package/src/store/session/slices/chat/reducers/message.test.ts +0 -319
  201. package/src/store/session/slices/chat/reducers/message.ts +0 -156
  202. package/src/store/session/slices/chat/reducers/topic.test.ts +0 -179
  203. package/src/store/session/slices/chat/reducers/topic.ts +0 -81
  204. package/src/store/session/slices/chat/selectors/chat.ts +0 -111
  205. package/src/store/session/slices/chat/selectors/index.ts +0 -26
  206. package/src/store/session/slices/chat/selectors/topic.ts +0 -30
  207. package/src/store/session/slices/chat/selectors/utils.test.ts +0 -385
  208. package/src/store/session/slices/chat/selectors/utils.ts +0 -91
  209. package/src/store/session/slices/chat/utils.ts +0 -13
  210. package/src/store/session/slices/session/reducers/session.test.ts +0 -577
  211. package/src/store/session/slices/session/reducers/session.ts +0 -182
  212. package/src/types/database/db.ts +0 -4
  213. /package/src/store/{session/slices/chat → chat}/reducers/files.test.ts +0 -0
  214. /package/src/store/{session/slices/chat → chat}/reducers/files.ts +0 -0
@@ -1,49 +1,48 @@
1
- import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
2
1
  import { StateCreator } from 'zustand/vanilla';
3
2
 
4
3
  import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
5
4
  import { chatService } from '@/services/chat';
6
- import { SessionStore } from '@/store/session';
7
- import { OpenAIFunctionCall } from '@/types/chatMessage';
5
+ import { messageService } from '@/services/message';
6
+ import { ChatStore } from '@/store/chat/store';
7
+ import { ChatPluginPayload } from '@/types/chatMessage';
8
+ import { OpenAIFunctionCall } from '@/types/openai/functionCall';
8
9
  import { setNamespace } from '@/utils/storeDebug';
9
10
 
10
- import { sessionSelectors } from '../../session/selectors';
11
11
  import { chatSelectors } from '../selectors';
12
12
 
13
- const t = setNamespace('chat/plugin');
13
+ const t = setNamespace('plugin');
14
14
 
15
- /**
16
- * 插件方法
17
- */
18
15
  export interface ChatPluginAction {
19
16
  fillPluginMessageContent: (id: string, content: string) => Promise<void>;
20
17
  runPluginDefaultType: (id: string, payload: any) => Promise<void>;
21
18
  triggerFunctionCall: (id: string) => Promise<void>;
22
- updatePluginState: (id: string, key: string, value: any) => void;
19
+ updatePluginState: (id: string, key: string, value: any) => Promise<void>;
23
20
  }
24
21
 
25
22
  export const chatPlugin: StateCreator<
26
- SessionStore,
23
+ ChatStore,
27
24
  [['zustand/devtools', never]],
28
25
  [],
29
26
  ChatPluginAction
30
27
  > = (set, get) => ({
31
28
  fillPluginMessageContent: async (id, content) => {
32
- const { dispatchMessage, coreProcessMessage } = get();
29
+ const { coreProcessMessage, refreshMessages } = get();
33
30
 
34
- dispatchMessage({ id, key: 'content', type: 'updateMessage', value: content });
31
+ await messageService.updateMessageContent(id, content);
32
+ await refreshMessages();
35
33
 
36
34
  const chats = chatSelectors.currentChats(get());
37
35
  await coreProcessMessage(chats, id);
38
36
  },
39
37
  runPluginDefaultType: async (id, payload) => {
40
- const { dispatchMessage, coreProcessMessage, toggleChatLoading } = get();
38
+ const { refreshMessages, coreProcessMessage, toggleChatLoading } = get();
41
39
  let data: string;
42
40
  try {
43
41
  const abortController = toggleChatLoading(true, id, t('fetchPlugin') as string);
44
42
  data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
45
43
  } catch (error) {
46
- dispatchMessage({ id, key: 'error', type: 'updateMessage', value: error });
44
+ await messageService.updateMessageError(id, error as any);
45
+ await refreshMessages();
47
46
 
48
47
  data = '';
49
48
  }
@@ -51,21 +50,19 @@ export const chatPlugin: StateCreator<
51
50
  // 如果报错则结束了
52
51
  if (!data) return;
53
52
 
54
- dispatchMessage({ id, key: 'content', type: 'updateMessage', value: data });
53
+ await messageService.updateMessageContent(id, data);
54
+ await refreshMessages();
55
55
 
56
56
  const chats = chatSelectors.currentChats(get());
57
57
  await coreProcessMessage(chats, id);
58
58
  },
59
59
  triggerFunctionCall: async (id) => {
60
- const { dispatchMessage, runPluginDefaultType } = get();
61
- const session = sessionSelectors.currentSession(get());
60
+ const { runPluginDefaultType, refreshMessages } = get();
62
61
 
63
- if (!session) return;
64
-
65
- const message = session.chats[id];
62
+ const message = chatSelectors.getMessageById(id)(get());
66
63
  if (!message) return;
67
64
 
68
- let payload: PluginRequestPayload = { apiName: '', identifier: '' };
65
+ let payload = { apiName: '', identifier: '' } as ChatPluginPayload;
69
66
 
70
67
  // 识别到内容是 function_call 的情况下
71
68
  // 将 function_call 转换为 plugin request payload
@@ -80,30 +77,28 @@ export const chatPlugin: StateCreator<
80
77
  apiName,
81
78
  arguments: function_call.arguments,
82
79
  identifier,
83
- type: type ?? 'default',
80
+ type: (type ?? 'default') as any,
84
81
  };
85
82
 
86
- dispatchMessage({ id, key: 'plugin', type: 'updateMessage', value: payload });
87
- dispatchMessage({ id, key: 'content', type: 'updateMessage', value: '' });
83
+ await messageService.updateMessageContent(id, '');
88
84
  } else {
89
- if (message.plugin) {
90
- payload = message.plugin;
91
- }
85
+ if (message.plugin) payload = message.plugin;
92
86
  }
93
87
 
94
88
  if (!payload.apiName) return;
95
89
 
96
- dispatchMessage({ id, key: 'role', type: 'updateMessage', value: 'function' });
97
- dispatchMessage({ id, key: 'name', type: 'updateMessage', value: payload.identifier });
98
- dispatchMessage({ id, key: 'plugin', type: 'updateMessage', value: payload });
90
+ await messageService.updateMessageRole(id, 'function');
91
+ await messageService.updateMessagePlugin(id, payload);
92
+ await refreshMessages();
99
93
 
100
94
  if (payload.type === 'standalone') {
101
- // nothing to do
95
+ // TODO: need to auth user's settings
102
96
  } else runPluginDefaultType(id, payload);
103
97
  },
104
- updatePluginState: (id, key, value) => {
105
- const { dispatchMessage } = get();
98
+ updatePluginState: async (id, key, value) => {
99
+ const { refreshMessages } = get();
106
100
 
107
- dispatchMessage({ id, key, type: 'updatePluginState', value });
101
+ await messageService.updateMessagePluginState(id, key, value);
102
+ await refreshMessages();
108
103
  },
109
104
  });
@@ -4,12 +4,12 @@ import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
6
6
  import { genShareGPTUrl } from '@/services/shareGPT';
7
- import { SessionStore } from '@/store/session';
7
+ import { useSessionStore } from '@/store/session';
8
+ import { agentSelectors } from '@/store/session/selectors';
8
9
  import { ShareGPTConversation } from '@/types/share';
9
10
 
10
- import { agentSelectors } from '../../agent';
11
- import { sessionSelectors } from '../../session/selectors';
12
11
  import { chatSelectors } from '../selectors';
12
+ import { ChatStore } from '../store';
13
13
 
14
14
  interface ShareMessage {
15
15
  from: 'human' | 'gpt';
@@ -50,18 +50,14 @@ export interface ShareAction {
50
50
  }) => void;
51
51
  }
52
52
 
53
- export const chatShare: StateCreator<
54
- SessionStore,
55
- [['zustand/devtools', never]],
56
- [],
57
- ShareAction
58
- > = (set, get) => ({
53
+ export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [], ShareAction> = (
54
+ set,
55
+ get,
56
+ ) => ({
59
57
  shareToShareGPT: async ({ withSystemRole, withPluginInfo, avatar }) => {
60
- const session = sessionSelectors.currentSession(get());
61
- if (!session) return;
62
58
  const messages = chatSelectors.currentChats(get());
63
- const config = agentSelectors.currentAgentConfig(get());
64
- const meta = agentSelectors.currentAgentMeta(get());
59
+ const config = agentSelectors.currentAgentConfig(useSessionStore.getState());
60
+ const meta = agentSelectors.currentAgentMeta(useSessionStore.getState());
65
61
 
66
62
  const defaultMsg: ShareGPTConversation['items'] = [];
67
63
  const showSystemRole = withSystemRole && !!config.systemRole;
@@ -0,0 +1,197 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
+ // Note: To make the code more logic and readable, we just disable the auto sort key eslint rule
3
+ // DON'T REMOVE THE FIRST LINE
4
+ import { t } from 'i18next';
5
+ import { produce } from 'immer';
6
+ import useSWR, { SWRResponse, mutate } from 'swr';
7
+ import { StateCreator } from 'zustand/vanilla';
8
+
9
+ import { chainSummaryTitle } from '@/chains/summaryTitle';
10
+ import { LOADING_FLAT } from '@/const/message';
11
+ import { chatService } from '@/services/chat';
12
+ import { messageService } from '@/services/message';
13
+ import { topicService } from '@/services/topic';
14
+ import { ChatStore } from '@/store/chat';
15
+ import { ChatMessage } from '@/types/chatMessage';
16
+ import { ChatTopic } from '@/types/topic';
17
+ import { setNamespace } from '@/utils/storeDebug';
18
+
19
+ import { chatSelectors, topicSelectors } from '../selectors';
20
+
21
+ const n = setNamespace('topic');
22
+
23
+ export interface ChatTopicAction {
24
+ favoriteTopic: (id: string, favState: boolean) => Promise<void>;
25
+ openNewTopicOrSaveTopic: () => void;
26
+ refreshTopic: () => Promise<void>;
27
+ removeAllTopics: () => Promise<void>;
28
+ removeSessionTopics: () => Promise<void>;
29
+ removeTopic: (id: string) => Promise<void>;
30
+ removeUnstarredTopic: () => void;
31
+ saveToTopic: () => Promise<string | undefined>;
32
+ summaryTopicTitle: (topicId: string, messages: ChatMessage[]) => Promise<void>;
33
+ switchTopic: (id?: string) => Promise<void>;
34
+ updateTopicTitleInSummary: (id: string, title: string) => void;
35
+ updateTopicLoading: (id?: string) => void;
36
+ updateTopicTitle: (id: string, title: string) => Promise<void>;
37
+ useFetchTopics: (sessionId: string) => SWRResponse<ChatTopic[]>;
38
+ useSearchTopics: (keywords?: string) => SWRResponse<ChatTopic[]>;
39
+ }
40
+
41
+ export const chatTopic: StateCreator<
42
+ ChatStore,
43
+ [['zustand/devtools', never]],
44
+ [],
45
+ ChatTopicAction
46
+ > = (set, get) => ({
47
+ // create
48
+ openNewTopicOrSaveTopic: () => {
49
+ const { switchTopic, saveToTopic, activeTopicId } = get();
50
+ const hasTopic = !!activeTopicId;
51
+
52
+ if (hasTopic) switchTopic();
53
+ else {
54
+ saveToTopic();
55
+ }
56
+ },
57
+
58
+ saveToTopic: async () => {
59
+ // if there is no message, stop
60
+ const messages = chatSelectors.currentChats(get());
61
+ if (messages.length === 0) return;
62
+
63
+ const { activeId, summaryTopicTitle, refreshTopic, refreshMessages } = get();
64
+
65
+ // 1. create topic
66
+ const topicId = await topicService.createTopic({
67
+ sessionId: activeId,
68
+ title: t('topic.defaultTitle', { ns: 'chat' }),
69
+ });
70
+ await refreshTopic();
71
+
72
+ // 2.add topicId to these message
73
+ await messageService.bindMessagesToTopic(
74
+ topicId,
75
+ messages.map((m) => m.id),
76
+ );
77
+ await refreshMessages();
78
+
79
+ // 3. auto summary topic Title
80
+ // we don't need to wait for summary, just let it run async
81
+ summaryTopicTitle(topicId, messages);
82
+
83
+ return topicId;
84
+ },
85
+ // update
86
+ summaryTopicTitle: async (topicId, messages) => {
87
+ const { updateTopicTitleInSummary, updateTopicLoading, refreshTopic } = get();
88
+ const topic = topicSelectors.getTopicById(topicId)(get());
89
+ if (!topic) return;
90
+
91
+ updateTopicTitleInSummary(topicId, LOADING_FLAT);
92
+
93
+ let output = '';
94
+
95
+ // 自动总结话题标题
96
+ await chatService.fetchPresetTaskResult({
97
+ onError: () => {
98
+ updateTopicTitleInSummary(topicId, topic.title);
99
+ },
100
+ onFinish: (text) => {
101
+ topicService.updateTitle(topicId, text);
102
+ },
103
+ onLoadingChange: (loading) => {
104
+ updateTopicLoading(loading ? topicId : undefined);
105
+ },
106
+ onMessageHandle: (x) => {
107
+ output += x;
108
+ updateTopicTitleInSummary(topicId, output);
109
+ },
110
+ params: await chainSummaryTitle(messages),
111
+ });
112
+ await refreshTopic();
113
+ },
114
+ favoriteTopic: async (id, favState) => {
115
+ await topicService.updateFavorite(id, favState);
116
+ await get().refreshTopic();
117
+ },
118
+ updateTopicTitle: async (id, title) => {
119
+ await topicService.updateTitle(id, title);
120
+ await get().refreshTopic();
121
+ },
122
+ // query
123
+ useFetchTopics: (sessionId) =>
124
+ useSWR<ChatTopic[]>(sessionId, async (sessionId) => topicService.getTopics({ sessionId }), {
125
+ onSuccess: (topics) => {
126
+ set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId }));
127
+ },
128
+ }),
129
+ useSearchTopics: (keywords) =>
130
+ useSWR<ChatTopic[]>(keywords, topicService.searchTopics, {
131
+ onSuccess: (data) => {
132
+ set({ searchTopics: data }, false, n('useSearchTopics(success)', { keywords }));
133
+ },
134
+ }),
135
+ switchTopic: async (id) => {
136
+ set({ activeTopicId: id }, false, n('toggleTopic'));
137
+
138
+ await get().refreshMessages();
139
+ },
140
+ // delete
141
+ removeSessionTopics: async () => {
142
+ const { switchTopic, activeId, refreshTopic } = get();
143
+
144
+ await topicService.removeTopics(activeId);
145
+ await refreshTopic();
146
+
147
+ // switch to default topic
148
+ switchTopic();
149
+ },
150
+ removeAllTopics: async () => {
151
+ const { refreshTopic } = get();
152
+
153
+ await topicService.removeAllTopic();
154
+ await refreshTopic();
155
+ },
156
+ removeTopic: async (id) => {
157
+ const { activeId, switchTopic, refreshTopic } = get();
158
+
159
+ // remove messages in the topic
160
+ await messageService.removeMessages(activeId, id);
161
+
162
+ // remove topic
163
+ await topicService.removeTopic(id);
164
+ await refreshTopic();
165
+
166
+ // switch bach to default topic
167
+ switchTopic();
168
+ },
169
+ removeUnstarredTopic: async () => {
170
+ const { refreshTopic, switchTopic } = get();
171
+ const topics = topicSelectors.currentUnFavTopics(get());
172
+
173
+ await topicService.batchRemoveTopics(topics.map((t) => t.id));
174
+ await refreshTopic();
175
+
176
+ // 切换到默认 topic
177
+ switchTopic();
178
+ },
179
+
180
+ // Internal process method of the topics
181
+ updateTopicTitleInSummary: (id, title) => {
182
+ const topics = produce(get().topics, (draftState) => {
183
+ const topic = draftState.find((i) => i.id === id);
184
+
185
+ if (!topic) return;
186
+ topic.title = title;
187
+ });
188
+
189
+ set({ topics }, false, n(`updateTopicTitleInSummary`, { id, title }));
190
+ },
191
+ updateTopicLoading: (id) => {
192
+ set({ topicLoadingId: id }, false, n('updateTopicLoading'));
193
+ },
194
+ refreshTopic: async () => {
195
+ await mutate(get().activeId);
196
+ },
197
+ });
@@ -0,0 +1,23 @@
1
+ import { LobeAgentConfig } from '@/types/agent';
2
+ import { ChatMessage } from '@/types/chatMessage';
3
+ import { OpenAIChatMessage } from '@/types/openai/chat';
4
+ import { encodeAsync } from '@/utils/tokenizer';
5
+
6
+ export const getMessagesTokenCount = async (messages: OpenAIChatMessage[]) =>
7
+ encodeAsync(messages.map((m) => m.content).join(''));
8
+
9
+ const getSlicedMessagesWithConfig = (
10
+ messages: ChatMessage[],
11
+ config: LobeAgentConfig,
12
+ ): ChatMessage[] => {
13
+ // 如果没有开启历史消息数限制,或者限制为 0,则直接返回
14
+ if (!config.enableHistoryCount || !config.historyCount) return messages;
15
+
16
+ // 如果开启了,则返回尾部的N条消息
17
+ return messages.reverse().slice(0, config.historyCount).reverse();
18
+ };
19
+
20
+ export const chatHelpers = {
21
+ getMessagesTokenCount,
22
+ getSlicedMessagesWithConfig,
23
+ };
@@ -0,0 +1,3 @@
1
+ export type { ChatStoreState } from './initialState';
2
+ export type { ChatStore } from './store';
3
+ export { useChatStore } from './store';
@@ -0,0 +1,48 @@
1
+ import { ChatMessage } from '@/types/chatMessage';
2
+ import { ChatTopic } from '@/types/topic';
3
+
4
+ export interface ChatStoreState {
5
+ abortController?: AbortController;
6
+ /**
7
+ * @title 当前活动的会话
8
+ * @description 当前正在编辑或查看的会话
9
+ */
10
+ activeId: string;
11
+ activeTopicId?: string;
12
+ /**
13
+ * @deprecated
14
+ */
15
+ chatLoadingId?: string;
16
+ inputMessage: string;
17
+ isSearchingTopic: boolean;
18
+ messageLoadingIds: [];
19
+ messages: ChatMessage[];
20
+ /**
21
+ * whether messages have fetched
22
+ */
23
+ messagesInit: boolean;
24
+
25
+ searchTopics: ChatTopic[];
26
+ shareLoading?: boolean;
27
+ topicLoadingId?: string;
28
+ topicRenamingId?: string;
29
+ topicSearchKeywords: string;
30
+ topics: ChatTopic[];
31
+ /**
32
+ * whether topics have fetched
33
+ */
34
+ topicsInit: boolean;
35
+ }
36
+
37
+ export const initialState: ChatStoreState = {
38
+ activeId: 'inbox',
39
+ inputMessage: '',
40
+ isSearchingTopic: false,
41
+ messageLoadingIds: [],
42
+ messages: [],
43
+ messagesInit: false,
44
+ searchTopics: [],
45
+ topicSearchKeywords: '',
46
+ topics: [],
47
+ topicsInit: false,
48
+ };
@@ -0,0 +1,83 @@
1
+ import { ChatMessage, ChatMessageMap } from '@/types/chatMessage';
2
+
3
+ import { MessageDispatch, messagesReducer } from './message';
4
+
5
+ describe('messagesReducer', () => {
6
+ let initialState: ChatMessage[];
7
+
8
+ beforeEach(() => {
9
+ initialState = [
10
+ {
11
+ id: 'message1',
12
+ content: 'Hello World',
13
+ createdAt: 1629264000000,
14
+ updatedAt: 1629264000000,
15
+ role: 'user',
16
+ meta: {},
17
+ },
18
+ {
19
+ id: 'message2',
20
+ content: 'How are you?',
21
+ createdAt: 1629264000000,
22
+ updatedAt: 1629264000000,
23
+ role: 'system',
24
+ meta: {},
25
+ },
26
+ ];
27
+ });
28
+
29
+ describe('updateMessage', () => {
30
+ it('should update the specified message with the provided value', () => {
31
+ const payload: MessageDispatch = {
32
+ type: 'updateMessage',
33
+ id: 'message1',
34
+ key: 'content',
35
+ value: 'Updated Message',
36
+ };
37
+
38
+ const newState = messagesReducer(initialState, payload);
39
+ const newMessage = newState.find((i) => i.id === 'message1');
40
+
41
+ expect(newMessage).not.toBeUndefined();
42
+ expect(newMessage?.content).toBe('Updated Message');
43
+ expect(newMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
44
+ });
45
+
46
+ it('should not modify the state if the specified message does not exist', () => {
47
+ const payload: MessageDispatch = {
48
+ type: 'updateMessage',
49
+ id: 'nonexistentMessage',
50
+ key: 'content',
51
+ value: 'Updated Message',
52
+ };
53
+
54
+ const newState = messagesReducer(initialState, payload);
55
+
56
+ expect(newState).toEqual(initialState);
57
+ });
58
+
59
+ it('should not modify the state if the specified message does not exist', () => {
60
+ const payload: MessageDispatch = {
61
+ type: 'updateMessage',
62
+ id: 'nonexistentMessage',
63
+ key: 'content',
64
+ value: 'Updated Message',
65
+ };
66
+
67
+ const newState = messagesReducer(initialState, payload);
68
+
69
+ expect(newState).toEqual(initialState);
70
+ });
71
+ });
72
+
73
+ describe('unimplemented type', () => {
74
+ it('should throw an error when an unimplemented type is provided', () => {
75
+ // @ts-ignore
76
+ const payload: MessageDispatch = { type: 'unimplementedType' };
77
+
78
+ expect(() => messagesReducer(initialState, payload)).toThrowError(
79
+ '暂未实现的 type,请检查 reducer',
80
+ );
81
+ });
82
+ });
83
+ });
@@ -0,0 +1,83 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import { produce } from 'immer';
3
+
4
+ import { ChatMessage } from '@/types/chatMessage';
5
+ import { merge } from '@/utils/merge';
6
+
7
+ interface UpdateMessage {
8
+ id: string;
9
+ key: keyof ChatMessage;
10
+ type: 'updateMessage';
11
+ value: ChatMessage[keyof ChatMessage];
12
+ }
13
+
14
+ interface UpdatePluginState {
15
+ id: string;
16
+ key: string;
17
+ type: 'updatePluginState';
18
+ value: any;
19
+ }
20
+ interface UpdateMessageExtra {
21
+ id: string;
22
+ key: string;
23
+ type: 'updateMessageExtra';
24
+ value: any;
25
+ }
26
+
27
+ export type MessageDispatch = UpdateMessage | UpdatePluginState | UpdateMessageExtra;
28
+
29
+ export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
30
+ switch (payload.type) {
31
+ case 'updateMessage': {
32
+ return produce(state, (draftState) => {
33
+ const { id, key, value } = payload;
34
+ const message = draftState.find((i) => i.id === id);
35
+ if (!message) return;
36
+
37
+ // @ts-ignore
38
+ message[key] = value;
39
+ message.updatedAt = Date.now();
40
+ });
41
+ }
42
+
43
+ case 'updateMessageExtra': {
44
+ return produce(state, (draftState) => {
45
+ const { id, key, value } = payload;
46
+ const message = draftState.find((i) => i.id === id);
47
+ if (!message) return;
48
+
49
+ if (!message.extra) {
50
+ message.extra = { [key]: value } as any;
51
+ } else {
52
+ message.extra[key] = value;
53
+ }
54
+
55
+ message.updateAt = Date.now();
56
+ });
57
+ }
58
+
59
+ case 'updatePluginState': {
60
+ return produce(state, (draftState) => {
61
+ const { id, key, value } = payload;
62
+ const message = draftState.find((i) => i.id === id);
63
+ if (!message) return;
64
+
65
+ let newState;
66
+ if (!message.pluginState) {
67
+ newState = { [key]: value } as any;
68
+ } else {
69
+ newState = merge(message.pluginState, { [key]: value });
70
+ }
71
+
72
+ if (isEqual(message.pluginState, newState)) return;
73
+
74
+ message.pluginState = newState;
75
+ message.updatedAt = Date.now();
76
+ });
77
+ }
78
+
79
+ default: {
80
+ throw new Error('暂未实现的 type,请检查 reducer');
81
+ }
82
+ }
83
+ };
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ChatStore, ChatStoreState } from '@/store/chat';
4
+ import { initialState } from '@/store/chat/initialState';
5
+ import { merge } from '@/utils/merge';
6
+
7
+ import { topicSelectors } from '..';
8
+
9
+ const initialStore = initialState as ChatStore;
10
+
11
+ const mockTopics = [
12
+ { id: 'topic1', name: 'Topic 1' },
13
+ { id: 'topic2', name: 'Topic 2' },
14
+ ];
15
+
16
+ describe('topicSelectors', () => {
17
+ describe('currentTopics', () => {
18
+ it('should return an empty array if there are no topics', () => {
19
+ const topics = topicSelectors.currentTopics(initialStore);
20
+ expect(topics).toEqual([]);
21
+ });
22
+
23
+ it('should return all current topics from the store', () => {
24
+ const state = merge(initialStore, { topics: mockTopics });
25
+
26
+ const topics = topicSelectors.currentTopics(state);
27
+ expect(topics).toEqual(mockTopics);
28
+ });
29
+ });
30
+
31
+ describe('currentTopicLength', () => {
32
+ it('should return 0 if there are no topics', () => {
33
+ const length = topicSelectors.currentTopicLength(initialStore);
34
+ expect(length).toBe(0);
35
+ });
36
+
37
+ it('should return the number of current topics', () => {
38
+ const state = merge(initialStore, { topics: mockTopics });
39
+ const length = topicSelectors.currentTopicLength(state);
40
+ expect(length).toBe(mockTopics.length);
41
+ });
42
+ });
43
+ });