@lobehub/chat 0.105.2 → 0.107.0

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 (224) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +18 -0
  3. package/README.zh-CN.md +18 -0
  4. package/docs/Environment-Variable.md +4 -2
  5. package/docs/Environment-Variable.zh-CN.md +4 -2
  6. package/locales/en_US/common.json +19 -0
  7. package/locales/en_US/migration.json +45 -0
  8. package/locales/en_US/setting.json +7 -7
  9. package/locales/ja_JP/common.json +19 -0
  10. package/locales/ja_JP/migration.json +45 -0
  11. package/locales/ja_JP/setting.json +2 -2
  12. package/locales/ko_KR/common.json +19 -0
  13. package/locales/ko_KR/migration.json +45 -0
  14. package/locales/ko_KR/setting.json +2 -2
  15. package/locales/ru_RU/common.json +19 -0
  16. package/locales/ru_RU/migration.json +45 -0
  17. package/locales/ru_RU/setting.json +2 -2
  18. package/locales/zh_CN/common.json +19 -0
  19. package/locales/zh_CN/migration.json +45 -0
  20. package/locales/zh_CN/setting.json +1 -1
  21. package/locales/zh_TW/common.json +19 -0
  22. package/locales/zh_TW/migration.json +45 -0
  23. package/locales/zh_TW/setting.json +2 -2
  24. package/package.json +2 -1
  25. package/src/app/api/openai/chat/createChatCompletion.test.ts +4 -4
  26. package/src/app/api/openai/chat/createChatCompletion.ts +4 -10
  27. package/src/app/api/openai/createBizOpenAI/index.ts +1 -1
  28. package/src/app/api/openai/errorResponse.test.ts +1 -1
  29. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +2 -1
  30. package/src/app/chat/(mobile)/features/ChatInput/SendButton.tsx +2 -2
  31. package/src/app/chat/features/ChatHeader/ShareButton/Inner.tsx +2 -5
  32. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +2 -2
  33. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +2 -2
  34. package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +1 -1
  35. package/src/app/chat/features/ChatInput/ActionBar/Token/TokenTag.tsx +8 -2
  36. package/src/app/chat/features/ChatInput/InputAreaInner/index.tsx +2 -2
  37. package/src/app/chat/features/ChatInput/STT/index.tsx +2 -2
  38. package/src/app/chat/features/ChatInput/Topic/index.tsx +2 -2
  39. package/src/app/chat/features/ChatInput/useSend.ts +3 -3
  40. package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +2 -2
  41. package/src/app/chat/features/Conversation/ChatList/Error/ApiKeyForm.tsx +2 -2
  42. package/src/app/chat/features/Conversation/ChatList/Error/InvalidAccess.tsx +2 -2
  43. package/src/app/chat/features/Conversation/ChatList/Error/Plugin/PluginSettings.tsx +2 -2
  44. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.test.tsx +4 -4
  45. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +30 -31
  46. package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +2 -2
  47. package/src/app/chat/features/Conversation/ChatList/Extras/Translate.tsx +2 -2
  48. package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +8 -5
  49. package/src/app/chat/features/Conversation/ChatList/Extras/index.ts +3 -3
  50. package/src/app/chat/features/Conversation/ChatList/Extras/type.ts +5 -0
  51. package/src/app/chat/features/Conversation/ChatList/Messages/Assistant.tsx +10 -6
  52. package/src/app/chat/features/Conversation/ChatList/Messages/Default.tsx +7 -3
  53. package/src/app/chat/features/Conversation/ChatList/Messages/Function.tsx +7 -6
  54. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +7 -7
  55. package/src/app/chat/features/Conversation/ChatList/Plugins/Render/StandaloneType/Iframe.tsx +6 -6
  56. package/src/app/chat/features/Conversation/ChatList/index.tsx +16 -24
  57. package/src/app/chat/features/Conversation/ScrollAnchor/index.tsx +2 -2
  58. package/src/app/chat/features/Conversation/index.tsx +3 -3
  59. package/src/app/chat/features/Conversation/useInitConversation.ts +43 -0
  60. package/src/app/chat/features/Migration/ExportConfigButton.tsx +33 -0
  61. package/src/app/chat/features/Migration/Failed.tsx +115 -0
  62. package/src/app/chat/features/Migration/Modal.tsx +80 -0
  63. package/src/app/chat/features/Migration/Start.tsx +106 -0
  64. package/src/app/chat/features/Migration/UpgradeButton.tsx +74 -0
  65. package/src/app/chat/features/Migration/const.ts +15 -0
  66. package/src/app/chat/features/Migration/index.tsx +44 -0
  67. package/src/app/chat/features/SessionListContent/DefaultMode.tsx +54 -0
  68. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +5 -4
  69. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +22 -28
  70. package/src/app/chat/features/SessionListContent/SearchMode.tsx +15 -0
  71. package/src/app/chat/features/SessionListContent/index.tsx +4 -44
  72. package/src/app/chat/features/SessionSearchBar/index.tsx +11 -3
  73. package/src/app/chat/features/TopicListContent/Header.tsx +4 -4
  74. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +11 -9
  75. package/src/app/chat/features/TopicListContent/Topic/TopicItem.tsx +15 -13
  76. package/src/app/chat/features/TopicListContent/Topic/index.tsx +9 -16
  77. package/src/app/chat/features/TopicListContent/TopicSearchBar/index.tsx +13 -4
  78. package/src/app/chat/features/TopicListContent/index.tsx +1 -2
  79. package/src/app/chat/page.tsx +6 -1
  80. package/src/app/chat/settings/features/HeaderContent.tsx +3 -3
  81. package/src/app/home/Redirect.tsx +13 -7
  82. package/src/app/settings/(mobile)/features/ExtraList.tsx +12 -4
  83. package/src/app/settings/common/Common.tsx +16 -4
  84. package/src/app/welcome/features/Banner/index.tsx +9 -10
  85. package/src/chains/summaryTitle.ts +5 -2
  86. package/src/components/DataStyleModal/index.tsx +64 -0
  87. package/src/config/client.ts +5 -0
  88. package/src/const/session.ts +21 -0
  89. package/src/const/settings.ts +1 -1
  90. package/src/database/core/__tests__/db.test.ts +8 -5
  91. package/src/database/core/__tests__/model.test.ts +3 -3
  92. package/src/database/core/db.ts +38 -10
  93. package/src/database/core/index.ts +1 -0
  94. package/src/database/core/model.ts +121 -10
  95. package/src/database/core/schemas.ts +22 -0
  96. package/src/database/core/types/db.ts +13 -0
  97. package/src/database/models/__DEBUG.ts +119 -0
  98. package/src/database/models/{file.test.ts → __tests__/file.test.ts} +19 -6
  99. package/src/database/models/__tests__/message.test.ts +314 -0
  100. package/src/database/models/__tests__/session.test.ts +117 -0
  101. package/src/database/models/__tests__/topic.test.ts +250 -0
  102. package/src/database/models/file.ts +9 -5
  103. package/src/database/models/message.ts +186 -0
  104. package/src/database/models/session.ts +160 -0
  105. package/src/database/models/topic.ts +196 -0
  106. package/src/{types/database → database/schemas}/files.ts +2 -2
  107. package/src/database/schemas/message.ts +37 -0
  108. package/src/database/schemas/session.ts +56 -0
  109. package/src/database/schemas/topic.ts +12 -0
  110. package/src/features/AgentSetting/AgentConfig/index.tsx +8 -1
  111. package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +1 -1
  112. package/src/features/AgentSetting/store/action.ts +1 -1
  113. package/src/features/AgentSetting/store/initialState.ts +1 -1
  114. package/src/features/AgentSetting/store/reducers/config.ts +1 -1
  115. package/src/features/DataImporter/index.tsx +240 -0
  116. package/src/features/DebugUI/index.tsx +34 -0
  117. package/src/features/SideBar/BottomActions.tsx +8 -14
  118. package/src/hooks/useImportConfig.ts +13 -30
  119. package/src/hooks/useTTS.ts +1 -1
  120. package/src/layout/GlobalLayout/index.tsx +16 -1
  121. package/src/locales/default/common.ts +19 -0
  122. package/src/locales/default/migration.ts +47 -0
  123. package/src/locales/default/setting.ts +1 -1
  124. package/src/locales/resources/en_US.ts +2 -0
  125. package/src/locales/resources/ja_JP.ts +2 -0
  126. package/src/locales/resources/ko_KR.ts +2 -0
  127. package/src/locales/resources/ru_RU.ts +2 -0
  128. package/src/locales/resources/zh_CN.ts +2 -0
  129. package/src/locales/resources/zh_TW.ts +2 -0
  130. package/src/migrations/FromV0ToV1.ts +2 -4
  131. package/src/migrations/FromV1ToV2/fixtures/input-v1-session.json +191 -0
  132. package/src/migrations/FromV1ToV2/fixtures/output-v2.json +202 -0
  133. package/src/migrations/FromV1ToV2/index.ts +82 -0
  134. package/src/migrations/FromV1ToV2/migrations.test.ts +224 -0
  135. package/src/migrations/FromV1ToV2/types/v1.ts +78 -0
  136. package/src/migrations/FromV1ToV2/types/v2.ts +52 -0
  137. package/src/{utils → migrations}/VersionController.test.ts +1 -3
  138. package/src/{utils → migrations}/VersionController.ts +5 -2
  139. package/src/migrations/index.ts +17 -3
  140. package/src/services/__tests__/file.test.ts +3 -3
  141. package/src/services/chat.ts +66 -5
  142. package/src/services/config.ts +184 -0
  143. package/src/services/debug.ts +13 -0
  144. package/src/services/file.ts +6 -2
  145. package/src/services/github.ts +25 -0
  146. package/src/services/message.ts +79 -0
  147. package/src/services/session.ts +63 -0
  148. package/src/services/topic.ts +56 -0
  149. package/src/store/chat/actions/enhance.ts +95 -0
  150. package/src/store/{session/slices/chat → chat}/actions/message.ts +225 -247
  151. package/src/store/{session/slices/chat → chat}/actions/plugin.ts +29 -34
  152. package/src/store/{session/slices/chat → chat}/actions/share.ts +9 -13
  153. package/src/store/chat/actions/topic.ts +197 -0
  154. package/src/store/chat/helpers.ts +23 -0
  155. package/src/store/chat/index.ts +3 -0
  156. package/src/store/chat/initialState.ts +48 -0
  157. package/src/store/chat/reducers/message.test.ts +83 -0
  158. package/src/store/chat/reducers/message.ts +83 -0
  159. package/src/store/chat/selectors/__tests__/topic.test.ts +43 -0
  160. package/src/store/chat/selectors/chat.ts +119 -0
  161. package/src/store/chat/selectors/index.ts +2 -0
  162. package/src/store/chat/selectors/topic.ts +28 -0
  163. package/src/store/chat/store.ts +63 -0
  164. package/src/store/files/slices/images/action.ts +6 -4
  165. package/src/store/global/selectors/__snapshots__/settings.test.ts.snap +99 -0
  166. package/src/store/global/selectors/settings.test.ts +51 -0
  167. package/src/store/global/selectors/settings.ts +36 -5
  168. package/src/store/global/store.ts +1 -1
  169. package/src/store/middleware/createHyperStorage/index.ts +40 -24
  170. package/src/store/middleware/createHyperStorage/keyMapper.ts +8 -2
  171. package/src/store/middleware/createHyperStorage/type.ts +10 -8
  172. package/src/store/plugin/slices/plugin/action.ts +1 -1
  173. package/src/store/plugin/store.ts +2 -4
  174. package/src/store/session/helpers.ts +1 -1
  175. package/src/store/session/hooks/useSessionHydrated.ts +6 -10
  176. package/src/store/session/initialState.ts +26 -7
  177. package/src/store/session/selectors.ts +1 -2
  178. package/src/store/session/slices/agent/action.ts +26 -35
  179. package/src/store/session/slices/agent/selectors.ts +32 -37
  180. package/src/store/session/slices/session/action.ts +81 -88
  181. package/src/store/session/slices/session/helpers.ts +19 -0
  182. package/src/store/session/slices/session/initialState.ts +7 -10
  183. package/src/store/session/slices/session/selectors/export.ts +12 -9
  184. package/src/store/session/slices/session/selectors/index.ts +2 -35
  185. package/src/store/session/slices/session/selectors/list.test.ts +25 -203
  186. package/src/store/session/slices/session/selectors/list.ts +40 -78
  187. package/src/store/session/store.ts +11 -17
  188. package/src/types/agent/index.ts +61 -0
  189. package/src/types/chatMessage.ts +11 -20
  190. package/src/types/exportConfig.ts +7 -2
  191. package/src/types/files.ts +2 -2
  192. package/src/types/llm.ts +1 -1
  193. package/src/types/meta.ts +24 -12
  194. package/src/types/openai/chat.ts +9 -1
  195. package/src/types/openai/functionCall.ts +4 -0
  196. package/src/types/session.ts +10 -94
  197. package/src/types/settings.ts +2 -0
  198. package/src/types/translate.ts +2 -1
  199. package/src/utils/config.ts +5 -11
  200. package/src/utils/fetch.ts +8 -2
  201. package/src/app/chat/features/Conversation/usePluginsInit.ts +0 -14
  202. package/src/database/core/schema.ts +0 -9
  203. package/src/helpers/export.ts +0 -69
  204. package/src/hooks/useExportConfig.ts +0 -6
  205. package/src/store/session/slices/chat/actions/index.ts +0 -32
  206. package/src/store/session/slices/chat/actions/topic.ts +0 -171
  207. package/src/store/session/slices/chat/actions/translate.ts +0 -113
  208. package/src/store/session/slices/chat/helpers.ts +0 -9
  209. package/src/store/session/slices/chat/initialState.ts +0 -13
  210. package/src/store/session/slices/chat/reducers/message.test.ts +0 -319
  211. package/src/store/session/slices/chat/reducers/message.ts +0 -156
  212. package/src/store/session/slices/chat/reducers/topic.test.ts +0 -179
  213. package/src/store/session/slices/chat/reducers/topic.ts +0 -81
  214. package/src/store/session/slices/chat/selectors/chat.ts +0 -111
  215. package/src/store/session/slices/chat/selectors/index.ts +0 -26
  216. package/src/store/session/slices/chat/selectors/topic.ts +0 -30
  217. package/src/store/session/slices/chat/selectors/utils.test.ts +0 -385
  218. package/src/store/session/slices/chat/selectors/utils.ts +0 -91
  219. package/src/store/session/slices/chat/utils.ts +0 -13
  220. package/src/store/session/slices/session/reducers/session.test.ts +0 -577
  221. package/src/store/session/slices/session/reducers/session.ts +0 -182
  222. package/src/types/database/db.ts +0 -4
  223. /package/src/store/{session/slices/chat → chat}/reducers/files.test.ts +0 -0
  224. /package/src/store/{session/slices/chat → chat}/reducers/files.ts +0 -0
