@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/next.config.ts +5 -6
- package/package.json +2 -2
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
- package/packages/agent-runtime/src/core/runtime.ts +63 -18
- package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
- package/packages/agent-runtime/src/types/index.ts +1 -0
- package/packages/agent-runtime/src/types/instruction.ts +10 -3
- package/packages/const/src/user.ts +0 -1
- package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
- package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
- package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
- package/packages/conversation-flow/src/index.ts +1 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
- package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
- package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
- package/packages/database/src/models/message.ts +18 -19
- package/packages/types/src/aiChat.ts +2 -0
- package/packages/types/src/importer.ts +2 -2
- package/packages/types/src/message/ui/chat.ts +17 -1
- package/packages/types/src/message/ui/extra.ts +2 -2
- package/packages/types/src/message/ui/params.ts +2 -2
- package/packages/types/src/user/preference.ts +0 -4
- package/packages/utils/src/tokenizer/index.ts +3 -11
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
- package/src/app/[variants]/(main)/labs/page.tsx +0 -9
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
- package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
- package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
- package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
- package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
- package/src/features/Conversation/Error/index.tsx +0 -5
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
- package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
- package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
- package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
- package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
- package/src/features/Conversation/Messages/Default.tsx +1 -0
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
- package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
- package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
- package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
- package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
- package/src/features/Conversation/Messages/Group/index.tsx +2 -1
- package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
- package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
- package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
- package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
- package/src/features/Conversation/Messages/User/index.tsx +43 -44
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +3 -3
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
- package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
- package/src/hooks/useHotkeys/chatScope.ts +15 -7
- package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
- package/src/server/routers/lambda/aiChat.ts +3 -2
- package/src/server/routers/lambda/message.ts +8 -16
- package/src/server/services/message/__tests__/index.test.ts +29 -39
- package/src/server/services/message/index.ts +41 -36
- package/src/services/electron/desktopNotification.ts +6 -6
- package/src/services/electron/file.ts +6 -6
- package/src/services/file/ClientS3/index.ts +8 -8
- package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
- package/src/services/message/index.ts +21 -15
- package/src/services/upload.ts +11 -11
- package/src/services/utils/abortableRequest.test.ts +161 -0
- package/src/services/utils/abortableRequest.ts +67 -0
- package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
- package/src/store/chat/agents/createAgentExecutors.ts +395 -0
- package/src/store/chat/helpers.test.ts +0 -99
- package/src/store/chat/helpers.ts +0 -11
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
- package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
- package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
- package/src/store/chat/slices/message/action.test.ts +79 -68
- package/src/store/chat/slices/message/actions/index.ts +39 -0
- package/src/store/chat/slices/message/actions/internals.ts +77 -0
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
- package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
- package/src/store/chat/slices/message/actions/query.ts +120 -0
- package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
- package/src/store/chat/slices/message/initialState.ts +13 -0
- package/src/store/chat/slices/message/reducer.test.ts +48 -370
- package/src/store/chat/slices/message/reducer.ts +17 -81
- package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
- package/src/store/chat/slices/message/selectors/chat.ts +78 -242
- package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
- package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
- package/src/store/chat/slices/plugin/action.test.ts +62 -64
- package/src/store/chat/slices/plugin/action.ts +34 -28
- package/src/store/chat/slices/thread/action.test.ts +28 -31
- package/src/store/chat/slices/thread/action.ts +13 -10
- package/src/store/chat/slices/thread/selectors/index.ts +8 -6
- package/src/store/chat/slices/topic/reducer.ts +11 -3
- package/src/store/chat/store.ts +1 -1
- package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
- package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
- package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
- package/packages/database/src/utils/groupMessages.ts +0 -361
- package/packages/utils/src/tokenizer/client.ts +0 -35
- package/packages/utils/src/tokenizer/estimated.ts +0 -4
- package/packages/utils/src/tokenizer/server.ts +0 -11
- package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
- package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
- package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
- 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
|
+
});
|