@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.
@@ -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('message');
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: (params: CreateMessageParams) => Promise<string>;
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 = get().messages.filter((m) => m.tool_call_id === tool.id);
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
- // if message is empty and no files, then stop
222
- if (!message && (!fileIdList || fileIdList?.length === 0)) return;
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 id = await get().internal_createMessage(newMessage);
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) return;
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
- if (!activeTopicId && chats.length >= agentConfig.autoCreateTopicThreshold) {
252
- const { saveToTopic, switchTopic } = get();
253
- const id = await saveToTopic();
254
- if (id) switchTopic(id);
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
- { activeId: sessionId, messages, messagesInit: true },
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().messages, payload);
443
+ const messages = messagesReducer(chatSelectors.currentChats(get()), payload);
444
+
445
+ const nextMap = { ...get().messagesMap, [chatSelectors.currentChatKey(get())]: messages };
364
446
 
365
- set({ messages }, false, { type: `dispatchMessage/${payload.type}`, payload });
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 { internal_dispatchMessage, refreshMessages, internal_toggleMessageLoading } = get();
703
+ internal_createMessage: async (message, context) => {
704
+ const { internal_createTmpMessage, refreshMessages, internal_toggleMessageLoading } = get();
705
+ let tempId = context?.tempMessageId;
621
706
 
622
- // use optimistic update to avoid the slow waiting
623
- const tempId = 'tmp_' + nanoid();
624
- internal_dispatchMessage({ type: 'createMessage', id: tempId, value: message });
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 = { messages: mockMessages } as ChatStore;
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, { messages: mockMessages });
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(mockMessages[0]);
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(mockMessages[0]);
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, { messages: mockMessages });
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, { messages: mockMessages });
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, { messages: mockMessages, activeId: 'someActiveId' });
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
- messages: mockMessages,
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
- messages: mockMessages,
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) => `${s.activeId}_${s.activeTopicId}`;
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
- return s.messages.map((i) => ({ ...i, meta: getMeta(i) }));
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) => chatHelpers.getMessageById(s.messages, id);
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,
@@ -0,0 +1,7 @@
1
+ export const messageMapKey = (sessionId: string, topicId?: string | null) => {
2
+ let topic = topicId;
3
+
4
+ if (typeof topicId === 'undefined') topic = null;
5
+
6
+ return `${sessionId}_${topic}`;
7
+ };
@@ -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
- messages: [message],
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
- messages: [message],
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({ messages: [pluginMessage] });
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({ messages: [pluginMessage] });
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({ messages });
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({ messages: [] });
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({ messages, activeId: 'session-id' });
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('topic');
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({ type: 'addTopic', value: { ...params, id: tmpId } });
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
  });