@@ -1,56 +1,59 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
+ // Note: DON'T REMOVE THE FIRST LINE
3
+ // Disable the auto sort key eslint rule to make the code more logic and readable
1
4
  import { template } from 'lodash-es';
5
+ import useSWR, { SWRResponse, mutate } from 'swr';
2
6
  import { StateCreator } from 'zustand/vanilla';
3
7
 
4
8
  import { VISION_MODEL_WHITE_LIST } from '@/const/llm';
5
9
  import { LOADING_FLAT } from '@/const/message';
6
10
  import { VISION_MODEL_DEFAULT_MAX_TOKENS } from '@/const/settings';
11
+ import { CreateMessageParams } from '@/database/models/message';
12
+ import { DB_Message } from '@/database/schemas/message';
7
13
  import { chatService } from '@/services/chat';
8
- import { filesSelectors, useFileStore } from '@/store/files';
9
- import { SessionStore } from '@/store/session';
14
+ import { messageService } from '@/services/message';
15
+ import { topicService } from '@/services/topic';
16
+ import { chatHelpers } from '@/store/chat/helpers';
17
+ import { ChatStore } from '@/store/chat/store';
18
+ import { useSessionStore } from '@/store/session';
19
+ import { agentSelectors } from '@/store/session/selectors';
10
20
  import { ChatMessage } from '@/types/chatMessage';
