@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 +50 -0
- package/package.json +1 -1
- package/src/app/chat/features/Conversation/ChatList/index.tsx +2 -2
- package/src/app/settings/(desktop)/index.tsx +3 -2
- package/src/app/settings/common/Common.tsx +6 -1
- package/src/app/settings/common/index.tsx +3 -3
- package/src/app/settings/common/page.tsx +7 -1
- package/src/app/settings/page.tsx +4 -1
- package/src/config/server.ts +5 -1
- package/src/store/chat/actions/enhance.ts +2 -2
- package/src/store/chat/actions/message.ts +7 -7
- package/src/store/chat/actions/plugin.ts +2 -2
- package/src/store/chat/actions/topic.test.ts +391 -0
- package/src/store/chat/actions/topic.ts +2 -2
- package/src/store/chat/helpers.test.ts +109 -0
- package/src/store/chat/helpers.ts +10 -3
- package/src/store/chat/selectors/__tests__/chat.test.ts +251 -0
- package/src/store/chat/selectors/__tests__/topic.test.ts +1 -1
- package/src/store/chat/selectors/chat.ts +38 -38
- package/src/store/file/slices/images/action.ts +5 -5
- package/src/store/global/slices/agent.ts +2 -2
- package/src/store/global/slices/common.ts +6 -6
- package/src/store/global/slices/settings.ts +4 -4
- package/src/store/plugin/slices/customPlugin/action.ts +4 -4
- package/src/store/plugin/slices/plugin/action.ts +8 -8
- package/src/store/session/slices/session/action.ts +4 -4
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
|
+
[](#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
|
+
[](#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.
|
|
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
|
-
|
|
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 () =>
|
|
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
|
-
|
|
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;
|
package/src/config/server.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
194
|
+
toggleChatLoading(false, undefined, n('stopGenerateMessage') as string);
|
|
195
195
|
},
|
|
196
196
|
updateInputMessage: (message) => {
|
|
197
|
-
set({ inputMessage: message }, false,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|