@lobehub/chat 0.107.15 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 0.108.0](https://github.com/lobehub/lobe-chat/compare/v0.107.16...v0.108.0)
6
+
7
+ <sup>Released on **2023-12-03**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Hide the password form item in the settings when there is no `ACCESS_CODE` env.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Hide the password form item in the settings when there is no `ACCESS_CODE` env, closes [#568](https://github.com/lobehub/lobe-chat/issues/568) ([3b5f8b2](https://github.com/lobehub/lobe-chat/commit/3b5f8b2))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 0.107.16](https://github.com/lobehub/lobe-chat/compare/v0.107.15...v0.107.16)
31
+
32
+ <sup>Released on **2023-12-03**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix custom agent meta issue.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix custom agent meta issue, closes [#567](https://github.com/lobehub/lobe-chat/issues/567) ([75560e1](https://github.com/lobehub/lobe-chat/commit/75560e1))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.107.15](https://github.com/lobehub/lobe-chat/compare/v0.107.14...v0.107.15)
6
56
 
7
57
  <sup>Released on **2023-12-03**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.107.15",
3
+ "version": "0.108.0",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -18,8 +18,8 @@ import SkeletonList from './SkeletonList';
18
18
 
19
19
  const List = memo(() => {
20
20
  const { t } = useTranslation('common');
21
-
22
- const data = useChatStore(chatSelectors.currentChatsWithGuideMessage, isEqual);
21
+ const meta = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
22
+ const data = useChatStore(chatSelectors.currentChatsWithGuideMessage(meta), isEqual);
23
23
 
24
24
  const [init, chatLoadingId, resendMessage, updateMessageContent] = useChatStore((s) => [
25
25
  s.messagesInit,
@@ -6,14 +6,15 @@ import { FC, memo } from 'react';
6
6
  import ResponsiveIndex from '@/components/ResponsiveIndex';
7
7
 
8
8
  import Common from '../common';
9
+ import { SettingsCommonProps } from '../common/Common';
9
10
  import Layout from './layout.desktop';
10
11
 
11
12
  const Mobile: FC = dynamic(() => import('../(mobile)'), { ssr: false }) as FC;
12
13
 
13
- export default memo(() => (
14
+ export default memo<SettingsCommonProps>((props) => (
14
15
  <ResponsiveIndex Mobile={Mobile}>
15
16
  <Layout>
16
- <Common />
17
+ <Common {...props} />
17
18
  </Layout>
18
19
  </ResponsiveIndex>
19
20
  ));
@@ -21,7 +21,11 @@ import { ThemeSwatchesNeutral, ThemeSwatchesPrimary } from '../features/ThemeSwa
21
21
 
22
22
  type SettingItemGroup = ItemGroup;
23
23
 
24
- const Common = memo(() => {
24
+ export interface SettingsCommonProps {
25
+ showAccessCodeConfig: boolean;
26
+ }
27
+
28
+ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig }) => {
25
29
  const { t } = useTranslation('setting');
26
30
  const [form] = AntForm.useForm();
27
31
 
@@ -154,6 +158,7 @@ const Common = memo(() => {
154
158
  {
155
159
  children: <Input.Password placeholder={t('settingSystem.accessCode.placeholder')} />,
156
160
  desc: t('settingSystem.accessCode.desc'),
161
+ hidden: !showAccessCodeConfig,
157
162
  label: t('settingSystem.accessCode.title'),
158
163
  name: 'password',
159
164
  },
@@ -9,16 +9,16 @@ import { useSwitchSideBarOnInit } from '@/store/global/hooks/useSwitchSettingsOn
9
9
  import { SettingsTabs } from '@/store/global/initialState';
10
10
 
11
11
  import Footer from '../features/Footer';
12
- import Common from './Common';
12
+ import Common, { SettingsCommonProps } from './Common';
13
13
 
14
- export default memo(() => {
14
+ export default memo<SettingsCommonProps>((props) => {
15
15
  useSwitchSideBarOnInit(SettingsTabs.Common);
16
16
  const { t } = useTranslation('setting');
17
17
 
18
18
  return (
19
19
  <>
20
20
  <PageTitle title={t('tab.common')} />
21
- <Common />
21
+ <Common {...props} />
22
22
  <Footer>LobeChat v{CURRENT_VERSION}</Footer>
23
23
  </>
24
24
  );
@@ -1,3 +1,9 @@
1
+ import { getServerConfig } from '@/config/server';
2
+
1
3
  import Index from './index';
2
4
 
3
- export default () => <Index />;
5
+ export default () => {
6
+ const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();
7
+
8
+ return <Index showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} />;
9
+ };
@@ -1,3 +1,4 @@
1
+ import { getServerConfig } from '@/config/server';
1
2
  import { isMobileDevice } from '@/utils/responsive';
2
3
 
3
4
  import DesktopPage from './(desktop)';
@@ -8,7 +9,9 @@ const Page = () => {
8
9
 
9
10
  const Page = mobile ? MobilePage : DesktopPage;
10
11
 
11
- return <Page />;
12
+ const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();
13
+
14
+ return <Page showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG} />;
12
15
  };
13
16
 
14
17
  export default Page;
@@ -33,8 +33,12 @@ export const getServerConfig = () => {
33
33
  regions = process.env.OPENAI_FUNCTION_REGIONS.split(',');
34
34
  }
35
35
 
36
+ const ACCESS_CODE = process.env.ACCESS_CODE;
37
+
36
38
  return {
37
- ACCESS_CODE: process.env.ACCESS_CODE,
39
+ ACCESS_CODE,
40
+
41
+ SHOW_ACCESS_CODE_CONFIG: !!ACCESS_CODE,
38
42
 
39
43
  OPENAI_API_KEY: process.env.OPENAI_API_KEY,
40
44
  OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
@@ -10,7 +10,7 @@ import { chatSelectors } from '@/store/chat/selectors';
10
10
  import { ChatStore } from '@/store/chat/store';
11
11
  import { setNamespace } from '@/utils/storeDebug';
12
12
 
13
- const t = setNamespace('enhance');
13
+ const n = setNamespace('enhance');
14
14
 
15
15
  /**
16
16
  * enhance chat action like translate,tts
@@ -49,7 +49,7 @@ export const chatEnhance: StateCreator<
49
49
  await messageService.updateMessageTranslate(id, { content: '', from: '', to: targetLang });
50
50
  await refreshMessages();
51
51
 
52
- toggleChatLoading(true, id, t('translateMessage(start)', { id }) as string);
52
+ toggleChatLoading(true, id, n('translateMessage(start)', { id }) as string);
53
53
 
54
54
  let content = '';
55
55
  let from = '';
@@ -25,7 +25,7 @@ import { setNamespace } from '@/utils/storeDebug';
25
25
  import { MessageDispatch, messagesReducer } from '../reducers/message';
26
26
  import { chatSelectors } from '../selectors';
27
27
 
28
- const t = setNamespace('message');
28
+ const n = setNamespace('message');
29
29
 
30
30
  export interface ChatMessageAction {
31
31
  // create
@@ -191,10 +191,10 @@ export const chatMessage: StateCreator<
191
191
 
192
192
  abortController.abort();
193
193
 
194
- toggleChatLoading(false, undefined, t('stopGenerateMessage') as string);
194
+ toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
195
195
  },
196
196
  updateInputMessage: (message) => {
197
- set({ inputMessage: message }, false, t('updateInputMessage', message));
197
+ set({ inputMessage: message }, false, n('updateInputMessage', message));
198
198
  },
199
199
  updateMessageContent: async (id, content) => {
200
200
  const { dispatchMessage, refreshMessages } = get();
@@ -217,7 +217,7 @@ export const chatMessage: StateCreator<
217
217
  set(
218
218
  { activeId: sessionId, messages, messagesInit: true },
219
219
  false,
220
- t('useFetchMessages', {
220
+ n('useFetchMessages', {
221
221
  messages,
222
222
  queryKey: key,
223
223
  }),
@@ -291,7 +291,7 @@ export const chatMessage: StateCreator<
291
291
 
292
292
  const messages = messagesReducer(get().messages, payload);
293
293
 
294
- set({ messages }, false, t(`dispatchMessage/${payload.type}`, payload));
294
+ set({ messages }, false, n(`dispatchMessage/${payload.type}`, payload));
295
295
  },
296
296
  fetchAIChatMessage: async (messages, assistantId) => {
297
297
  const { toggleChatLoading, refreshMessages } = get();
@@ -299,7 +299,7 @@ export const chatMessage: StateCreator<
299
299
  const abortController = toggleChatLoading(
300
300
  true,
301
301
  assistantId,
302
- t('generateMessage(start)', { assistantId, messages }) as string,
302
+ n('generateMessage(start)', { assistantId, messages }) as string,
303
303
  );
304
304
 
305
305
  const config = getAgentConfig();
@@ -380,7 +380,7 @@ export const chatMessage: StateCreator<
380
380
  },
381
381
  });
382
382
 
383
- toggleChatLoading(false, undefined, t('generateMessage(end)') as string);
383
+ toggleChatLoading(false, undefined, n('generateMessage(end)') as string);
384
384
 
385
385
  // also exist message like this:
386
386
  // 请稍等,我帮您查询一下。{"function_call": {"name": "plugin-identifier____recommendClothes____standalone", "arguments": "{\n "mood": "",\n "gender": "man"\n}"}}
@@ -10,7 +10,7 @@ import { setNamespace } from '@/utils/storeDebug';
10
10
 
11
11
  import { chatSelectors } from '../selectors';
12
12
 
13
- const t = setNamespace('plugin');
13
+ const n = setNamespace('plugin');
14
14
 
15
15
  export interface ChatPluginAction {
16
16
  fillPluginMessageContent: (id: string, content: string) => Promise<void>;
@@ -38,7 +38,7 @@ export const chatPlugin: StateCreator<
38
38
  const { refreshMessages, coreProcessMessage, toggleChatLoading } = get();
39
39
  let data: string;
40
40
  try {
41
- const abortController = toggleChatLoading(true, id, t('fetchPlugin') as string);
41
+ const abortController = toggleChatLoading(true, id, n('fetchPlugin') as string);
42
42
  data = await chatService.runPluginApi(payload, { signal: abortController?.signal });
43
43
  } catch (error) {
44
44
  await messageService.updateMessageError(id, error as any);
@@ -0,0 +1,391 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { mutate } from 'swr';
3
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { LOADING_FLAT } from '@/const/message';
6
+ import { chatService } from '@/services/chat';
7
+ import { messageService } from '@/services/message';
8
+ import { topicService } from '@/services/topic';
9
+ import { ChatMessage } from '@/types/chatMessage';
10
+ import { ChatTopic } from '@/types/topic';
11
+
12
+ import { useChatStore } from '../store';
13
+
14
+ // Mock topicService 和 messageService
15
+ vi.mock('@/services/topic', () => ({
16
+ topicService: {
17
+ removeTopics: vi.fn(),
18
+ removeAllTopic: vi.fn(),
19
+ removeTopic: vi.fn(),
20
+ createTopic: vi.fn(),
21
+ updateTitle: vi.fn(),
22
+ updateFavorite: vi.fn(),
23
+ batchRemoveTopics: vi.fn(),
24
+ getTopics: vi.fn(),
25
+ searchTopics: vi.fn(),
26
+ },
27
+ }));
28
+
29
+ vi.mock('@/services/message', () => ({
30
+ messageService: {
31
+ removeMessages: vi.fn(),
32
+ },
33
+ }));
34
+
35
+ beforeEach(() => {
36
+ // Setup initial state and mocks before each test
37
+ vi.clearAllMocks();
38
+ useChatStore.setState(
39
+ {
40
+ activeId: undefined,
41
+ activeTopicId: undefined,
42
+ // ... initial state
43
+ },
44
+ false,
45
+ );
46
+ });
47
+
48
+ afterEach(() => {
49
+ // Cleanup mocks after each test
50
+ vi.restoreAllMocks();
51
+ });
52
+
53
+ describe('topic action', () => {
54
+ describe('openNewTopicOrSaveTopic', () => {
55
+ it('should call switchTopic if activeTopicId exists', async () => {
56
+ const { result } = renderHook(() => useChatStore());
57
+ await act(async () => {
58
+ useChatStore.setState({ activeTopicId: 'existing-topic-id' });
59
+ });
60
+
61
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
62
+
63
+ await act(async () => {
64
+ result.current.openNewTopicOrSaveTopic();
65
+ });
66
+
67
+ expect(switchTopicSpy).toHaveBeenCalled();
68
+ });
69
+
70
+ it('should call saveToTopic if activeTopicId does not exist', async () => {
71
+ const { result } = renderHook(() => useChatStore());
72
+ await act(async () => {
73
+ useChatStore.setState({ activeTopicId: '' });
74
+ });
75
+
76
+ const saveToTopicSpy = vi.spyOn(result.current, 'saveToTopic');
77
+
78
+ await act(async () => {
79
+ await result.current.openNewTopicOrSaveTopic();
80
+ });
81
+
82
+ expect(saveToTopicSpy).toHaveBeenCalled();
83
+ });
84
+ });
85
+ describe('saveToTopic', () => {
86
+ it('should not create a topic if there are no messages', async () => {
87
+ const { result } = renderHook(() => useChatStore());
88
+ useChatStore.setState({ messages: [] });
89
+
90
+ const createTopicSpy = vi.spyOn(topicService, 'createTopic');
91
+
92
+ const topicId = await result.current.saveToTopic();
93
+
94
+ expect(createTopicSpy).not.toHaveBeenCalled();
95
+ expect(topicId).toBeUndefined();
96
+ });
97
+
98
+ it('should create a topic and bind messages to it', async () => {
99
+ const { result } = renderHook(() => useChatStore());
100
+ const messages = [{ id: 'message1' }, { id: 'message2' }] as ChatMessage[];
101
+ useChatStore.setState({ messages, activeId: 'session-id' });
102
+
103
+ const createTopicSpy = vi
104
+ .spyOn(topicService, 'createTopic')
105
+ .mockResolvedValue('new-topic-id');
106
+
107
+ const topicId = await result.current.saveToTopic();
108
+
109
+ expect(createTopicSpy).toHaveBeenCalledWith(
110
+ expect.objectContaining({
111
+ sessionId: 'session-id',
112
+ messages: messages.map((m) => m.id),
113
+ }),
114
+ );
115
+ expect(topicId).toEqual('new-topic-id');
116
+ });
117
+ });
118
+ describe('refreshTopic', () => {
119
+ beforeEach(() => {
120
+ vi.mock('swr', async () => {
121
+ const actual = await vi.importActual('swr');
122
+ return {
123
+ ...(actual as any),
124
+ mutate: vi.fn(),
125
+ };
126
+ });
127
+ });
128
+ afterEach(() => {
129
+ // 在每个测试用例开始前恢复到实际的 SWR 实现
130
+ vi.resetAllMocks();
131
+ });
132
+
133
+ it('should call mutate to refresh topics', async () => {
134
+ const { result } = renderHook(() => useChatStore());
135
+ const activeId = 'test-session-id';
136
+ useChatStore.setState({ activeId });
137
+
138
+ // Mock the mutate function to resolve immediately
139
+
140
+ await act(async () => {
141
+ await result.current.refreshTopic();
142
+ });
143
+
144
+ // Check if mutate has been called with the active session ID
145
+ expect(mutate).toHaveBeenCalledWith(activeId);
146
+ });
147
+
148
+ it('should handle errors during refreshing topics', async () => {
149
+ const { result } = renderHook(() => useChatStore());
150
+ const activeId = 'test-session-id';
151
+ useChatStore.setState({ activeId });
152
+
153
+ // Mock the mutate function to throw an error
154
+ // 设置模拟错误
155
+ (mutate as Mock).mockImplementation(() => {
156
+ throw new Error('Mutate error');
157
+ });
158
+
159
+ await act(async () => {
160
+ await expect(result.current.refreshTopic()).rejects.toThrow('Mutate error');
161
+ });
162
+
163
+ // 确保恢复 mutate 的模拟,以免影响其他测试
164
+ (mutate as Mock).mockReset();
165
+ });
166
+
167
+ // Additional tests for refreshTopic can be added here...
168
+ });
169
+ describe('favoriteTopic', () => {
170
+ it('should update the favorite state of a topic and refresh topics', async () => {
171
+ const { result } = renderHook(() => useChatStore());
172
+ const topicId = 'topic-id';
173
+ const favState = true;
174
+
175
+ const updateFavoriteSpy = vi.spyOn(topicService, 'updateFavorite').mockResolvedValue(true);
176
+
177
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
178
+
179
+ await act(async () => {
180
+ await result.current.favoriteTopic(topicId, favState);
181
+ });
182
+
183
+ expect(updateFavoriteSpy).toHaveBeenCalledWith(topicId, favState);
184
+ expect(refreshTopicSpy).toHaveBeenCalled();
185
+ });
186
+ });
187
+ describe('useFetchTopics', () => {
188
+ it('should fetch topics for a given session id', async () => {
189
+ const sessionId = 'test-session-id';
190
+ const topics = [{ id: 'topic-id', title: 'Test Topic' }];
191
+
192
+ // Mock the topicService.getTopics to resolve with topics array
193
+ (topicService.getTopics as Mock).mockResolvedValue(topics);
194
+
195
+ // Use the hook with the session id
196
+ const { result } = renderHook(() => useChatStore().useFetchTopics(sessionId));
197
+
198
+ // Wait for the hook to resolve and update the state
199
+ await waitFor(() => {
200
+ expect(result.current.data).toEqual(topics);
201
+ });
202
+ expect(useChatStore.getState().topicsInit).toBeTruthy();
203
+ expect(useChatStore.getState().topics).toEqual(topics);
204
+ });
205
+ });
206
+ describe('useSearchTopics', () => {
207
+ it('should search topics with the given keywords', async () => {
208
+ const keywords = 'search-term';
209
+ const searchResults = [{ id: 'searched-topic-id', title: 'Searched Topic' }];
210
+
211
+ // Mock the topicService.searchTopics to resolve with search results
212
+ (topicService.searchTopics as Mock).mockResolvedValue(searchResults);
213
+
214
+ // Use the hook with the keywords
215
+ const { result } = renderHook(() => useChatStore().useSearchTopics(keywords));
216
+
217
+ // Wait for the hook to resolve and update the state
218
+ await waitFor(() => {
219
+ expect(result.current.data).toEqual(searchResults);
220
+ });
221
+ });
222
+ });
223
+ describe('updateTopicTitle', () => {
224
+ it('should call topicService.updateTitle with correct parameters and refresh the topic', async () => {
225
+ const topicId = 'topic-id';
226
+ const newTitle = 'Updated Topic Title';
227
+ // Mock the topicService.updateTitle to resolve immediately
228
+ (topicService.updateTitle as Mock).mockResolvedValue(undefined);
229
+
230
+ const { result } = renderHook(() => useChatStore());
231
+
232
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
233
+
234
+ // Call the action with the topicId and newTitle
235
+ await act(async () => {
236
+ await result.current.updateTopicTitle(topicId, newTitle);
237
+ });
238
+
239
+ // Verify that the topicService.updateTitle was called with correct parameters
240
+ expect(topicService.updateTitle).toHaveBeenCalledWith(topicId, newTitle);
241
+
242
+ // Verify that the refreshTopic was called to update the state
243
+ expect(refreshTopicSpy).toHaveBeenCalled();
244
+ });
245
+ });
246
+ describe('switchTopic', () => {
247
+ it('should update activeTopicId and call refreshMessages', async () => {
248
+ const topicId = 'topic-id';
249
+ const { result } = renderHook(() => useChatStore());
250
+
251
+ const refreshMessagesSpy = vi.spyOn(result.current, 'refreshMessages');
252
+ // Call the switchTopic action with the topicId
253
+ await act(async () => {
254
+ await result.current.switchTopic(topicId);
255
+ });
256
+
257
+ // Verify that the activeTopicId has been updated
258
+ expect(useChatStore.getState().activeTopicId).toBe(topicId);
259
+
260
+ // Verify that the refreshMessages was called to update the messages
261
+ expect(refreshMessagesSpy).toHaveBeenCalled();
262
+ });
263
+ });
264
+ describe('removeSessionTopics', () => {
265
+ it('should remove all topics from the current session and refresh the topic list', async () => {
266
+ const { result } = renderHook(() => useChatStore());
267
+ const activeId = 'test-session-id';
268
+ await act(async () => {
269
+ useChatStore.setState({ activeId });
270
+ });
271
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
272
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
273
+
274
+ await act(async () => {
275
+ await result.current.removeSessionTopics();
276
+ });
277
+
278
+ expect(topicService.removeTopics).toHaveBeenCalledWith(activeId);
279
+ expect(refreshTopicSpy).toHaveBeenCalled();
280
+ expect(switchTopicSpy).toHaveBeenCalled();
281
+ });
282
+ });
283
+ describe('removeAllTopics', () => {
284
+ it('should remove all topics and refresh the topic list', async () => {
285
+ const { result } = renderHook(() => useChatStore());
286
+
287
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
288
+
289
+ await act(async () => {
290
+ await result.current.removeAllTopics();
291
+ });
292
+
293
+ expect(topicService.removeAllTopic).toHaveBeenCalled();
294
+ expect(refreshTopicSpy).toHaveBeenCalled();
295
+ });
296
+ });
297
+ describe('removeTopic', () => {
298
+ it('should remove a specific topic and its messages, then refresh the topic list', async () => {
299
+ const topicId = 'topic-1';
300
+ const { result } = renderHook(() => useChatStore());
301
+ const activeId = 'test-session-id';
302
+
303
+ await act(async () => {
304
+ useChatStore.setState({ activeId });
305
+ });
306
+
307
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
308
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
309
+
310
+ await act(async () => {
311
+ await result.current.removeTopic(topicId);
312
+ });
313
+
314
+ expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
315
+ expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
316
+ expect(refreshTopicSpy).toHaveBeenCalled();
317
+ expect(switchTopicSpy).toHaveBeenCalled();
318
+ });
319
+ });
320
+ describe('removeUnstarredTopic', () => {
321
+ it('should remove unstarred topics and refresh the topic list', async () => {
322
+ const { result } = renderHook(() => useChatStore());
323
+ // Set up mock state with unstarred topics
324
+ await act(async () => {
325
+ useChatStore.setState({
326
+ topics: [
327
+ { id: 'topic-1', favorite: false },
328
+ { id: 'topic-2', favorite: true },
329
+ { id: 'topic-3', favorite: false },
330
+ ] as ChatTopic[],
331
+ });
332
+ });
333
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
334
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
335
+
336
+ await act(async () => {
337
+ await result.current.removeUnstarredTopic();
338
+ });
339
+
340
+ expect(topicService.batchRemoveTopics).toHaveBeenCalledWith(['topic-1', 'topic-3']);
341
+ expect(refreshTopicSpy).toHaveBeenCalled();
342
+ expect(switchTopicSpy).toHaveBeenCalled();
343
+ });
344
+ });
345
+ describe('updateTopicLoading', () => {
346
+ it('should call update topicLoadingId', async () => {
347
+ const { result } = renderHook(() => useChatStore());
348
+ expect(result.current.topicLoadingId).toBeUndefined();
349
+
350
+ // Call the action with the topicId and newTitle
351
+ await act(async () => {
352
+ await result.current.updateTopicLoading('loading-id');
353
+ });
354
+
355
+ expect(result.current.topicLoadingId).toEqual('loading-id');
356
+ });
357
+ });
358
+ describe('summaryTopicTitle', () => {
359
+ it('should auto-summarize the topic title and update it', async () => {
360
+ const topicId = 'topic-1';
361
+ const messages = [{ id: 'message-1', content: 'Hello' }] as ChatMessage[];
362
+ const topics = [{ id: 'topic-1', title: 'Test Topic' }] as ChatTopic[];
363
+ const { result } = renderHook(() => useChatStore());
364
+ await act(async () => {
365
+ useChatStore.setState({ topics });
366
+ });
367
+
368
+ // Mock the `updateTopicTitleInSummary` and `refreshTopic` for spying
369
+ const updateTopicTitleInSummarySpy = vi.spyOn(result.current, 'updateTopicTitleInSummary');
370
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
371
+
372
+ // Mock the `chatService.fetchPresetTaskResult` to simulate the AI response
373
+ vi.spyOn(chatService, 'fetchPresetTaskResult').mockImplementation((params) => {
374
+ if (params) {
375
+ params.onFinish?.('Summarized Title');
376
+ }
377
+ return Promise.resolve(undefined);
378
+ });
379
+
380
+ await act(async () => {
381
+ await result.current.summaryTopicTitle(topicId, messages);
382
+ });
383
+
384
+ // Verify that the title was updated and the topic was refreshed
385
+ expect(updateTopicTitleInSummarySpy).toHaveBeenCalledWith(topicId, LOADING_FLAT);
386
+ expect(refreshTopicSpy).toHaveBeenCalled();
387
+
388
+ // TODO: need to test with fetchPresetTaskResult
389
+ });
390
+ });
391
+ });
@@ -22,7 +22,7 @@ const n = setNamespace('topic');
22
22
 
23
23
  export interface ChatTopicAction {
24
24
  favoriteTopic: (id: string, favState: boolean) => Promise<void>;
25
- openNewTopicOrSaveTopic: () => void;
25
+ openNewTopicOrSaveTopic: () => Promise<void>;
26
26
  refreshTopic: () => Promise<void>;
27
27
  removeAllTopics: () => Promise<void>;
28
28
  removeSessionTopics: () => Promise<void>;
@@ -45,7 +45,7 @@ export const chatTopic: StateCreator<
45
45
  ChatTopicAction
46
46
  > = (set, get) => ({
47
47
  // create
48
- openNewTopicOrSaveTopic: () => {
48
+ openNewTopicOrSaveTopic: async () => {
49
49
  const { switchTopic, saveToTopic, activeTopicId } = get();
50
50
  const hasTopic = !!activeTopicId;
51
51