11
- import { OpenAIChatMessage, UserMessageContentPart } from '@/types/openai/chat';
12
21
  import { fetchSSE } from '@/utils/fetch';
13
22
  import { isFunctionMessageAtStart, testFunctionMessageAtEnd } from '@/utils/message';
14
23
  import { setNamespace } from '@/utils/storeDebug';
15
- import { nanoid } from '@/utils/uuid';
16
24
 
17
- import { agentSelectors } from '../../agent/selectors';
18
- import { sessionSelectors } from '../../session/selectors';
19
- import { FileDispatch, filesReducer } from '../reducers/files';
20
25
  import { MessageDispatch, messagesReducer } from '../reducers/message';
21
26
  import { chatSelectors } from '../selectors';
22
- import { getSlicedMessagesWithConfig } from '../utils';
23
27
 
24
- const t = setNamespace('chat/message');
28
+ const t = setNamespace('message');
25
29
 
26
- /**
27
- * 聊天操作
28
- */
29
30
  export interface ChatMessageAction {
31
+ // create
32
+ resendMessage: (id: string) => Promise<void>;
33
+ sendMessage: (text: string, images?: { id: string; url: string }[]) => Promise<void>;
34
+ // delete
30
35
  /**
31
- * 清除消息
32
- */
33
- clearMessage: () => void;
34
- /**
35
- * 处理 ai 消息的核心逻辑(包含前处理与后处理)
36
- * @param messages - 聊天消息数组
37
- * @param parentId - 父消息 ID,可选
38
- */
39
- coreProcessMessage: (messages: ChatMessage[], parentId: string) => Promise<void>;
40
- /**
41
- * 删除消息
42
- * @param id - 消息 ID
36
+ * clear message on the active session
43
37
  */
44
- deleteMessage: (id: string) => void;
38
+ clearMessage: () => Promise<void>;
39
+ deleteMessage: (id: string) => Promise<void>;
40
+ clearAllMessages: () => Promise<void>;
41
+ // update
42
+ updateInputMessage: (message: string) => void;
43
+ updateMessageContent: (id: string, content: string) => Promise<void>;
44
+ // query
45
+ useFetchMessages: (sessionId: string, topicId?: string) => SWRResponse<ChatMessage[]>;
46
+ stopGenerateMessage: () => void;
47
+
45
48
  /**
46
- * agent files dispatch method
49
+ * update message at the frontend point
50
+ * this method will not update messages to database
47
51
  */
48
- dispatchAgentFile: (payload: FileDispatch) => void;
52
+ dispatchMessage: (payload: MessageDispatch) => void;
49
53
  /**
50
- * 分发消息
51
- * @param payload - 消息分发参数
54
+ * core process of the AI message (include preprocess and postprocess)
52
55
  */
53
- dispatchMessage: (payload: MessageDispatch) => void;
56
+ coreProcessMessage: (messages: ChatMessage[], parentId: string) => Promise<void>;
54
57
  /**
55
58
  * 实际获取 AI 响应
56
59
  * @param messages - 聊天消息数组
@@ -65,135 +68,225 @@ export interface ChatMessageAction {
65
68
  functionCallContent: string;
66
69
  isFunctionCall: boolean;
67
70
  }>;
68
-
69
- /**
70
- * 重新发送消息
71
- * @param id - 消息 ID
72
- */
73
- resendMessage: (id: string) => Promise<void>;
74
- /**
75
- * 发送消息
76
- * @param text - 消息文本
77
- */
78
- sendMessage: (text: string, images?: { id: string; url: string }[]) => Promise<void>;
79
- stopGenerateMessage: () => void;
80
71
  toggleChatLoading: (
81
72
  loading: boolean,
82
73
  id?: string,
83
74
  action?: string,
84
75
  ) => AbortController | undefined;
85
-
86
- updateInputMessage: (message: string) => void;
76
+ refreshMessages: () => Promise<void>;
87
77
  }
