@lobehub/chat 0.159.11 → 0.159.12
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/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/package.json +1 -1
- package/src/app/(main)/chat/@session/features/SessionHydration.tsx +2 -0
- package/src/features/Conversation/Error/index.tsx +13 -4
- package/src/features/Conversation/components/VirtualizedList/index.tsx +27 -21
- package/src/services/message/type.ts +6 -3
- package/src/store/chat/slices/enchance/action.test.ts +16 -12
- package/src/store/chat/slices/message/action.test.ts +495 -24
- package/src/store/chat/slices/message/action.ts +143 -32
- package/src/store/chat/slices/message/initialState.ts +2 -2
- package/src/store/chat/slices/message/selectors.test.ts +39 -9
- package/src/store/chat/slices/message/selectors.ts +13 -3
- package/src/store/chat/slices/message/utils.ts +7 -0
- package/src/store/chat/slices/plugin/action.test.ts +7 -2
- package/src/store/chat/slices/share/action.test.ts +19 -3
- package/src/store/chat/slices/topic/action.test.ts +13 -2
- package/src/store/chat/slices/topic/action.ts +27 -6
- package/src/store/chat/slices/topic/initialState.ts +2 -0
- package/src/utils/fetch.test.ts +17 -11
- package/src/utils/fetch.ts +20 -22
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
2
|
// Disable the auto sort key eslint rule to make the code more logic and readable
|
|
3
3
|
import { copyToClipboard } from '@lobehub/ui';
|
|
4
|
+
import isEqual from 'fast-deep-equal';
|
|
4
5
|
import { produce } from 'immer';
|
|
5
6
|
import { template } from 'lodash-es';
|
|
6
7
|
import { SWRResponse, mutate } from 'swr';
|
|
@@ -16,16 +17,17 @@ import { traceService } from '@/services/trace';
|
|
|
16
17
|
import { useAgentStore } from '@/store/agent';
|
|
17
18
|
import { agentSelectors } from '@/store/agent/selectors';
|
|
18
19
|
import { chatHelpers } from '@/store/chat/helpers';
|
|
20
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
19
21
|
import { ChatStore } from '@/store/chat/store';
|
|
20
22
|
import { ChatMessage, MessageToolCall } from '@/types/message';
|
|
21
23
|
import { TraceEventPayloads } from '@/types/trace';
|
|
22
24
|
import { setNamespace } from '@/utils/storeDebug';
|
|
23
25
|
import { nanoid } from '@/utils/uuid';
|
|
24
26
|
|
|
25
|
-
import { chatSelectors } from '../../selectors';
|
|
27
|
+
import { chatSelectors, topicSelectors } from '../../selectors';
|
|
26
28
|
import { MessageDispatch, messagesReducer } from './reducer';
|
|
27
29
|
|
|
28
|
-
const n = setNamespace('
|
|
30
|
+
const n = setNamespace('m');
|
|
29
31
|
|
|
30
32
|
const SWR_USE_FETCH_MESSAGES = 'SWR_USE_FETCH_MESSAGES';
|
|
31
33
|
|
|
@@ -121,7 +123,12 @@ export interface ChatMessageAction {
|
|
|
121
123
|
content: string,
|
|
122
124
|
toolCalls?: MessageToolCall[],
|
|
123
125
|
) => Promise<void>;
|
|
124
|
-
internal_createMessage: (
|
|
126
|
+
internal_createMessage: (
|
|
127
|
+
params: CreateMessageParams,
|
|
128
|
+
context?: { tempMessageId?: string; skipRefresh?: boolean },
|
|
129
|
+
) => Promise<string>;
|
|
130
|
+
internal_createTmpMessage: (params: CreateMessageParams) => string;
|
|
131
|
+
internal_fetchMessages: () => Promise<void>;
|
|
125
132
|
internal_resendMessage: (id: string, traceId?: string) => Promise<void>;
|
|
126
133
|
internal_traceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
|
|
127
134
|
}
|
|
@@ -166,7 +173,9 @@ export const chatMessage: StateCreator<
|
|
|
166
173
|
if (message.tools) {
|
|
167
174
|
const pools = message.tools
|
|
168
175
|
.flatMap((tool) => {
|
|
169
|
-
const messages =
|
|
176
|
+
const messages = chatSelectors
|
|
177
|
+
.currentChats(get())
|
|
178
|
+
.filter((m) => m.tool_call_id === tool.id);
|
|
170
179
|
|
|
171
180
|
return messages.map((m) => m.id);
|
|
172
181
|
})
|
|
@@ -218,8 +227,10 @@ export const chatMessage: StateCreator<
|
|
|
218
227
|
|
|
219
228
|
const fileIdList = files?.map((f) => f.id);
|
|
220
229
|
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
const isNoFile = !fileIdList || fileIdList.length === 0;
|
|
231
|
+
|
|
232
|
+
// if message is empty or no files, then stop
|
|
233
|
+
if (!message && isNoFile) return;
|
|
223
234
|
|
|
224
235
|
const newMessage: CreateMessageParams = {
|
|
225
236
|
content: message,
|
|
@@ -231,27 +242,91 @@ export const chatMessage: StateCreator<
|
|
|
231
242
|
topicId: activeTopicId,
|
|
232
243
|
};
|
|
233
244
|
|
|
234
|
-
const
|
|
245
|
+
const agentConfig = getAgentConfig();
|
|
246
|
+
|
|
247
|
+
let tempMessageId: string | undefined = undefined;
|
|
248
|
+
let newTopicId: string | undefined = undefined;
|
|
249
|
+
|
|
250
|
+
// it should be the default topic, then
|
|
251
|
+
// if autoCreateTopic is enabled, check to whether we need to create a topic
|
|
252
|
+
if (!onlyAddUserMessage && !activeTopicId && agentConfig.enableAutoCreateTopic) {
|
|
253
|
+
// check activeTopic and then auto create topic
|
|
254
|
+
const chats = chatSelectors.currentChats(get());
|
|
255
|
+
|
|
256
|
+
// we will add two messages (user and assistant), so the finial length should +2
|
|
257
|
+
const featureLength = chats.length + 2;
|
|
258
|
+
|
|
259
|
+
// if there is no activeTopicId and the feature length is greater than the threshold
|
|
260
|
+
// then create a new topic and active it
|
|
261
|
+
if (!get().activeTopicId && featureLength >= agentConfig.autoCreateTopicThreshold) {
|
|
262
|
+
// we need to create a temp message for optimistic update
|
|
263
|
+
tempMessageId = get().internal_createTmpMessage(newMessage);
|
|
264
|
+
get().internal_toggleMessageLoading(true, tempMessageId);
|
|
265
|
+
|
|
266
|
+
const topicId = await get().createTopic();
|
|
267
|
+
|
|
268
|
+
if (topicId) {
|
|
269
|
+
newTopicId = topicId;
|
|
270
|
+
newMessage.topicId = topicId;
|
|
271
|
+
|
|
272
|
+
// we need to copy the messages to the new topic or the message will disappear
|
|
273
|
+
const mapKey = chatSelectors.currentChatKey(get());
|
|
274
|
+
const newMaps = {
|
|
275
|
+
...get().messagesMap,
|
|
276
|
+
[messageMapKey(activeId, topicId)]: get().messagesMap[mapKey],
|
|
277
|
+
};
|
|
278
|
+
set({ messagesMap: newMaps }, false, 'internal_copyMessages');
|
|
279
|
+
|
|
280
|
+
// get().internal_dispatchMessage({ type: 'deleteMessage', id: tempMessageId });
|
|
281
|
+
get().internal_toggleMessageLoading(false, tempMessageId);
|
|
282
|
+
|
|
283
|
+
// make the topic loading
|
|
284
|
+
get().internal_updateTopicLoading(topicId, true);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const id = await get().internal_createMessage(newMessage, {
|
|
290
|
+
tempMessageId,
|
|
291
|
+
skipRefresh: !onlyAddUserMessage,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// switch to the new topic if create the new topic
|
|
295
|
+
if (!!newTopicId) {
|
|
296
|
+
await get().switchTopic(newTopicId, true);
|
|
297
|
+
await get().internal_fetchMessages();
|
|
298
|
+
|
|
299
|
+
// delete previous messages
|
|
300
|
+
// remove the temp message map
|
|
301
|
+
const newMaps = { ...get().messagesMap, [messageMapKey(activeId, null)]: [] };
|
|
302
|
+
set({ messagesMap: newMaps }, false, 'internal_copyMessages');
|
|
303
|
+
}
|
|
235
304
|
|
|
236
305
|
// if only add user message, then stop
|
|
237
|
-
if (onlyAddUserMessage)
|
|
306
|
+
if (onlyAddUserMessage) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
238
309
|
|
|
239
310
|
// Get the current messages to generate AI response
|
|
240
311
|
const messages = chatSelectors.currentChats(get());
|
|
241
312
|
|
|
242
313
|
await internal_coreProcessMessage(messages, id, { isWelcomeQuestion });
|
|
243
314
|
|
|
244
|
-
// check activeTopic and then auto create topic
|
|
245
|
-
const chats = chatSelectors.currentChats(get());
|
|
246
|
-
|
|
247
|
-
const agentConfig = getAgentConfig();
|
|
248
315
|
// if autoCreateTopic is false, then stop
|
|
249
316
|
if (!agentConfig.enableAutoCreateTopic) return;
|
|
250
317
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
318
|
+
// check activeTopic and then auto update topic title
|
|
319
|
+
if (newTopicId) {
|
|
320
|
+
const chats = chatSelectors.currentChats(get());
|
|
321
|
+
await get().summaryTopicTitle(newTopicId, chats);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const topic = topicSelectors.currentActiveTopic(get());
|
|
326
|
+
|
|
327
|
+
if (topic && !topic.title) {
|
|
328
|
+
const chats = chatSelectors.currentChats(get());
|
|
329
|
+
await get().summaryTopicTitle(topic.id, chats);
|
|
255
330
|
}
|
|
256
331
|
},
|
|
257
332
|
addAIMessage: async () => {
|
|
@@ -289,7 +364,10 @@ export const chatMessage: StateCreator<
|
|
|
289
364
|
|
|
290
365
|
internal_toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
|
|
291
366
|
},
|
|
367
|
+
|
|
292
368
|
updateInputMessage: (message) => {
|
|
369
|
+
if (isEqual(message, get().inputMessage)) return;
|
|
370
|
+
|
|
293
371
|
set({ inputMessage: message }, false, n('updateInputMessage', message));
|
|
294
372
|
},
|
|
295
373
|
modifyMessageContent: async (id, content) => {
|
|
@@ -308,16 +386,18 @@ export const chatMessage: StateCreator<
|
|
|
308
386
|
async ([, sessionId, topicId]: [string, string, string | undefined]) =>
|
|
309
387
|
messageService.getMessages(sessionId, topicId),
|
|
310
388
|
{
|
|
311
|
-
suspense: true,
|
|
312
|
-
fallbackData: [],
|
|
313
389
|
onSuccess: (messages, key) => {
|
|
390
|
+
const nextMap = {
|
|
391
|
+
...get().messagesMap,
|
|
392
|
+
[messageMapKey(sessionId, activeTopicId)]: messages,
|
|
393
|
+
};
|
|
394
|
+
// no need to update map if the messages have been init and the map is the same
|
|
395
|
+
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
|
|
396
|
+
|
|
314
397
|
set(
|
|
315
|
-
{
|
|
398
|
+
{ messagesInit: true, messagesMap: nextMap },
|
|
316
399
|
false,
|
|
317
|
-
n('useFetchMessages', {
|
|
318
|
-
messages,
|
|
319
|
-
queryKey: key,
|
|
320
|
-
}),
|
|
400
|
+
n('useFetchMessages', { messages, queryKey: key }),
|
|
321
401
|
);
|
|
322
402
|
},
|
|
323
403
|
},
|
|
@@ -360,9 +440,13 @@ export const chatMessage: StateCreator<
|
|
|
360
440
|
|
|
361
441
|
if (!activeId) return;
|
|
362
442
|
|
|
363
|
-
const messages = messagesReducer(get()
|
|
443
|
+
const messages = messagesReducer(chatSelectors.currentChats(get()), payload);
|
|
444
|
+
|
|
445
|
+
const nextMap = { ...get().messagesMap, [chatSelectors.currentChatKey(get())]: messages };
|
|
364
446
|
|
|
365
|
-
|
|
447
|
+
if (isEqual(nextMap, get().messagesMap)) return;
|
|
448
|
+
|
|
449
|
+
set({ messagesMap: nextMap }, false, { type: `dispatchMessage/${payload.type}`, payload });
|
|
366
450
|
},
|
|
367
451
|
internal_fetchAIChatMessage: async (messages, assistantId, params) => {
|
|
368
452
|
const {
|
|
@@ -616,22 +700,49 @@ export const chatMessage: StateCreator<
|
|
|
616
700
|
await refreshMessages();
|
|
617
701
|
},
|
|
618
702
|
|
|
619
|
-
internal_createMessage: async (message) => {
|
|
620
|
-
const {
|
|
703
|
+
internal_createMessage: async (message, context) => {
|
|
704
|
+
const { internal_createTmpMessage, refreshMessages, internal_toggleMessageLoading } = get();
|
|
705
|
+
let tempId = context?.tempMessageId;
|
|
621
706
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
707
|
+
if (!tempId) {
|
|
708
|
+
// use optimistic update to avoid the slow waiting
|
|
709
|
+
tempId = internal_createTmpMessage(message);
|
|
710
|
+
|
|
711
|
+
internal_toggleMessageLoading(true, tempId);
|
|
712
|
+
}
|
|
625
713
|
|
|
626
|
-
internal_toggleMessageLoading(true, tempId);
|
|
627
714
|
const id = await messageService.createMessage(message);
|
|
715
|
+
if (!context?.skipRefresh) {
|
|
716
|
+
await refreshMessages();
|
|
717
|
+
}
|
|
628
718
|
|
|
629
|
-
await refreshMessages();
|
|
630
719
|
internal_toggleMessageLoading(false, tempId);
|
|
631
720
|
|
|
632
721
|
return id;
|
|
633
722
|
},
|
|
634
723
|
|
|
724
|
+
internal_fetchMessages: async () => {
|
|
725
|
+
const messages = await messageService.getMessages(get().activeId, get().activeTopicId);
|
|
726
|
+
const nextMap = { ...get().messagesMap, [chatSelectors.currentChatKey(get())]: messages };
|
|
727
|
+
// no need to update map if the messages have been init and the map is the same
|
|
728
|
+
if (get().messagesInit && isEqual(nextMap, get().messagesMap)) return;
|
|
729
|
+
|
|
730
|
+
set(
|
|
731
|
+
{ messagesInit: true, messagesMap: nextMap },
|
|
732
|
+
false,
|
|
733
|
+
n('internal_fetchMessages', { messages }),
|
|
734
|
+
);
|
|
735
|
+
},
|
|
736
|
+
internal_createTmpMessage: (message) => {
|
|
737
|
+
const { internal_dispatchMessage } = get();
|
|
738
|
+
|
|
739
|
+
// use optimistic update to avoid the slow waiting
|
|
740
|
+
const tempId = 'tmp_' + nanoid();
|
|
741
|
+
internal_dispatchMessage({ type: 'createMessage', id: tempId, value: message });
|
|
742
|
+
|
|
743
|
+
return tempId;
|
|
744
|
+
},
|
|
745
|
+
|
|
635
746
|
internal_traceMessage: async (id, payload) => {
|
|
636
747
|
// tracing the diff of update
|
|
637
748
|
const message = chatSelectors.getMessageById(id)(get());
|
|
@@ -20,11 +20,11 @@ export interface ChatMessageState {
|
|
|
20
20
|
* is the message is creating or updating in the service
|
|
21
21
|
*/
|
|
22
22
|
messageLoadingIds: string[];
|
|
23
|
-
messages: ChatMessage[];
|
|
24
23
|
/**
|
|
25
24
|
* whether messages have fetched
|
|
26
25
|
*/
|
|
27
26
|
messagesInit: boolean;
|
|
27
|
+
messagesMap: Record<string, ChatMessage[]>;
|
|
28
28
|
/**
|
|
29
29
|
* the tool calling stream ids
|
|
30
30
|
*/
|
|
@@ -37,7 +37,7 @@ export const initialMessageState: ChatMessageState = {
|
|
|
37
37
|
inputMessage: '',
|
|
38
38
|
messageEditingIds: [],
|
|
39
39
|
messageLoadingIds: [],
|
|
40
|
-
messages: [],
|
|
41
40
|
messagesInit: false,
|
|
41
|
+
messagesMap: {},
|
|
42
42
|
toolCallingStreamIds: {},
|
|
43
43
|
};
|
|
@@ -6,6 +6,7 @@ import { INBOX_SESSION_ID } from '@/const/session';
|
|
|
6
6
|
import { useAgentStore } from '@/store/agent';
|
|
7
7
|
import { ChatStore } from '@/store/chat';
|
|
8
8
|
import { initialState } from '@/store/chat/initialState';
|
|
9
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
9
10
|
import { useSessionStore } from '@/store/session';
|
|
10
11
|
import { useUserStore } from '@/store/user';
|
|
11
12
|
import { LobeAgentConfig } from '@/types/agent';
|
|
@@ -87,7 +88,12 @@ const mockedChats = [
|
|
|
87
88
|
},
|
|
88
89
|
] as ChatMessage[];
|
|
89
90
|
|
|
90
|
-
const mockChatStore = {
|
|
91
|
+
const mockChatStore = {
|
|
92
|
+
messagesMap: {
|
|
93
|
+
[messageMapKey('abc')]: mockMessages,
|
|
94
|
+
},
|
|
95
|
+
activeId: 'abc',
|
|
96
|
+
} as ChatStore;
|
|
91
97
|
|
|
92
98
|
describe('chatSelectors', () => {
|
|
93
99
|
describe('getMessageById', () => {
|
|
@@ -97,14 +103,19 @@ describe('chatSelectors', () => {
|
|
|
97
103
|
});
|
|
98
104
|
|
|
99
105
|
it('should return the message object with the matching id', () => {
|
|
100
|
-
const state = merge(initialStore, {
|
|
106
|
+
const state = merge(initialStore, {
|
|
107
|
+
messagesMap: {
|
|
108
|
+
[messageMapKey('abc')]: mockMessages,
|
|
109
|
+
},
|
|
110
|
+
activeId: 'abc',
|
|
111
|
+
});
|
|
101
112
|
const message = chatSelectors.getMessageById('msg1')(state);
|
|
102
|
-
expect(message).toEqual(
|
|
113
|
+
expect(message).toEqual(mockedChats[0]);
|
|
103
114
|
});
|
|
104
115
|
|
|
105
116
|
it('should return the message with the matching id', () => {
|
|
106
117
|
const message = chatSelectors.getMessageById('msg1')(mockChatStore);
|
|
107
|
-
expect(message).toEqual(
|
|
118
|
+
expect(message).toEqual(mockedChats[0]);
|
|
108
119
|
});
|
|
109
120
|
|
|
110
121
|
it('should return undefined if no message matches the id', () => {
|
|
@@ -115,14 +126,24 @@ describe('chatSelectors', () => {
|
|
|
115
126
|
|
|
116
127
|
describe('currentChatsWithHistoryConfig', () => {
|
|
117
128
|
it('should slice the messages according to the current agent config', () => {
|
|
118
|
-
const state = merge(initialStore, {
|
|
129
|
+
const state = merge(initialStore, {
|
|
130
|
+
messagesMap: {
|
|
131
|
+
[messageMapKey('abc')]: mockMessages,
|
|
132
|
+
},
|
|
133
|
+
activeId: 'abc',
|
|
134
|
+
});
|
|
119
135
|
|
|
120
136
|
const chats = chatSelectors.currentChatsWithHistoryConfig(state);
|
|
121
137
|
expect(chats).toHaveLength(3);
|
|
122
138
|
expect(chats).toEqual(mockedChats);
|
|
123
139
|
});
|
|
124
140
|
it('should slice the messages according to config, assuming historyCount is mocked to 2', async () => {
|
|
125
|
-
const state = merge(initialStore, {
|
|
141
|
+
const state = merge(initialStore, {
|
|
142
|
+
messagesMap: {
|
|
143
|
+
[messageMapKey('abc')]: mockMessages,
|
|
144
|
+
},
|
|
145
|
+
activeId: 'abc',
|
|
146
|
+
});
|
|
126
147
|
act(() => {
|
|
127
148
|
useAgentStore.setState({
|
|
128
149
|
activeId: 'inbox',
|
|
@@ -172,7 +193,12 @@ describe('chatSelectors', () => {
|
|
|
172
193
|
|
|
173
194
|
describe('currentChatsWithGuideMessage', () => {
|
|
174
195
|
it('should return existing messages if there are any', () => {
|
|
175
|
-
const state = merge(initialStore, {
|
|
196
|
+
const state = merge(initialStore, {
|
|
197
|
+
messagesMap: {
|
|
198
|
+
[messageMapKey('someActiveId')]: mockMessages,
|
|
199
|
+
},
|
|
200
|
+
activeId: 'someActiveId',
|
|
201
|
+
});
|
|
176
202
|
const chats = chatSelectors.currentChatsWithGuideMessage({} as MetaData)(state);
|
|
177
203
|
expect(chats).toEqual(mockedChats);
|
|
178
204
|
});
|
|
@@ -212,7 +238,9 @@ describe('chatSelectors', () => {
|
|
|
212
238
|
it('should concatenate the contents of all messages returned by currentChatsWithHistoryConfig', () => {
|
|
213
239
|
// Prepare a state with a few messages
|
|
214
240
|
const state = merge(initialStore, {
|
|
215
|
-
|
|
241
|
+
messagesMap: {
|
|
242
|
+
[messageMapKey('active-session')]: mockMessages,
|
|
243
|
+
},
|
|
216
244
|
activeId: 'active-session',
|
|
217
245
|
});
|
|
218
246
|
|
|
@@ -241,7 +269,9 @@ describe('chatSelectors', () => {
|
|
|
241
269
|
it('should return false if there are existing messages in the inbox session', () => {
|
|
242
270
|
const state = merge(initialStore, {
|
|
243
271
|
activeId: INBOX_SESSION_ID,
|
|
244
|
-
|
|
272
|
+
messagesMap: {
|
|
273
|
+
[messageMapKey('inbox')]: mockMessages,
|
|
274
|
+
},
|
|
245
275
|
});
|
|
246
276
|
const result = chatSelectors.showInboxWelcome(state);
|
|
247
277
|
expect(result).toBe(false);
|
|
@@ -4,6 +4,7 @@ import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta';
|
|
|
4
4
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
5
5
|
import { useAgentStore } from '@/store/agent';
|
|
6
6
|
import { agentSelectors } from '@/store/agent/selectors';
|
|
7
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
7
8
|
import { useSessionStore } from '@/store/session';
|
|
8
9
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
|
9
10
|
import { useUserStore } from '@/store/user';
|
|
@@ -33,13 +34,15 @@ const getMeta = (message: ChatMessage) => {
|
|
|
33
34
|
}
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
const currentChatKey = (s: ChatStore) =>
|
|
37
|
+
const currentChatKey = (s: ChatStore) => messageMapKey(s.activeId, s.activeTopicId);
|
|
37
38
|
|
|
38
39
|
// 当前激活的消息列表
|
|
39
40
|
const currentChats = (s: ChatStore): ChatMessage[] => {
|
|
40
41
|
if (!s.activeId) return [];
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
const messages = s.messagesMap[currentChatKey(s)] || [];
|
|
44
|
+
|
|
45
|
+
return messages.map((i) => ({ ...i, meta: getMeta(i) }));
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
const initTime = Date.now();
|
|
@@ -47,8 +50,10 @@ const initTime = Date.now();
|
|
|
47
50
|
const showInboxWelcome = (s: ChatStore): boolean => {
|
|
48
51
|
const isInbox = s.activeId === INBOX_SESSION_ID;
|
|
49
52
|
if (!isInbox) return false;
|
|
53
|
+
|
|
50
54
|
const data = currentChats(s);
|
|
51
55
|
const isBrandNewChat = data.length === 0;
|
|
56
|
+
|
|
52
57
|
return isBrandNewChat;
|
|
53
58
|
};
|
|
54
59
|
|
|
@@ -107,13 +112,17 @@ const chatsMessageString = (s: ChatStore): string => {
|
|
|
107
112
|
return chats.map((m) => m.content).join('');
|
|
108
113
|
};
|
|
109
114
|
|
|
110
|
-
const getMessageById = (id: string) => (s: ChatStore) =>
|
|
115
|
+
const getMessageById = (id: string) => (s: ChatStore) =>
|
|
116
|
+
chatHelpers.getMessageById(currentChats(s), id);
|
|
117
|
+
|
|
111
118
|
const getTraceIdByMessageId = (id: string) => (s: ChatStore) => getMessageById(id)(s)?.traceId;
|
|
112
119
|
|
|
113
120
|
const latestMessage = (s: ChatStore) => currentChats(s).at(-1);
|
|
114
121
|
|
|
115
122
|
const currentChatLoadingState = (s: ChatStore) => !s.messagesInit;
|
|
116
123
|
|
|
124
|
+
const isCurrentChatLoaded = (s: ChatStore) => !!s.messagesMap[currentChatKey(s)];
|
|
125
|
+
|
|
117
126
|
const isMessageEditing = (id: string) => (s: ChatStore) => s.messageEditingIds.includes(id);
|
|
118
127
|
const isMessageLoading = (id: string) => (s: ChatStore) => s.messageLoadingIds.includes(id);
|
|
119
128
|
const isMessageGenerating = (id: string) => (s: ChatStore) => s.chatLoadingIds.includes(id);
|
|
@@ -137,6 +146,7 @@ export const chatSelectors = {
|
|
|
137
146
|
getMessageById,
|
|
138
147
|
getTraceIdByMessageId,
|
|
139
148
|
isAIGenerating,
|
|
149
|
+
isCurrentChatLoaded,
|
|
140
150
|
isMessageEditing,
|
|
141
151
|
isMessageGenerating,
|
|
142
152
|
isMessageLoading,
|
|
@@ -7,6 +7,7 @@ import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/p
|
|
|
7
7
|
import { chatService } from '@/services/chat';
|
|
8
8
|
import { messageService } from '@/services/message';
|
|
9
9
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
10
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
10
11
|
import { useChatStore } from '@/store/chat/store';
|
|
11
12
|
import { useToolStore } from '@/store/tool';
|
|
12
13
|
import { ChatMessage, ChatToolPayload } from '@/types/message';
|
|
@@ -224,7 +225,9 @@ describe('ChatPluginAction', () => {
|
|
|
224
225
|
|
|
225
226
|
act(() => {
|
|
226
227
|
useChatStore.setState({
|
|
227
|
-
|
|
228
|
+
messagesMap: {
|
|
229
|
+
[messageMapKey('session-id', 'topic-id')]: [message],
|
|
230
|
+
},
|
|
228
231
|
invokeStandaloneTypePlugin: invokeStandaloneTypePluginMock,
|
|
229
232
|
invokeMarkdownTypePlugin: invokeMarkdownTypePluginMock,
|
|
230
233
|
invokeBuiltinTool: invokeBuiltinToolMock,
|
|
@@ -320,7 +323,9 @@ describe('ChatPluginAction', () => {
|
|
|
320
323
|
triggerAIMessage: triggerAIMessageMock,
|
|
321
324
|
internal_createMessage: internal_createMessageMock,
|
|
322
325
|
activeId: 'session-id',
|
|
323
|
-
|
|
326
|
+
messagesMap: {
|
|
327
|
+
[messageMapKey('session-id', 'topic-id')]: [message],
|
|
328
|
+
},
|
|
324
329
|
activeTopicId: 'topic-id',
|
|
325
330
|
});
|
|
326
331
|
});
|
|
@@ -3,6 +3,7 @@ import { act, renderHook } from '@testing-library/react';
|
|
|
3
3
|
import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
|
|
4
4
|
import { shareService } from '@/services/share';
|
|
5
5
|
import { useChatStore } from '@/store/chat';
|
|
6
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
6
7
|
import { ChatMessage } from '@/types/message';
|
|
7
8
|
|
|
8
9
|
describe('shareSlice actions', () => {
|
|
@@ -97,7 +98,12 @@ describe('shareSlice actions', () => {
|
|
|
97
98
|
} as ChatMessage;
|
|
98
99
|
|
|
99
100
|
act(() => {
|
|
100
|
-
useChatStore.setState({
|
|
101
|
+
useChatStore.setState({
|
|
102
|
+
messagesMap: {
|
|
103
|
+
[messageMapKey('abc')]: [pluginMessage],
|
|
104
|
+
},
|
|
105
|
+
activeId: 'abc',
|
|
106
|
+
});
|
|
101
107
|
});
|
|
102
108
|
|
|
103
109
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -130,7 +136,12 @@ describe('shareSlice actions', () => {
|
|
|
130
136
|
} as ChatMessage;
|
|
131
137
|
|
|
132
138
|
act(() => {
|
|
133
|
-
useChatStore.setState({
|
|
139
|
+
useChatStore.setState({
|
|
140
|
+
messagesMap: {
|
|
141
|
+
[messageMapKey('abc')]: [pluginMessage],
|
|
142
|
+
},
|
|
143
|
+
activeId: 'abc',
|
|
144
|
+
});
|
|
134
145
|
});
|
|
135
146
|
|
|
136
147
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -167,7 +178,12 @@ describe('shareSlice actions', () => {
|
|
|
167
178
|
] as ChatMessage[];
|
|
168
179
|
|
|
169
180
|
act(() => {
|
|
170
|
-
useChatStore.setState({
|
|
181
|
+
useChatStore.setState({
|
|
182
|
+
messagesMap: {
|
|
183
|
+
[messageMapKey('abc')]: messages,
|
|
184
|
+
},
|
|
185
|
+
activeId: 'abc',
|
|
186
|
+
});
|
|
171
187
|
});
|
|
172
188
|
|
|
173
189
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -6,6 +6,7 @@ import { LOADING_FLAT } from '@/const/message';
|
|
|
6
6
|
import { chatService } from '@/services/chat';
|
|
7
7
|
import { messageService } from '@/services/message';
|
|
8
8
|
import { topicService } from '@/services/topic';
|
|
9
|
+
import { messageMapKey } from '@/store/chat/slices/message/utils';
|
|
9
10
|
import { ChatMessage } from '@/types/message';
|
|
10
11
|
import { ChatTopic } from '@/types/topic';
|
|
11
12
|
|
|
@@ -87,7 +88,12 @@ describe('topic action', () => {
|
|
|
87
88
|
it('should not create a topic if there are no messages', async () => {
|
|
88
89
|
const { result } = renderHook(() => useChatStore());
|
|
89
90
|
act(() => {
|
|
90
|
-
useChatStore.setState({
|
|
91
|
+
useChatStore.setState({
|
|
92
|
+
messagesMap: {
|
|
93
|
+
[messageMapKey('session')]: [],
|
|
94
|
+
},
|
|
95
|
+
activeId: 'session',
|
|
96
|
+
});
|
|
91
97
|
});
|
|
92
98
|
|
|
93
99
|
const createTopicSpy = vi.spyOn(topicService, 'createTopic');
|
|
@@ -102,7 +108,12 @@ describe('topic action', () => {
|
|
|
102
108
|
const { result } = renderHook(() => useChatStore());
|
|
103
109
|
const messages = [{ id: 'message1' }, { id: 'message2' }] as ChatMessage[];
|
|
104
110
|
act(() => {
|
|
105
|
-
useChatStore.setState({
|
|
111
|
+
useChatStore.setState({
|
|
112
|
+
messagesMap: {
|
|
113
|
+
[messageMapKey('session-id')]: messages,
|
|
114
|
+
},
|
|
115
|
+
activeId: 'session-id',
|
|
116
|
+
});
|
|
106
117
|
});
|
|
107
118
|
|
|
108
119
|
const createTopicSpy = vi
|
|
@@ -24,7 +24,7 @@ import { chatSelectors } from '../message/selectors';
|
|
|
24
24
|
import { ChatTopicDispatch, topicReducer } from './reducer';
|
|
25
25
|
import { topicSelectors } from './selectors';
|
|
26
26
|
|
|
27
|
-
const n = setNamespace('
|
|
27
|
+
const n = setNamespace('t');
|
|
28
28
|
|
|
29
29
|
const SWR_USE_FETCH_TOPIC = 'SWR_USE_FETCH_TOPIC';
|
|
30
30
|
const SWR_USE_SEARCH_TOPIC = 'SWR_USE_SEARCH_TOPIC';
|
|
@@ -38,10 +38,12 @@ export interface ChatTopicAction {
|
|
|
38
38
|
removeTopic: (id: string) => Promise<void>;
|
|
39
39
|
removeUnstarredTopic: () => void;
|
|
40
40
|
saveToTopic: () => Promise<string | undefined>;
|
|
41
|
+
createTopic: () => Promise<string | undefined>;
|
|
42
|
+
|
|
41
43
|
autoRenameTopicTitle: (id: string) => Promise<void>;
|
|
42
44
|
duplicateTopic: (id: string) => Promise<void>;
|
|
43
45
|
summaryTopicTitle: (topicId: string, messages: ChatMessage[]) => Promise<void>;
|
|
44
|
-
switchTopic: (id?: string) => Promise<void>;
|
|
46
|
+
switchTopic: (id?: string, skipRefreshMessage?: boolean) => Promise<void>;
|
|
45
47
|
updateTopicTitleInSummary: (id: string, title: string) => void;
|
|
46
48
|
updateTopicTitle: (id: string, title: string) => Promise<void>;
|
|
47
49
|
useFetchTopics: (sessionId: string) => SWRResponse<ChatTopic[]>;
|
|
@@ -71,6 +73,21 @@ export const chatTopic: StateCreator<
|
|
|
71
73
|
}
|
|
72
74
|
},
|
|
73
75
|
|
|
76
|
+
createTopic: async () => {
|
|
77
|
+
const { activeId, internal_createTopic } = get();
|
|
78
|
+
|
|
79
|
+
const messages = chatSelectors.currentChats(get());
|
|
80
|
+
const topicId = await internal_createTopic({
|
|
81
|
+
sessionId: activeId,
|
|
82
|
+
title: t('topic.defaultTitle', { ns: 'chat' }),
|
|
83
|
+
messages: messages.map((m) => m.id),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// get().internal_updateTopicLoading(topicId, true);
|
|
87
|
+
|
|
88
|
+
return topicId;
|
|
89
|
+
},
|
|
90
|
+
|
|
74
91
|
saveToTopic: async () => {
|
|
75
92
|
// if there is no message, stop
|
|
76
93
|
const messages = chatSelectors.currentChats(get());
|
|
@@ -189,9 +206,10 @@ export const chatTopic: StateCreator<
|
|
|
189
206
|
},
|
|
190
207
|
},
|
|
191
208
|
),
|
|
192
|
-
switchTopic: async (id) => {
|
|
193
|
-
set({ activeTopicId: id }, false, n('toggleTopic'));
|
|
209
|
+
switchTopic: async (id, skipRefreshMessage) => {
|
|
210
|
+
set({ activeTopicId: !id ? (null as any) : id }, false, n('toggleTopic'));
|
|
194
211
|
|
|
212
|
+
if (skipRefreshMessage) return;
|
|
195
213
|
await get().refreshMessages();
|
|
196
214
|
},
|
|
197
215
|
// delete
|
|
@@ -272,7 +290,10 @@ export const chatTopic: StateCreator<
|
|
|
272
290
|
},
|
|
273
291
|
internal_createTopic: async (params) => {
|
|
274
292
|
const tmpId = Date.now().toString();
|
|
275
|
-
get().internal_dispatchTopic(
|
|
293
|
+
get().internal_dispatchTopic(
|
|
294
|
+
{ type: 'addTopic', value: { ...params, id: tmpId } },
|
|
295
|
+
'internal_createTopic',
|
|
296
|
+
);
|
|
276
297
|
|
|
277
298
|
get().internal_updateTopicLoading(tmpId, true);
|
|
278
299
|
const topicId = await topicService.createTopic(params);
|
|
@@ -288,6 +309,6 @@ export const chatTopic: StateCreator<
|
|
|
288
309
|
internal_dispatchTopic: (payload, action) => {
|
|
289
310
|
const nextTopics = topicReducer(get().topics, payload);
|
|
290
311
|
|
|
291
|
-
set({ topics: nextTopics }, false, action);
|
|
312
|
+
set({ topics: nextTopics }, false, action ?? n(`dispatchTopic/${payload.type}`));
|
|
292
313
|
},
|
|
293
314
|
});
|