@lobehub/lobehub 2.0.0-next.35 → 2.0.0-next.36

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 (154) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/next.config.ts +5 -6
  4. package/package.json +2 -2
  5. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
  6. package/packages/agent-runtime/src/core/runtime.ts +63 -18
  7. package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
  8. package/packages/agent-runtime/src/types/index.ts +1 -0
  9. package/packages/agent-runtime/src/types/instruction.ts +10 -3
  10. package/packages/const/src/user.ts +0 -1
  11. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
  12. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
  19. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
  22. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
  23. package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
  24. package/packages/conversation-flow/src/index.ts +1 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
  26. package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
  27. package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
  28. package/packages/database/src/models/message.ts +18 -19
  29. package/packages/types/src/aiChat.ts +2 -0
  30. package/packages/types/src/importer.ts +2 -2
  31. package/packages/types/src/message/ui/chat.ts +17 -1
  32. package/packages/types/src/message/ui/extra.ts +2 -2
  33. package/packages/types/src/message/ui/params.ts +2 -2
  34. package/packages/types/src/user/preference.ts +0 -4
  35. package/packages/utils/src/tokenizer/index.ts +3 -11
  36. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
  37. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
  38. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
  39. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
  40. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
  41. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
  42. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
  43. package/src/app/[variants]/(main)/labs/page.tsx +0 -9
  44. package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
  45. package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
  46. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  47. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
  48. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  49. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  50. package/src/features/Conversation/Error/index.tsx +0 -5
  51. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
  52. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
  53. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
  54. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
  55. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
  56. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
  57. package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
  58. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
  59. package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
  60. package/src/features/Conversation/Messages/Default.tsx +1 -0
  61. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
  62. package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
  63. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
  64. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
  65. package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
  66. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
  67. package/src/features/Conversation/Messages/Group/index.tsx +2 -1
  68. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
  69. package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
  70. package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
  71. package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
  72. package/src/features/Conversation/Messages/User/index.tsx +43 -44
  73. package/src/features/Conversation/Messages/index.tsx +3 -3
  74. package/src/features/Conversation/components/AutoScroll.tsx +3 -3
  75. package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
  76. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
  77. package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
  78. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
  79. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
  80. package/src/hooks/useHotkeys/chatScope.ts +15 -7
  81. package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
  82. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
  83. package/src/server/routers/lambda/aiChat.ts +3 -2
  84. package/src/server/routers/lambda/message.ts +8 -16
  85. package/src/server/services/message/__tests__/index.test.ts +29 -39
  86. package/src/server/services/message/index.ts +41 -36
  87. package/src/services/electron/desktopNotification.ts +6 -6
  88. package/src/services/electron/file.ts +6 -6
  89. package/src/services/file/ClientS3/index.ts +8 -8
  90. package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
  91. package/src/services/message/index.ts +21 -15
  92. package/src/services/upload.ts +11 -11
  93. package/src/services/utils/abortableRequest.test.ts +161 -0
  94. package/src/services/utils/abortableRequest.ts +67 -0
  95. package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
  96. package/src/store/chat/agents/createAgentExecutors.ts +395 -0
  97. package/src/store/chat/helpers.test.ts +0 -99
  98. package/src/store/chat/helpers.ts +0 -11
  99. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
  100. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
  101. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
  102. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
  103. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
  104. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
  105. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
  106. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
  107. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
  108. package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
  109. package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
  110. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
  111. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
  112. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
  113. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
  114. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
  115. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  116. package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
  117. package/src/store/chat/slices/message/action.test.ts +79 -68
  118. package/src/store/chat/slices/message/actions/index.ts +39 -0
  119. package/src/store/chat/slices/message/actions/internals.ts +77 -0
  120. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
  121. package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
  122. package/src/store/chat/slices/message/actions/query.ts +120 -0
  123. package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
  124. package/src/store/chat/slices/message/initialState.ts +13 -0
  125. package/src/store/chat/slices/message/reducer.test.ts +48 -370
  126. package/src/store/chat/slices/message/reducer.ts +17 -81
  127. package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
  128. package/src/store/chat/slices/message/selectors/chat.ts +78 -242
  129. package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
  130. package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
  131. package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
  132. package/src/store/chat/slices/plugin/action.test.ts +62 -64
  133. package/src/store/chat/slices/plugin/action.ts +34 -28
  134. package/src/store/chat/slices/thread/action.test.ts +28 -31
  135. package/src/store/chat/slices/thread/action.ts +13 -10
  136. package/src/store/chat/slices/thread/selectors/index.ts +8 -6
  137. package/src/store/chat/slices/topic/reducer.ts +11 -3
  138. package/src/store/chat/store.ts +1 -1
  139. package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
  140. package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
  141. package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
  142. package/packages/database/src/utils/groupMessages.ts +0 -361
  143. package/packages/utils/src/tokenizer/client.ts +0 -35
  144. package/packages/utils/src/tokenizer/estimated.ts +0 -4
  145. package/packages/utils/src/tokenizer/server.ts +0 -11
  146. package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
  147. package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
  148. package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
  149. package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
  150. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
  151. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
  152. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
  153. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
  154. package/src/store/chat/slices/message/action.ts +0 -629