88
78
 
79
+ const getAgentConfig = () => agentSelectors.currentAgentConfig(useSessionStore.getState());
80
+
89
81
  export const chatMessage: StateCreator<
90
- SessionStore,
82
+ ChatStore,
91
83
  [['zustand/devtools', never]],
92
84
  [],
93
85
  ChatMessageAction
94
86
  > = (set, get) => ({
95
- clearMessage: () => {
96
- const { dispatchMessage, activeTopicId, dispatchTopic, toggleTopic } = get();
87
+ deleteMessage: async (id) => {
88
+ await messageService.removeMessage(id);
89
+ await get().refreshMessages();
90
+ },
91
+ clearMessage: async () => {
92
+ const { activeId, activeTopicId, refreshMessages, refreshTopic, switchTopic } = get();
97
93
 
98
- dispatchMessage({ topicId: activeTopicId, type: 'resetMessages' });
94
+ await messageService.removeMessages(activeId, activeTopicId);
99
95
 
100
96
  if (activeTopicId) {
101
- dispatchTopic({ id: activeTopicId, type: 'deleteChatTopic' });
97
+ await topicService.removeTopics(activeId);
102
98
  }
99
+ await refreshTopic();
100
+ await refreshMessages();
103
101
 
104
102
  // after remove topic , go back to default topic
105
- toggleTopic();
103
+ switchTopic();
106
104
  },
107
- coreProcessMessage: async (messages, userMessageId) => {
108
- const { dispatchMessage, fetchAIChatMessage, triggerFunctionCall, activeTopicId } = get();
105
+ clearAllMessages: async () => {
106
+ const { refreshMessages } = get();
107
+ await messageService.clearAllMessage();
108
+ await refreshMessages();
109
+ },
110
+ resendMessage: async (messageId) => {
111
+ // 1. 构造所有相关的历史记录
112
+ const chats = chatSelectors.currentChats(get());
109
113
 
110
- const { model } = agentSelectors.currentAgentConfig(get());
114
+ const currentIndex = chats.findIndex((c) => c.id === messageId);
115
+ if (currentIndex < 0) return;
111
116
 
112
- // 添加一个空的信息用于放置 ai 响应,注意顺序不能反
113
- // 因为如果顺序反了,messages 中将包含新增的 ai message
114
- const mid = nanoid();
117
+ const currentMessage = chats[currentIndex];
115
118
 
116
- dispatchMessage({
117
- id: mid,
118
- message: LOADING_FLAT,
119
- parentId: userMessageId,
120
- role: 'assistant',
121
- type: 'addMessage',
122
- });
119
+ let contextMessages: ChatMessage[] = [];
123
120
 
124
- // 如果有 activeTopicId,则添加 topicId
125
- if (activeTopicId) {
126
- dispatchMessage({ id: mid, key: 'topicId', type: 'updateMessage', value: activeTopicId });
121
+ switch (currentMessage.role) {
122
+ case 'function':
123
+ case 'user': {
124
+ contextMessages = chats.slice(0, currentIndex + 1);
125
+ break;
126
+ }
127
+ case 'assistant': {
128
+ // 消息是 AI 发出的因此需要找到它的 user 消息
129
+ const userId = currentMessage.parentId;
130
+ const userIndex = chats.findIndex((c) => c.id === userId);
131
+ // 如果消息没有 parentId,那么同 user/function 模式
132
+ contextMessages = chats.slice(0, userIndex < 0 ? currentIndex + 1 : userIndex + 1);
133
+ break;
134
+ }
127
135
  }
128
136
 
129
- // 为模型添加 fromModel 的额外信息
130
- dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: model });
137
+ if (contextMessages.length <= 0) return;
138
+
139
+ const { coreProcessMessage } = get();
140
+
141
+ const latestMsg = contextMessages.filter((s) => s.role === 'user').at(-1);
142
+
143
+ if (!latestMsg) return;
144
+
145
+ await coreProcessMessage(contextMessages, latestMsg.id);
146
+ },
147
+ sendMessage: async (message, files) => {
148
+ const { coreProcessMessage, activeTopicId, activeId } = get();
149
+ if (!message || !activeId) return;
150
+
151
+ const fileIdList = files?.map((f) => f.id);
152
+
153
+ let newMessage: CreateMessageParams = {
154
+ content: message,
155
+ // if message has attached with files, then add files to message and the agent
156
+ files: fileIdList,
157
+ role: 'user',
158
+ sessionId: activeId,
159
+ // if there is activeTopicId,then add topicId to message
160
+ topicId: activeTopicId,
161
+ };
162
+
163
+ const id = await messageService.create(newMessage);
164
+ await get().refreshMessages();
165
+
166
+ // Get the current messages to generate AI response
167
+ const messages = chatSelectors.currentChats(get());
168
+
169
+ await coreProcessMessage(messages, id);
170
+
171
+ // check activeTopic and then auto create topic
172
+ const chats = chatSelectors.currentChats(get());
173
+
174
+ const agentConfig = getAgentConfig();
175
+ // if autoCreateTopic is false, then stop
176
+ if (!agentConfig.enableAutoCreateTopic) return;
177
+
178
+ if (!activeTopicId && chats.length >= agentConfig.autoCreateTopicThreshold) {
179
+ const { saveToTopic, switchTopic } = get();
180
+ const id = await saveToTopic();
181
+ if (id) switchTopic(id);
182
+ }
183
+ },
184
+
185
+ stopGenerateMessage: () => {
186
+ const { abortController, toggleChatLoading } = get();
187
+ if (!abortController) return;
188
+
189
+ abortController.abort();
190
+
191
+ toggleChatLoading(false, undefined, t('stopGenerateMessage') as string);
192
+ },
193
+ updateInputMessage: (message) => {
194
+ set({ inputMessage: message }, false, t('updateInputMessage', message));
195
+ },
196
+ updateMessageContent: async (id, content) => {
197
+ const { dispatchMessage, refreshMessages } = get();
198
+
199
+ // Due to the async update method and refresh need about 100ms
200
+ // we need to update the message content at the frontend to avoid the update flick
201
+ // refs: https://medium.com/@kyledeguzmanx/what-are-optimistic-updates-483662c3e171
202
+ dispatchMessage({ id, key: 'content', type: 'updateMessage', value: content });
203
+
204
+ await messageService.updateMessageContent(id, content);
205
+ await refreshMessages();
206
+ },
207
+ useFetchMessages: (sessionId, activeTopicId) =>
208
+ useSWR<ChatMessage[]>(
209
+ [sessionId, activeTopicId],
210
+ async ([sessionId, topicId]: [string, string | undefined]) =>
211
+ messageService.getMessages(sessionId, topicId),
212
+ {
213
+ onSuccess: (messages, key) => {
214
+ set(
215
+ { activeId: sessionId, messages, messagesInit: true },
216
+ false,
217
+ t('useFetchMessages', {
218
+ messages,
219
+ queryKey: key,
220
+ }),
221
+ );
222
+ },
223
+ },
224
+ ),
225
+ refreshMessages: async () => {
226
+ await mutate([get().activeId, get().activeTopicId]);
227
+ },
228
+
229
+ // the internal process method of the AI message
230
+ coreProcessMessage: async (messages, userMessageId) => {
231
+ const { fetchAIChatMessage, triggerFunctionCall, refreshMessages, activeTopicId } = get();
232
+
233
+ const { model } = getAgentConfig();
131
234
 
132
- // 生成 ai message
235
+ // 1. Add an empty message to place the AI response
236
+ const assistantMessage: DB_Message = {
237
+ role: 'assistant',
238
+ content: LOADING_FLAT,
239
+ fromModel: model,
240
+
241
+ parentId: userMessageId,
242
+ sessionId: get().activeId,
243
+ topicId: activeTopicId, // if there is activeTopicId,then add it to topicId
244
+ };
245
+
246
+ const mid = await messageService.create(assistantMessage);
247
+ await refreshMessages();
248
+
249
+ // 2. fetch the AI response
133
250
  const { isFunctionCall, content, functionCallAtEnd, functionCallContent } =
134
251
  await fetchAIChatMessage(messages, mid);
135
252
 
136
- // 如果是 function,则发送函数调用方法
253
+ // 3. if it's the function call message, trigger the function method
137
254
  if (isFunctionCall) {
138
255
  let functionId = mid;
139
256
 
257
+ // if the function call is at the end of the message, then create a new function message
140
258
  if (functionCallAtEnd) {
141
259
  // create a new separate message and remove the function call from the prev message
142
- dispatchMessage({
143
- id: mid,
144
- key: 'content',
145
- type: 'updateMessage',
146
- value: content.replace(functionCallContent, ''),
147
- });
148
260
 
149
- functionId = nanoid();
150
- dispatchMessage({
151
- id: functionId,
152
- message: functionCallContent,
153
- parentId: userMessageId,
154
- role: 'assistant',
155
- type: 'addMessage',
156
- });
261
+ await messageService.updateMessageContent(mid, content.replace(functionCallContent, ''));
157
262
 
158
- // also add activeTopicId
159
- if (activeTopicId)
160
- dispatchMessage({
161
- id: functionId,
162
- key: 'topicId',
163
- type: 'updateMessage',
164
- value: activeTopicId,
165
- });
263
+ const functionMessage: CreateMessageParams = {
264
+ role: 'function',
265
+ content: functionCallContent,
266
+ extra: {
267
+ fromModel: model,
268
+ },
269
+ parentId: userMessageId,
270
+ sessionId: get().activeId,
271
+ topicId: activeTopicId,
272
+ };
273
+ functionId = await messageService.create(functionMessage);
166
274
  }
167
275
 
168
276
  triggerFunctionCall(functionId);
169
277
  }
170
278
  },
171
-
172
- deleteMessage: (id) => {
173
- get().dispatchMessage({ id, type: 'deleteMessage' });
174
- },
175
-
176
- dispatchAgentFile: (payload) => {
177
- const { activeId } = get();
178
- const session = sessionSelectors.currentSession(get());
179
- if (!activeId || !session) return;
180
-
181
- const files = filesReducer(session.files || [], payload);
182
-
183
- get().dispatchSession({ files, id: activeId, type: 'updateSessionFiles' });
184
- },
185
279
  dispatchMessage: (payload) => {
186
280
  const { activeId } = get();
187
- const session = sessionSelectors.currentSession(get());
188
- if (!activeId || !session) return;
189
281
 
190
- const chats = messagesReducer(session.chats, payload);
282
+ if (!activeId) return;
191
283
 
192
- get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' });
193
- },
284
+ const messages = messagesReducer(get().messages, payload);
194
285
 
286
+ set({ messages }, false, t(`dispatchMessage/${payload.type}`, payload));
287
+ },
195
288
  fetchAIChatMessage: async (messages, assistantId) => {
196
- const { dispatchMessage, toggleChatLoading } = get();
289
+ const { dispatchMessage, toggleChatLoading, refreshMessages } = get();
197
290
 
198
291
  const abortController = toggleChatLoading(
199
292
  true,
@@ -201,7 +294,7 @@ export const chatMessage: StateCreator<
201
294
  t('generateMessage(start)', { assistantId, messages }) as string,
202
295
  );
203
296
 
204
- const config = agentSelectors.currentAgentConfig(get());
297
+ const config = getAgentConfig();
205
298
 
206
299
  const compiler = template(config.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
207
300
 
@@ -210,7 +303,7 @@ export const chatMessage: StateCreator<
210
303
  // ================================== //
211
304
 
212
305
  // 1. slice messages with config
213
- let preprocessMsgs = getSlicedMessagesWithConfig(messages, config);
306
+ let preprocessMsgs = chatHelpers.getSlicedMessagesWithConfig(messages, config);
214
307
 
215
308
  // 2. replace inputMessage template
216
309
  preprocessMsgs = !config.inputTemplate
@@ -234,38 +327,18 @@ export const chatMessage: StateCreator<
234
327
  preprocessMsgs.unshift({ content: config.systemRole, role: 'system' } as ChatMessage);
235
328
  }
236
329
 
237
- let postMessages: OpenAIChatMessage[] = preprocessMsgs;
238
-
239
- // 4. handle content type for vision model
240
- // for the models with visual ability, add image url to content
241
- // refs: https://platform.openai.com/docs/guides/vision/quick-start
330
+ // 4. handle config for the vision model
331
+ // Due to vision model's default max_tokens is very small
332
+ // we need to set the max_tokens a larger one.
242
333
  if (VISION_MODEL_WHITE_LIST.includes(config.model)) {
243
- postMessages = preprocessMsgs.map((m) => {
244
- if (!m.files) return m;
245
-
246
- const imageList = filesSelectors.getImageUrlOrBase64ByList(m.files)(
247
- useFileStore.getState(),
248
- );
249
-
250
- if (imageList.length === 0) return m;
251
-
252
- const content: UserMessageContentPart[] = [
253
- { text: m.content, type: 'text' },
254
- ...imageList.map(
255
- (i) => ({ image_url: { detail: 'auto', url: i.url }, type: 'image_url' }) as const,
256
- ),
257
- ];
258
- return { ...m, content };
259
- });
260
-
261
- // due to vision model's default max_tokens is very small, we need to set the max_tokens a larger one.
334
+ /* eslint-disable unicorn/no-lonely-if */
262
335
  if (!config.params.max_tokens) config.params.max_tokens = VISION_MODEL_DEFAULT_MAX_TOKENS;
263
336
  }
264
337
 
265
338
  const fetcher = () =>
266
- chatService.getChatCompletion(
339
+ chatService.createAssistantMessage(
267
340
  {
268
- messages: postMessages,
341
+ messages: preprocessMsgs,
269
342
  model: config.model,
270
343
  ...config.params,
271
344
  plugins: config.plugins,
@@ -279,24 +352,29 @@ export const chatMessage: StateCreator<
279
352
  let functionCallContent = '';
280
353
 
281
354
  await fetchSSE(fetcher, {
282
- onErrorHandle: (error) => {
283
- dispatchMessage({ id: assistantId, key: 'error', type: 'updateMessage', value: error });
355
+ onErrorHandle: async (error) => {
356
+ await messageService.updateMessageError(assistantId, error);
357
+ await refreshMessages();
358
+ },
359
+ onFinish: async (content) => {
360
+ // update the content after fetch result
361
+ await messageService.updateMessageContent(assistantId, content);
284
362
  },
285
363
  onMessageHandle: (text) => {
286
364
  output += text;
287
-
365
+ // Note: Don't need change this to `messageService.updateMessageContent`
366
+ // because we don't need to update the message content to the database in the middle of the process
288
367
  dispatchMessage({ id: assistantId, key: 'content', type: 'updateMessage', value: output });
289
368
 
290
369
  // is this message is just a function call
291
- if (isFunctionMessageAtStart(output)) {
292
- isFunctionCall = true;
293
- }
370
+ if (isFunctionMessageAtStart(output)) isFunctionCall = true;
294
371
  },
295
372
  });
296
373
 
297
374
  toggleChatLoading(false, undefined, t('generateMessage(end)') as string);
298
375
 
299
- // also exist message like this: 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
376
+ // also exist message like this:
377
+ // 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
300
378
  if (!isFunctionCall) {
301
379
  const { content, valid } = testFunctionMessageAtEnd(output);
302
380
 
@@ -310,103 +388,6 @@ export const chatMessage: StateCreator<
310
388
 
311
389
  return { content: output, functionCallAtEnd, functionCallContent, isFunctionCall };
312
390
  },
313
-
314
- resendMessage: async (messageId) => {
315
- const session = sessionSelectors.currentSession(get());
316
-
317
- if (!session) return;
318
-
319
- // 1. 构造所有相关的历史记录
320
- const chats = chatSelectors.currentChats(get());
321
-
322
- const currentIndex = chats.findIndex((c) => c.id === messageId);
323
- if (currentIndex < 0) return;
324
-
325
- const currentMessage = chats[currentIndex];
326
-
327
- let contextMessages: ChatMessage[] = [];
328
-
329
- switch (currentMessage.role) {
330
- case 'function':
331
- case 'user': {
332
- contextMessages = chats.slice(0, currentIndex + 1);
333
- break;
334
- }
335
- case 'assistant': {
336
- // 消息是 AI 发出的因此需要找到它的 user 消息
337
- const userId = currentMessage.parentId;
338
- const userIndex = chats.findIndex((c) => c.id === userId);
339
- // 如果消息没有 parentId,那么同 user/function 模式
340
- contextMessages = chats.slice(0, userIndex < 0 ? currentIndex + 1 : userIndex + 1);
341
- break;
342
- }
343
- }
344
-
345
- if (contextMessages.length <= 0) return;
346
-
347
- const { coreProcessMessage } = get();
348
-
349
- const latestMsg = contextMessages.filter((s) => s.role === 'user').at(-1);
350
-
351
- if (!latestMsg) return;
352
-
353
- await coreProcessMessage(contextMessages, latestMsg.id);
354
- },
355
-
356
- sendMessage: async (message, files) => {
357
- const { dispatchMessage, dispatchAgentFile, coreProcessMessage, activeTopicId } = get();
358
- const session = sessionSelectors.currentSession(get());
359
- if (!session || !message) return;
360
-
361
- const userId = nanoid();
362
-
363
- dispatchMessage({
364
- id: userId,
365
- message: message,
366
- role: 'user',
367
- type: 'addMessage',
368
- });
369
-
370
- // if message has attached with files, then add files to message and the agent
371
- if (files && files.length > 0) {
372
- const fileIdList = files.map((f) => f.id);
373
- dispatchMessage({ id: userId, key: 'files', type: 'updateMessage', value: fileIdList });
374
-
375
- dispatchAgentFile({ files: fileIdList, type: 'addFiles' });
376
- }
377
-
378
- // if there is activeTopicId,then add topicId to message
379
- if (activeTopicId) {
380
- dispatchMessage({ id: userId, key: 'topicId', type: 'updateMessage', value: activeTopicId });
381
- }
382
-
383
- // Get the current messages to generate AI response
384
- const messages = chatSelectors.currentChats(get());
385
-
386
- await coreProcessMessage(messages, userId);
387
-
388
- // check activeTopic and then auto create topic
389
- const chats = chatSelectors.currentChats(get());
390
-
391
- const agentConfig = agentSelectors.currentAgentConfig(get());
392
- // if autoCreateTopic is false, then stop
393
- if (!agentConfig.enableAutoCreateTopic) return;
394
-
395
- if (!activeTopicId && chats.length >= agentConfig.autoCreateTopicThreshold) {
396
- const { saveToTopic, toggleTopic } = get();
397
- const id = await saveToTopic();
398
- if (id) toggleTopic(id);
399
- }
400
- },
401
-
402
- stopGenerateMessage: () => {
403
- const { abortController, toggleChatLoading } = get();
404
- if (!abortController) return;
405
-
406
- abortController.abort();
407
-
408
- toggleChatLoading(false);
409
- },
410
391
  toggleChatLoading: (loading, id, action) => {
411
392
  if (loading) {
412
393
  const abortController = new AbortController();
@@ -416,7 +397,4 @@ export const chatMessage: StateCreator<
416
397
  set({ abortController: undefined, chatLoadingId: undefined }, false, action);
417
398
  }
418
399
  },
419
- updateInputMessage: (message) => {
420
- set({ inputMessage: message }, false, t('updateInputMessage'));
421
- },
422
400
  });