@@ -0,0 +1,260 @@
1
+ import {
2
+ ChatErrorType,
3
+ ChatImageItem,
4
+ ChatMessageError,
5
+ ChatMessagePluginError,
6
+ CreateMessageParams,
7
+ GroundingSearch,
8
+ MessageMetadata,
9
+ MessageToolCall,
10
+ ModelReasoning,
11
+ UIChatMessage,
12
+ UpdateMessageRAGParams,
13
+ } from '@lobechat/types';
14
+ import { nanoid } from '@lobechat/utils';
15
+ import { StateCreator } from 'zustand/vanilla';
16
+
17
+ import { messageService } from '@/services/message';
18
+ import { ChatStore } from '@/store/chat/store';
19
+
20
+ /**
21
+ * Optimistic update operations
22
+ * All methods follow the pattern: update frontend first, then persist to database
23
+ */
24
+ export interface MessageOptimisticUpdateAction {
25
+ /**
26
+ * create a message with optimistic update
27
+ * returns the created message ID and updated message list
28
+ */
29
+ optimisticCreateMessage: (
30
+ params: CreateMessageParams,
31
+ context?: { groupMessageId?: string; skipRefresh?: boolean; tempMessageId?: string },
32
+ ) => Promise<{ id: string; messages: UIChatMessage[] } | undefined>;
33
+
34
+ /**
35
+ * create a temp message for optimistic update
36
+ * otherwise the message will be too slow to show
37
+ */
38
+ optimisticCreateTmpMessage: (params: CreateMessageParams) => string;
39
+
40
+ /**
41
+ * delete the message content with optimistic update
42
+ */
43
+ optimisticDeleteMessage: (id: string) => Promise<void>;
44
+
45
+ /**
46
+ * update the message content with optimistic update
47
+ * a method used by other action
48
+ */
49
+ optimisticUpdateMessageContent: (
50
+ id: string,
51
+ content: string,
52
+ extra?: {
53
+ imageList?: ChatImageItem[];
54
+ metadata?: MessageMetadata;
55
+ model?: string;
56
+ provider?: string;
57
+ reasoning?: ModelReasoning;
58
+ search?: GroundingSearch;
59
+ toolCalls?: MessageToolCall[];
60
+ },
61
+ ) => Promise<void>;
62
+
63
+ /**
64
+ * update the message error with optimistic update
65
+ */
66
+ optimisticUpdateMessageError: (id: string, error: ChatMessageError | null) => Promise<void>;
67
+
68
+ /**
69
+ * update the message metadata with optimistic update
70
+ */
71
+ optimisticUpdateMessageMetadata: (
72
+ id: string,
73
+ metadata: Partial<MessageMetadata>,
74
+ ) => Promise<void>;
75
+
76
+ /**
77
+ * update the message plugin error with optimistic update
78
+ */
79
+ optimisticUpdateMessagePluginError: (
80
+ id: string,
81
+ error: ChatMessagePluginError | null,
82
+ ) => Promise<void>;
83
+
84
+ /**
85
+ * update message RAG with optimistic update
86
+ */
87
+ optimisticUpdateMessageRAG: (id: string, input: UpdateMessageRAGParams) => Promise<void>;
88
+ }
89
+
90
+ export const messageOptimisticUpdate: StateCreator<
91
+ ChatStore,
92
+ [['zustand/devtools', never]],
93
+ [],
94
+ MessageOptimisticUpdateAction
95
+ > = (set, get) => ({
96
+ optimisticCreateMessage: async (message, context) => {
97
+ const {
98
+ optimisticCreateTmpMessage,
99
+ internal_toggleMessageLoading,
100
+ internal_dispatchMessage,
101
+ replaceMessages,
102
+ } = get();
103
+
104
+ let tempId = context?.tempMessageId;
105
+ if (!tempId) {
106
+ tempId = optimisticCreateTmpMessage(message as any);
107
+ internal_toggleMessageLoading(true, tempId);
108
+ }
109
+
110
+ try {
111
+ const result = await messageService.createMessage(message);
112
+
113
+ if (!context?.skipRefresh) {
114
+ // Use the messages returned from createMessage (already grouped)
115
+ replaceMessages(result.messages);
116
+ }
117
+
118
+ internal_toggleMessageLoading(false, tempId);
119
+ return result;
120
+ } catch (e) {
121
+ internal_toggleMessageLoading(false, tempId);
122
+ internal_dispatchMessage({
123
+ id: tempId,
124
+ type: 'updateMessage',
125
+ value: {
126
+ error: {
127
+ body: e,
128
+ message: (e as Error).message,
129
+ type: ChatErrorType.CreateMessageError,
130
+ },
131
+ },
132
+ });
133
+ }
134
+ },
135
+
136
+ optimisticCreateTmpMessage: (message) => {
137
+ const { internal_dispatchMessage } = get();
138
+
139
+ // use optimistic update to avoid the slow waiting
140
+ const tempId = 'tmp_' + nanoid();
141
+ internal_dispatchMessage({ id: tempId, type: 'createMessage', value: message });
142
+
143
+ return tempId;
144
+ },
145
+
146
+ optimisticDeleteMessage: async (id: string) => {
147
+ get().internal_dispatchMessage({ id, type: 'deleteMessage' });
148
+ const result = await messageService.removeMessage(id, {
149
+ sessionId: get().activeId,
150
+ topicId: get().activeTopicId,
151
+ });
152
+ if (result?.success && result.messages) {
153
+ get().replaceMessages(result.messages);
154
+ }
155
+ },
156
+
157
+ optimisticUpdateMessageContent: async (id, content, extra) => {
158
+ const {
159
+ internal_dispatchMessage,
160
+ refreshMessages,
161
+ internal_transformToolCalls,
162
+ replaceMessages,
163
+ } = get();
164
+
165
+ // Due to the async update method and refresh need about 100ms
166
+ // we need to update the message content at the frontend to avoid the update flick
167
+ // refs: https://medium.com/@kyledeguzmanx/what-are-optimistic-updates-483662c3e171
168
+ if (extra?.toolCalls) {
169
+ internal_dispatchMessage({
170
+ id,
171
+ type: 'updateMessage',
172
+ value: { tools: internal_transformToolCalls(extra?.toolCalls) },
173
+ });
174
+ } else {
175
+ internal_dispatchMessage({
176
+ id,
177
+ type: 'updateMessage',
178
+ value: { content },
179
+ });
180
+ }
181
+
182
+ const result = await messageService.updateMessage(
183
+ id,
184
+ {
185
+ content,
186
+ imageList: extra?.imageList,
187
+ metadata: extra?.metadata,
188
+ model: extra?.model,
189
+ provider: extra?.provider,
190
+ reasoning: extra?.reasoning,
191
+ search: extra?.search,
192
+ tools: extra?.toolCalls ? internal_transformToolCalls(extra?.toolCalls) : undefined,
193
+ },
194
+ { sessionId: get().activeId, topicId: get().activeTopicId },
195
+ );
196
+
197
+ if (result && result.success && result.messages) {
198
+ replaceMessages(result.messages);
199
+ } else {
200
+ await refreshMessages();
201
+ }
202
+ },
203
+
204
+ optimisticUpdateMessageError: async (id, error) => {
205
+ get().internal_dispatchMessage({ id, type: 'updateMessage', value: { error } });
206
+ const result = await messageService.updateMessage(
207
+ id,
208
+ { error },
209
+ { sessionId: get().activeId, topicId: get().activeTopicId },
210
+ );
211
+ if (result?.success && result.messages) {
212
+ get().replaceMessages(result.messages);
213
+ } else {
214
+ await get().refreshMessages();
215
+ }
216
+ },
217
+
218
+ optimisticUpdateMessageMetadata: async (id, metadata) => {
219
+ const { internal_dispatchMessage, refreshMessages, replaceMessages } = get();
220
+
221
+ // Optimistic update: update the frontend immediately
222
+ internal_dispatchMessage({
223
+ id,
224
+ type: 'updateMessageMetadata',
225
+ value: metadata,
226
+ });
227
+
228
+ // Persist to database
229
+ const result = await messageService.updateMessageMetadata(id, metadata, {
230
+ sessionId: get().activeId,
231
+ topicId: get().activeTopicId,
232
+ });
233
+
234
+ if (result?.success && result.messages) {
235
+ replaceMessages(result.messages);
236
+ } else {
237
+ await refreshMessages();
238
+ }
239
+ },
240
+
241
+ optimisticUpdateMessagePluginError: async (id, error) => {
242
+ const result = await messageService.updateMessagePluginError(id, error, {
243
+ sessionId: get().activeId,
244
+ topicId: get().activeTopicId,
245
+ });
246
+ if (result?.success && result.messages) {
247
+ get().replaceMessages(result.messages);
248
+ }
249
+ },
250
+
251
+ optimisticUpdateMessageRAG: async (id, data) => {
252
+ const result = await messageService.updateMessageRAG(id, data, {
253
+ sessionId: get().activeId,
254
+ topicId: get().activeTopicId,
255
+ });
256
+ if (result?.success && result.messages) {
257
+ get().replaceMessages(result.messages);
258
+ }
259
+ },
260
+ });
@@ -0,0 +1,224 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix,typescript-sort-keys/interface */
2
+ import { TraceEventType } from '@lobechat/types';
3
+ import { copyToClipboard } from '@lobehub/ui';
4
+ import isEqual from 'fast-deep-equal';
5
+ import { StateCreator } from 'zustand/vanilla';
6
+
7
+ import { messageService } from '@/services/message';
8
+ import { topicService } from '@/services/topic';
9
+ import { ChatStore } from '@/store/chat/store';
10
+ import { useSessionStore } from '@/store/session';
11
+ import { sessionSelectors } from '@/store/session/selectors';
12
+ import { setNamespace } from '@/utils/storeDebug';
13
+
14
+ import { dbMessageSelectors, displayMessageSelectors } from '../../../selectors';
15
+ import { toggleBooleanList } from '../../../utils';
16
+
17
+ const n = setNamespace('m');
18
+
19
+ /**
20
+ * Public API for components
21
+ * These methods are directly called by UI components
22
+ */
23
+ export interface MessagePublicApiAction {
24
+ // ===== Create ===== //
25
+ addAIMessage: () => Promise<void>;
26
+ addUserMessage: (params: { message: string; fileList?: string[] }) => Promise<void>;
27
+
28
+ // ===== Delete ===== //
29
+ /**
30
+ * clear message on the active session
31
+ */
32
+ clearMessage: () => Promise<void>;
33
+ deleteMessage: (id: string) => Promise<void>;
34
+ deleteToolMessage: (id: string) => Promise<void>;
35
+ clearAllMessages: () => Promise<void>;
36
+
37
+ // ===== Update ===== //
38
+ /**
39
+ * Update message input box content
40
+ */
41
+ updateMessageInput: (message: string) => void;
42
+ modifyMessageContent: (id: string, content: string) => Promise<void>;
43
+ toggleMessageEditing: (id: string, editing: boolean) => void;
44
+
45
+ // ===== Others ===== //
46
+ copyMessage: (id: string, content: string) => Promise<void>;
47
+ }
48
+
49
+ export const messagePublicApi: StateCreator<
50
+ ChatStore,
51
+ [['zustand/devtools', never]],
52
+ [],
53
+ MessagePublicApiAction
54
+ > = (set, get) => ({
55
+ addAIMessage: async () => {
56
+ const { optimisticCreateMessage, updateMessageInput, activeTopicId, activeId, inputMessage } =
57
+ get();
58
+ if (!activeId) return;
59
+
60
+ const parentId = displayMessageSelectors.lastDisplayMessageId(get());
61
+
62
+ const result = await optimisticCreateMessage({
63
+ content: inputMessage,
64
+ role: 'assistant',
65
+ sessionId: activeId,
66
+ // if there is activeTopicId,then add topicId to message
67
+ topicId: activeTopicId,
68
+ parentId,
69
+ });
70
+
71
+ if (result) {
72
+ updateMessageInput('');
73
+ }
74
+ },
75
+
76
+ addUserMessage: async ({ message, fileList }) => {
77
+ const { optimisticCreateMessage, updateMessageInput, activeTopicId, activeId, activeThreadId } =
78
+ get();
79
+ if (!activeId) return;
80
+
81
+ const parentId = displayMessageSelectors.lastDisplayMessageId(get());
82
+
83
+ const result = await optimisticCreateMessage({
84
+ content: message,
85
+ files: fileList,
86
+ role: 'user',
87
+ sessionId: activeId,
88
+ // if there is activeTopicId,then add topicId to message
89
+ topicId: activeTopicId,
90
+ threadId: activeThreadId,
91
+ parentId,
92
+ });
93
+
94
+ if (result) {
95
+ updateMessageInput('');
96
+ }
97
+ },
98
+
99
+ deleteMessage: async (id) => {
100
+ const message = displayMessageSelectors.getDisplayMessageById(id)(get());
101
+ if (!message) return;
102
+
103
+ let ids = [message.id];
104
+ const allMessages = displayMessageSelectors.activeDisplayMessages(get());
105
+
106
+ // Handle assistantGroup messages: delete all child blocks and tool results
107
+ if (message.role === 'assistantGroup' && message.children) {
108
+ // Collect all child block IDs
109
+ const childIds = message.children.map((child) => child.id);
110
+ ids = ids.concat(childIds);
111
+
112
+ // Collect all tool result IDs from children
113
+ const toolResultIds = message.children.flatMap((child) => {
114
+ if (!child.tools) return [];
115
+ return child.tools.filter((tool) => tool.result?.id).map((tool) => tool.result!.id);
116
+ });
117
+ ids = ids.concat(toolResultIds);
118
+ }
119
+ // Handle regular messages with tools: find and delete related tool messages
120
+ else if (message.tools) {
121
+ const toolMessageIds = message.tools.flatMap((tool) => {
122
+ const messages = allMessages.filter((m) => m.tool_call_id === tool.id);
123
+ return messages.map((m) => m.id);
124
+ });
125
+ ids = ids.concat(toolMessageIds);
126
+ }
127
+
128
+ get().internal_dispatchMessage({ type: 'deleteMessages', ids });
129
+ const result = await messageService.removeMessages(ids, {
130
+ sessionId: get().activeId,
131
+ topicId: get().activeTopicId,
132
+ });
133
+ if (result?.success && result.messages) {
134
+ get().replaceMessages(result.messages);
135
+ }
136
+ },
137
+
138
+ deleteToolMessage: async (id) => {
139
+ const message = dbMessageSelectors.getDbMessageById(id)(get());
140
+ if (!message || message.role !== 'tool') return;
141
+
142
+ const removeToolInAssistantMessage = async () => {
143
+ if (!message.parentId) return;
144
+ await get().internal_removeToolToAssistantMessage(message.parentId, message.tool_call_id);
145
+ };
146
+
147
+ await Promise.all([
148
+ // 1. remove tool message
149
+ get().optimisticDeleteMessage(id),
150
+ // 2. remove the tool item in the assistant tools
151
+ removeToolInAssistantMessage(),
152
+ ]);
153
+ },
154
+
155
+ clearMessage: async () => {
156
+ const { activeId, activeTopicId, refreshTopic, switchTopic, activeSessionType } = get();
157
+
158
+ // Check if this is a group session - use activeSessionType if available, otherwise check session store
159
+ let isGroupSession = activeSessionType === 'group';
160
+ if (activeSessionType === undefined) {
161
+ // Fallback: check session store directly
162
+ const sessionStore = useSessionStore.getState();
163
+ isGroupSession = sessionSelectors.isCurrentSessionGroupSession(sessionStore);
164
+ }
165
+
166
+ // For group sessions, we need to clear group messages using groupId
167
+ // For regular sessions, we clear session messages using sessionId
168
+ if (isGroupSession) {
169
+ // For group chat, activeId is the groupId
170
+ await messageService.removeMessagesByGroup(activeId, activeTopicId);
171
+ } else {
172
+ // For regular session, activeId is the sessionId
173
+ await messageService.removeMessagesByAssistant(activeId, activeTopicId);
174
+ }
175
+
176
+ if (activeTopicId) {
177
+ await topicService.removeTopic(activeTopicId);
178
+ }
179
+ await refreshTopic();
180
+
181
+ // Clear messages directly since all messages are deleted
182
+ get().replaceMessages([]);
183
+
184
+ // after remove topic , go back to default topic
185
+ switchTopic();
186
+ },
187
+
188
+ clearAllMessages: async () => {
189
+ await messageService.removeAllMessages();
190
+ // Clear messages directly since all messages are deleted
191
+ get().replaceMessages([]);
192
+ },
193
+
194
+ copyMessage: async (id, content) => {
195
+ await copyToClipboard(content);
196
+
197
+ get().internal_traceMessage(id, { eventType: TraceEventType.CopyMessage });
198
+ },
199
+
200
+ toggleMessageEditing: (id, editing) => {
201
+ set(
202
+ { messageEditingIds: toggleBooleanList(get().messageEditingIds, id, editing) },
203
+ false,
204
+ 'toggleMessageEditing',
205
+ );
206
+ },
207
+
208
+ updateMessageInput: (message) => {
209
+ if (isEqual(message, get().inputMessage)) return;
210
+
211
+ set({ inputMessage: message }, false, n('updateMessageInput', message));
212
+ },
213
+
214
+ modifyMessageContent: async (id, content) => {
215
+ // tracing the diff of update
216
+ // due to message content will change, so we need send trace before update,or will get wrong data
217
+ get().internal_traceMessage(id, {
218
+ eventType: TraceEventType.ModifyMessage,
219
+ nextContent: content,
220
+ });
221
+
222
+ await get().optimisticUpdateMessageContent(id, content);
223
+ },
224
+ });
@@ -0,0 +1,120 @@
1
+ import { parse } from '@lobechat/conversation-flow';
2
+ import { UIChatMessage } from '@lobechat/types';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { SWRResponse, mutate } from 'swr';
5
+ import { StateCreator } from 'zustand/vanilla';
6
+
7
+ import { useClientDataSWR } from '@/libs/swr';
8
+ import { messageService } from '@/services/message';
9
+ import { ChatStore } from '@/store/chat/store';
10
+ import { setNamespace } from '@/utils/storeDebug';
11
+
12
+ import { messageMapKey } from '../../../utils/messageMapKey';
13
+
14
+ const n = setNamespace('m');
15
+
16
+ const SWR_USE_FETCH_MESSAGES = 'SWR_USE_FETCH_MESSAGES';
17
+
18
+ /**
19
+ * Data query and synchronization actions
20
+ * Handles fetching, refreshing, and replacing message data
21
+ */
22
+ export interface MessageQueryAction {
23
+ /**
24
+ * Manually refresh messages from server
25
+ */
26
+ refreshMessages: () => Promise<void>;
27
+
28
+ /**
29
+ * Replace current messages with new data
30
+ */
31
+ replaceMessages: (
32
+ messages: UIChatMessage[],
33
+ params?: {
34
+ action?: any;
35
+ sessionId?: string;
36
+ topicId?: string;
37
+ },
38
+ ) => void;
39
+
40
+ /**
41
+ * Fetch messages using SWR
42
+ * @param enable - whether to enable the fetch
43
+ * @param messageContextId - Can be sessionId or groupId
44
+ */
45
+ useFetchMessages: (
46
+ enable: boolean,
47
+ messageContextId: string,
48
+ activeTopicId?: string,
49
+ type?: 'session' | 'group',
50
+ ) => SWRResponse<UIChatMessage[]>;
51
+ }
52
+
53
+ export const messageQuery: StateCreator<
54
+ ChatStore,
55
+ [['zustand/devtools', never]],
56
+ [],
57
+ MessageQueryAction
58
+ > = (set, get) => ({
59
+ // TODO: The mutate should only be called once, but since we haven't merge session and group,
60
+ // we need to call it twice
61
+ refreshMessages: async () => {
62
+ await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId, 'session']);
63
+ await mutate([SWR_USE_FETCH_MESSAGES, get().activeId, get().activeTopicId, 'group']);
64
+ },
65
+
66
+ replaceMessages: (messages, params) => {
67
+ const messagesKey = messageMapKey(
68
+ params?.sessionId ?? get().activeId,
69
+ params?.topicId ?? get().activeTopicId,
70
+ );
71
+
72
+ // Get raw messages from dbMessagesMap and apply reducer
73
+ const nextDbMap = { ...get().dbMessagesMap, [messagesKey]: messages };
74
+
75
+ if (isEqual(nextDbMap, get().dbMessagesMap)) return;
76
+
77
+ // Parse messages using conversation-flow
78
+ const { flatList } = parse(messages);
79
+
80
+ set(
81
+ {
82
+ // Store raw messages from backend
83
+ dbMessagesMap: nextDbMap,
84
+ // Store parsed messages for display
85
+ messagesMap: { ...get().messagesMap, [messagesKey]: flatList },
86
+ },
87
+ false,
88
+ params?.action ?? 'replaceMessages',
89
+ );
90
+ },
91
+
92
+ useFetchMessages: (enable, messageContextId, activeTopicId, type = 'session') =>
93
+ useClientDataSWR<UIChatMessage[]>(
94
+ enable ? [SWR_USE_FETCH_MESSAGES, messageContextId, activeTopicId, type] : null,
95
+ async ([, sessionId, topicId, type]: [string, string, string | undefined, string]) =>
96
+ type === 'session'
97
+ ? messageService.getMessages(sessionId, topicId)
98
+ : messageService.getGroupMessages(sessionId, topicId),
99
+ {
100
+ onSuccess: (messages, key) => {
101
+ const nextMap = {
102
+ ...get().dbMessagesMap,
103
+ [messageMapKey(messageContextId || '', activeTopicId)]: messages,
104
+ };
105
+
106
+ // no need to update map if the messages have been init and the map is the same
107
+ if (get().messagesInit && isEqual(nextMap, get().dbMessagesMap)) return;
108
+
109
+ set(
110
+ { messagesInit: true },
111
+ false,
112
+ n('useFetchMessages(success)', { messages, queryKey: key }),
113
+ );
114
+ get().replaceMessages(messages, {
115
+ action: n('useFetchMessages/updateMessages'),
116
+ });
117
+ },
118
+ },
119
+ ),
120
+ });
@@ -0,0 +1,108 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { ChatStore } from '@/store/chat/store';
4
+ import { Action, setNamespace } from '@/utils/storeDebug';
5
+
6
+ import type { ChatStoreState } from '../../../initialState';
7
+ import { preventLeavingFn, toggleBooleanList } from '../../../utils';
8
+
9
+ const n = setNamespace('m');
10
+
11
+ /**
12
+ * Runtime state management for message-related states
13
+ * Handles loading states, active session tracking, etc.
14
+ */
15
+ export interface MessageRuntimeStateAction {
16
+ /**
17
+ * helper to toggle the loading state of the array,used by these three toggleXXXLoading
18
+ */
19
+ internal_toggleLoadingArrays: (
20
+ key: keyof ChatStoreState,
21
+ loading: boolean,
22
+ id?: string,
23
+ action?: Action,
24
+ ) => AbortController | undefined;
25
+
26
+ /**
27
+ * method to toggle message create loading state
28
+ * the AI message status is creating -> generating
29
+ * other message role like user and tool , only this method need to be called
30
+ */
31
+ internal_toggleMessageLoading: (loading: boolean, id: string) => void;
32
+
33
+ /**
34
+ * Update active session ID with cleanup of pending operations
35
+ */
36
+ internal_updateActiveId: (activeId: string) => void;
37
+
38
+ /**
39
+ * Update active session type
40
+ */
41
+ internal_updateActiveSessionType: (sessionType?: 'agent' | 'group') => void;
42
+ }
43
+
44
+ export const messageRuntimeState: StateCreator<
45
+ ChatStore,
46
+ [['zustand/devtools', never]],
47
+ [],
48
+ MessageRuntimeStateAction
49
+ > = (set, get) => ({
50
+ internal_toggleLoadingArrays: (key, loading, id, action) => {
51
+ const abortControllerKey = `${key}AbortController`;
52
+ if (loading) {
53
+ window.addEventListener('beforeunload', preventLeavingFn);
54
+
55
+ const abortController = new AbortController();
56
+ set(
57
+ {
58
+ [abortControllerKey]: abortController,
59
+ [key]: toggleBooleanList(get()[key] as string[], id!, loading),
60
+ },
61
+ false,
62
+ action,
63
+ );
64
+
65
+ return abortController;
66
+ } else {
67
+ if (!id) {
68
+ set({ [abortControllerKey]: undefined, [key]: [] }, false, action);
69
+ } else
70
+ set(
71
+ {
72
+ [abortControllerKey]: undefined,
73
+ [key]: toggleBooleanList(get()[key] as string[], id, loading),
74
+ },
75
+ false,
76
+ action,
77
+ );
78
+
79
+ window.removeEventListener('beforeunload', preventLeavingFn);
80
+ }
81
+ },
82
+
83
+ internal_toggleMessageLoading: (loading, id) => {
84
+ set(
85
+ {
86
+ messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
87
+ },
88
+ false,
89
+ `internal_toggleMessageLoading/${loading ? 'start' : 'end'}`,
90
+ );
91
+ },
92
+
93
+ internal_updateActiveId: (activeId: string) => {
94
+ const currentActiveId = get().activeId;
95
+ if (currentActiveId === activeId) return;
96
+
97
+ // Before switching sessions, cancel all pending supervisor decisions
98
+ get().internal_cancelAllSupervisorDecisions();
99
+
100
+ set({ activeId }, false, n(`updateActiveId/${activeId}`));
101
+ },
102
+
103
+ internal_updateActiveSessionType: (sessionType?: 'agent' | 'group') => {
104
+ if (get().activeSessionType === sessionType) return;
105
+
106
+ set({ activeSessionType: sessionType }, false, n('updateActiveSessionType'));
107
+ },
108
+ });