@lobehub/chat 1.123.4 → 1.124.1
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/.env.example +5 -0
- package/CHANGELOG.md +58 -0
- package/Dockerfile +2 -0
- package/Dockerfile.database +2 -0
- package/Dockerfile.pglite +2 -0
- package/changelog/v1.json +21 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
- package/locales/ar/chat.json +8 -2
- package/locales/ar/editor.json +47 -0
- package/locales/bg-BG/chat.json +8 -2
- package/locales/bg-BG/editor.json +47 -0
- package/locales/de-DE/chat.json +8 -2
- package/locales/de-DE/editor.json +47 -0
- package/locales/en-US/chat.json +8 -2
- package/locales/en-US/editor.json +47 -0
- package/locales/es-ES/chat.json +8 -2
- package/locales/es-ES/editor.json +47 -0
- package/locales/es-ES/models.json +3 -1
- package/locales/fa-IR/chat.json +8 -2
- package/locales/fa-IR/editor.json +47 -0
- package/locales/fr-FR/chat.json +8 -2
- package/locales/fr-FR/editor.json +47 -0
- package/locales/it-IT/chat.json +8 -2
- package/locales/it-IT/editor.json +47 -0
- package/locales/ja-JP/chat.json +8 -2
- package/locales/ja-JP/editor.json +47 -0
- package/locales/ko-KR/chat.json +8 -2
- package/locales/ko-KR/editor.json +47 -0
- package/locales/ko-KR/models.json +3 -1
- package/locales/nl-NL/chat.json +8 -2
- package/locales/nl-NL/editor.json +47 -0
- package/locales/nl-NL/models.json +3 -1
- package/locales/pl-PL/chat.json +8 -2
- package/locales/pl-PL/editor.json +47 -0
- package/locales/pt-BR/chat.json +8 -2
- package/locales/pt-BR/editor.json +47 -0
- package/locales/ru-RU/chat.json +8 -2
- package/locales/ru-RU/editor.json +47 -0
- package/locales/tr-TR/chat.json +8 -2
- package/locales/tr-TR/editor.json +47 -0
- package/locales/vi-VN/chat.json +8 -2
- package/locales/vi-VN/editor.json +47 -0
- package/locales/zh-CN/chat.json +8 -2
- package/locales/zh-CN/editor.json +47 -0
- package/locales/zh-CN/modelProvider.json +1 -1
- package/locales/zh-TW/chat.json +8 -2
- package/locales/zh-TW/editor.json +47 -0
- package/locales/zh-TW/models.json +3 -1
- package/next.config.ts +4 -0
- package/package.json +4 -2
- package/packages/const/src/layoutTokens.ts +1 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
- package/packages/model-bank/src/aiModels/groq.ts +26 -8
- package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
- package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
- package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
- package/packages/model-bank/src/aiModels/novita.ts +40 -9
- package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
- package/packages/model-bank/src/aiModels/qwen.ts +62 -1
- package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
- package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
- package/packages/model-runtime/src/newapi/index.test.ts +49 -42
- package/packages/model-runtime/src/newapi/index.ts +124 -143
- package/packages/types/src/index.ts +1 -0
- package/packages/utils/src/index.ts +1 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
- package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
- package/src/config/llm.ts +8 -0
- package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
- package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
- package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
- package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
- package/src/features/ChatInput/ActionBar/config.ts +7 -1
- package/src/features/ChatInput/ActionBar/index.tsx +40 -51
- package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
- package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
- package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
- package/src/features/ChatInput/Desktop/index.tsx +94 -69
- package/src/features/ChatInput/InputEditor/index.tsx +134 -0
- package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
- package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
- package/src/features/ChatInput/Mobile/index.tsx +72 -0
- package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
- package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
- package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
- package/src/features/ChatInput/SendArea/index.tsx +36 -0
- package/src/features/ChatInput/StoreUpdater.tsx +41 -0
- package/src/features/ChatInput/TypoBar/index.tsx +139 -0
- package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
- package/src/features/ChatInput/index.ts +7 -0
- package/src/features/ChatInput/store/action.ts +75 -0
- package/src/features/ChatInput/store/index.ts +23 -0
- package/src/features/ChatInput/store/initialState.ts +54 -0
- package/src/features/ChatInput/store/selectors.ts +5 -0
- package/src/features/Conversation/components/BackBottom/style.ts +1 -1
- package/src/features/Conversation/components/SkeletonList.tsx +10 -3
- package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
- package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
- package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
- package/src/features/Portal/Thread/Chat/index.tsx +2 -2
- package/src/features/Portal/Thread/Header/index.tsx +1 -1
- package/src/hooks/useHotkeys/chatScope.ts +5 -3
- package/src/layout/GlobalProvider/Editor.tsx +27 -0
- package/src/layout/GlobalProvider/Locale.tsx +3 -23
- package/src/locales/default/chat.ts +8 -2
- package/src/locales/default/editor.ts +47 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/modelProvider.ts +1 -1
- package/src/services/aiChat.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
- package/src/store/chat/slices/aiChat/initialState.ts +19 -0
- package/src/store/chat/slices/aiChat/selectors.ts +18 -0
- package/src/store/global/action.test.ts +6 -5
- package/src/store/global/actions/__tests__/general.test.ts +6 -6
- package/src/store/global/actions/workspacePane.ts +6 -0
- package/src/store/global/initialState.ts +2 -4
- package/src/store/global/selectors/systemStatus.test.ts +1 -2
- package/src/store/global/selectors/systemStatus.ts +2 -5
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
- package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
- package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
- package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
- package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
- package/src/features/ChatInput/useSend.ts +0 -102
- package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
- package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
- package/src/libs/trpc/client/types.ts +0 -18
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -1,4 +1,5 @@
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
2
|
+
import { TRPCClientError } from '@trpc/client';
|
2
3
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
3
4
|
|
4
5
|
import { LOADING_FLAT } from '@/const/message';
|
@@ -10,7 +11,6 @@ import {
|
|
10
11
|
} from '@/const/settings';
|
11
12
|
import { aiChatService } from '@/services/aiChat';
|
12
13
|
import { chatService } from '@/services/chat';
|
13
|
-
//
|
14
14
|
import { messageService } from '@/services/message';
|
15
15
|
import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
|
16
16
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
@@ -18,6 +18,8 @@ import { UploadFileItem } from '@/types/files/upload';
|
|
18
18
|
import { ChatMessage } from '@/types/message';
|
19
19
|
|
20
20
|
import { useChatStore } from '../../../../store';
|
21
|
+
import { messageMapKey } from '../../../../utils/messageMapKey';
|
22
|
+
import { generateAIChatV2 } from '../generateAIChatV2';
|
21
23
|
|
22
24
|
vi.stubGlobal(
|
23
25
|
'fetch',
|
@@ -115,7 +117,10 @@ const mockState = {
|
|
115
117
|
refreshTopic: vi.fn(),
|
116
118
|
internal_execAgentRuntime: vi.fn(),
|
117
119
|
saveToTopic: vi.fn(),
|
118
|
-
|
120
|
+
switchTopic: vi.fn(),
|
121
|
+
internal_shouldUseRAG: () => false,
|
122
|
+
internal_retrieveChunks: vi.fn(),
|
123
|
+
} as any;
|
119
124
|
|
120
125
|
beforeEach(() => {
|
121
126
|
vi.clearAllMocks();
|
@@ -136,11 +141,11 @@ afterEach(() => {
|
|
136
141
|
describe('generateAIChatV2 actions', () => {
|
137
142
|
describe('sendMessageInServer', () => {
|
138
143
|
it('should not send message if there is no active session', async () => {
|
139
|
-
useChatStore.setState({ activeId: undefined });
|
140
144
|
const { result } = renderHook(() => useChatStore());
|
141
145
|
const message = 'Test message';
|
142
146
|
|
143
147
|
await act(async () => {
|
148
|
+
useChatStore.setState({ activeId: undefined });
|
144
149
|
await result.current.sendMessage({ message });
|
145
150
|
});
|
146
151
|
|
@@ -187,18 +192,21 @@ describe('generateAIChatV2 actions', () => {
|
|
187
192
|
await result.current.sendMessage({ message, files });
|
188
193
|
});
|
189
194
|
|
190
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
191
|
-
|
192
|
-
|
193
|
-
|
195
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
196
|
+
{
|
197
|
+
newAssistantMessage: {
|
198
|
+
model: DEFAULT_MODEL,
|
199
|
+
provider: DEFAULT_PROVIDER,
|
200
|
+
},
|
201
|
+
newUserMessage: {
|
202
|
+
content: message,
|
203
|
+
files: files.map((f) => f.id),
|
204
|
+
},
|
205
|
+
sessionId: mockState.activeId,
|
206
|
+
topicId: mockState.activeTopicId,
|
194
207
|
},
|
195
|
-
|
196
|
-
|
197
|
-
files: files.map((f) => f.id),
|
198
|
-
},
|
199
|
-
sessionId: mockState.activeId,
|
200
|
-
topicId: mockState.activeTopicId,
|
201
|
-
});
|
208
|
+
expect.anything(),
|
209
|
+
);
|
202
210
|
expect(result.current.internal_execAgentRuntime).toHaveBeenCalled();
|
203
211
|
});
|
204
212
|
|
@@ -249,7 +257,7 @@ describe('generateAIChatV2 actions', () => {
|
|
249
257
|
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
250
258
|
});
|
251
259
|
|
252
|
-
it('
|
260
|
+
it('should pass isWelcomeQuestion correctly to internal_execAgentRuntime when isWelcomeQuestion is true', async () => {
|
253
261
|
const { result } = renderHook(() => useChatStore());
|
254
262
|
|
255
263
|
await act(async () => {
|
@@ -263,49 +271,55 @@ describe('generateAIChatV2 actions', () => {
|
|
263
271
|
);
|
264
272
|
});
|
265
273
|
|
266
|
-
it('
|
274
|
+
it('should send message correctly when only files are provided without message content', async () => {
|
267
275
|
const { result } = renderHook(() => useChatStore());
|
268
276
|
|
269
277
|
await act(async () => {
|
270
278
|
await result.current.sendMessage({ message: '', files: [{ id: 'file-1' }] as any });
|
271
279
|
});
|
272
280
|
|
273
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
274
|
-
|
275
|
-
|
276
|
-
|
281
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
282
|
+
{
|
283
|
+
newAssistantMessage: {
|
284
|
+
model: DEFAULT_MODEL,
|
285
|
+
provider: DEFAULT_PROVIDER,
|
286
|
+
},
|
287
|
+
newUserMessage: {
|
288
|
+
content: '',
|
289
|
+
files: ['file-1'],
|
290
|
+
},
|
291
|
+
sessionId: 'session-id',
|
292
|
+
topicId: 'topic-id',
|
277
293
|
},
|
278
|
-
|
279
|
-
|
280
|
-
files: ['file-1'],
|
281
|
-
},
|
282
|
-
sessionId: 'session-id',
|
283
|
-
topicId: 'topic-id',
|
284
|
-
});
|
294
|
+
expect.anything(),
|
295
|
+
);
|
285
296
|
});
|
286
297
|
|
287
|
-
it('
|
298
|
+
it('should send message correctly when both files and message content are provided', async () => {
|
288
299
|
const { result } = renderHook(() => useChatStore());
|
289
300
|
|
290
301
|
await act(async () => {
|
291
302
|
await result.current.sendMessage({ message: 'test', files: [{ id: 'file-1' }] as any });
|
292
303
|
});
|
293
304
|
|
294
|
-
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
295
|
-
|
296
|
-
|
297
|
-
|
305
|
+
expect(aiChatService.sendMessageInServer).toHaveBeenCalledWith(
|
306
|
+
{
|
307
|
+
newAssistantMessage: {
|
308
|
+
model: DEFAULT_MODEL,
|
309
|
+
provider: DEFAULT_PROVIDER,
|
310
|
+
},
|
311
|
+
newUserMessage: {
|
312
|
+
content: 'test',
|
313
|
+
files: ['file-1'],
|
314
|
+
},
|
315
|
+
sessionId: 'session-id',
|
316
|
+
topicId: 'topic-id',
|
298
317
|
},
|
299
|
-
|
300
|
-
|
301
|
-
files: ['file-1'],
|
302
|
-
},
|
303
|
-
sessionId: 'session-id',
|
304
|
-
topicId: 'topic-id',
|
305
|
-
});
|
318
|
+
expect.anything(),
|
319
|
+
);
|
306
320
|
});
|
307
321
|
|
308
|
-
it('
|
322
|
+
it('should handle errors correctly when createMessage throws error without affecting the app', async () => {
|
309
323
|
const { result } = renderHook(() => useChatStore());
|
310
324
|
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(
|
311
325
|
new Error('create message error'),
|
@@ -434,4 +448,344 @@ describe('generateAIChatV2 actions', () => {
|
|
434
448
|
expect(mockState.refreshMessages).toHaveBeenCalled();
|
435
449
|
});
|
436
450
|
});
|
451
|
+
|
452
|
+
describe('Error handling tests', () => {
|
453
|
+
it('should set error message when sendMessageInServer throws a regular error', async () => {
|
454
|
+
const { result } = renderHook(() => useChatStore());
|
455
|
+
const errorMessage = 'Network error';
|
456
|
+
const mockError = new TRPCClientError(errorMessage);
|
457
|
+
(mockError as any).data = { code: 'BAD_REQUEST' };
|
458
|
+
|
459
|
+
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(mockError);
|
460
|
+
|
461
|
+
await act(async () => {
|
462
|
+
await result.current.sendMessage({ message: 'test' });
|
463
|
+
});
|
464
|
+
|
465
|
+
const operationKey = messageMapKey('session-id', 'topic-id');
|
466
|
+
expect(result.current.mainSendMessageOperations[operationKey]?.inputSendErrorMsg).toBe(
|
467
|
+
errorMessage,
|
468
|
+
);
|
469
|
+
});
|
470
|
+
|
471
|
+
it('should not set error message when receiving a cancel signal', async () => {
|
472
|
+
const { result } = renderHook(() => useChatStore());
|
473
|
+
const abortError = new Error('AbortError');
|
474
|
+
abortError.name = 'AbortError';
|
475
|
+
|
476
|
+
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(abortError);
|
477
|
+
|
478
|
+
await act(async () => {
|
479
|
+
await result.current.sendMessage({ message: 'test' });
|
480
|
+
});
|
481
|
+
|
482
|
+
const operationKey = messageMapKey('session-id', 'topic-id');
|
483
|
+
expect(
|
484
|
+
result.current.mainSendMessageOperations[operationKey]?.inputSendErrorMsg,
|
485
|
+
).toBeUndefined();
|
486
|
+
});
|
487
|
+
});
|
488
|
+
|
489
|
+
describe('Topic switching tests', () => {
|
490
|
+
it('should automatically switch to newly created topic when no active topic exists', async () => {
|
491
|
+
const { result } = renderHook(() => useChatStore());
|
492
|
+
const mockSwitchTopic = vi.fn();
|
493
|
+
|
494
|
+
await act(async () => {
|
495
|
+
useChatStore.setState({
|
496
|
+
...mockState,
|
497
|
+
activeTopicId: undefined,
|
498
|
+
switchTopic: mockSwitchTopic,
|
499
|
+
});
|
500
|
+
await result.current.sendMessage({ message: 'test' });
|
501
|
+
});
|
502
|
+
|
503
|
+
expect(mockSwitchTopic).toHaveBeenCalledWith('topic-id', true);
|
504
|
+
});
|
505
|
+
|
506
|
+
it('should not need to switch topic when active topic exists', async () => {
|
507
|
+
const { result } = renderHook(() => useChatStore());
|
508
|
+
const mockSwitchTopic = vi.fn();
|
509
|
+
|
510
|
+
await act(async () => {
|
511
|
+
useChatStore.setState({
|
512
|
+
...mockState,
|
513
|
+
switchTopic: mockSwitchTopic,
|
514
|
+
});
|
515
|
+
await result.current.sendMessage({ message: 'test' });
|
516
|
+
});
|
517
|
+
|
518
|
+
expect(mockSwitchTopic).not.toHaveBeenCalled();
|
519
|
+
});
|
520
|
+
});
|
521
|
+
|
522
|
+
describe('Cancel send message tests', () => {
|
523
|
+
it('should correctly cancel the current active send operation', () => {
|
524
|
+
const { result } = renderHook(() => useChatStore());
|
525
|
+
const mockAbort = vi.fn();
|
526
|
+
const mockSetJSONState = vi.fn();
|
527
|
+
|
528
|
+
act(() => {
|
529
|
+
useChatStore.setState({
|
530
|
+
activeId: 'session-1',
|
531
|
+
activeTopicId: 'topic-1',
|
532
|
+
mainSendMessageOperations: {
|
533
|
+
[messageMapKey('session-1', 'topic-1')]: {
|
534
|
+
isLoading: true,
|
535
|
+
abortController: { abort: mockAbort, signal: {} as any },
|
536
|
+
inputEditorTempState: { content: 'saved content' },
|
537
|
+
},
|
538
|
+
},
|
539
|
+
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
540
|
+
});
|
541
|
+
});
|
542
|
+
|
543
|
+
act(() => {
|
544
|
+
result.current.cancelSendMessageInServer();
|
545
|
+
});
|
546
|
+
|
547
|
+
expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessageInServer operation');
|
548
|
+
expect(
|
549
|
+
result.current.mainSendMessageOperations[messageMapKey('session-1', 'topic-1')]?.isLoading,
|
550
|
+
).toBe(false);
|
551
|
+
});
|
552
|
+
|
553
|
+
it('should cancel the operation for the corresponding topic when topic ID is specified', () => {
|
554
|
+
const { result } = renderHook(() => useChatStore());
|
555
|
+
const mockAbort = vi.fn();
|
556
|
+
|
557
|
+
act(() => {
|
558
|
+
useChatStore.setState({
|
559
|
+
activeId: 'session-1',
|
560
|
+
mainSendMessageOperations: {
|
561
|
+
[messageMapKey('session-1', 'topic-2')]: {
|
562
|
+
isLoading: true,
|
563
|
+
abortController: { abort: mockAbort, signal: {} as any },
|
564
|
+
},
|
565
|
+
},
|
566
|
+
});
|
567
|
+
});
|
568
|
+
|
569
|
+
act(() => {
|
570
|
+
result.current.cancelSendMessageInServer('topic-2');
|
571
|
+
});
|
572
|
+
|
573
|
+
expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessageInServer operation');
|
574
|
+
});
|
575
|
+
|
576
|
+
it('should handle safely without throwing error when operation does not exist', () => {
|
577
|
+
const { result } = renderHook(() => useChatStore());
|
578
|
+
|
579
|
+
act(() => {
|
580
|
+
useChatStore.setState({ mainSendMessageOperations: {} });
|
581
|
+
});
|
582
|
+
|
583
|
+
expect(() => {
|
584
|
+
act(() => {
|
585
|
+
result.current.cancelSendMessageInServer('non-existing-topic');
|
586
|
+
});
|
587
|
+
}).not.toThrow();
|
588
|
+
});
|
589
|
+
});
|
590
|
+
|
591
|
+
describe('Clear send error tests', () => {
|
592
|
+
it('should correctly clear error state for current topic', () => {
|
593
|
+
const { result } = renderHook(() => useChatStore());
|
594
|
+
|
595
|
+
act(() => {
|
596
|
+
useChatStore.setState({
|
597
|
+
activeId: 'session-1',
|
598
|
+
activeTopicId: 'topic-1',
|
599
|
+
mainSendMessageOperations: {
|
600
|
+
[messageMapKey('session-1', 'topic-1')]: {
|
601
|
+
isLoading: false,
|
602
|
+
inputSendErrorMsg: 'Some error',
|
603
|
+
},
|
604
|
+
},
|
605
|
+
});
|
606
|
+
});
|
607
|
+
|
608
|
+
act(() => {
|
609
|
+
result.current.clearSendMessageError();
|
610
|
+
});
|
611
|
+
|
612
|
+
expect(
|
613
|
+
result.current.mainSendMessageOperations[messageMapKey('session-1', 'topic-1')],
|
614
|
+
).toBeUndefined();
|
615
|
+
});
|
616
|
+
|
617
|
+
it('should handle safely when no error operation exists', () => {
|
618
|
+
const { result } = renderHook(() => useChatStore());
|
619
|
+
|
620
|
+
act(() => {
|
621
|
+
useChatStore.setState({ mainSendMessageOperations: {} });
|
622
|
+
});
|
623
|
+
|
624
|
+
expect(() => {
|
625
|
+
act(() => {
|
626
|
+
result.current.clearSendMessageError();
|
627
|
+
});
|
628
|
+
}).not.toThrow();
|
629
|
+
});
|
630
|
+
});
|
631
|
+
|
632
|
+
describe('Operation state management tests', () => {
|
633
|
+
it('should correctly create new send operation', () => {
|
634
|
+
const { result } = renderHook(() => useChatStore());
|
635
|
+
let abortController: AbortController | undefined;
|
636
|
+
|
637
|
+
act(() => {
|
638
|
+
abortController = result.current.internal_toggleSendMessageOperation('test-key', true);
|
639
|
+
});
|
640
|
+
|
641
|
+
expect(abortController!).toBeInstanceOf(AbortController);
|
642
|
+
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(true);
|
643
|
+
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBe(
|
644
|
+
abortController,
|
645
|
+
);
|
646
|
+
});
|
647
|
+
|
648
|
+
it('should correctly stop send operation', () => {
|
649
|
+
const { result } = renderHook(() => useChatStore());
|
650
|
+
const mockAbortController = { abort: vi.fn() } as any;
|
651
|
+
|
652
|
+
let abortController: AbortController | undefined;
|
653
|
+
act(() => {
|
654
|
+
result.current.internal_updateSendMessageOperation('test-key', {
|
655
|
+
isLoading: true,
|
656
|
+
abortController: mockAbortController,
|
657
|
+
});
|
658
|
+
|
659
|
+
abortController = result.current.internal_toggleSendMessageOperation('test-key', false);
|
660
|
+
});
|
661
|
+
|
662
|
+
expect(abortController).toBeUndefined();
|
663
|
+
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(false);
|
664
|
+
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBeNull();
|
665
|
+
});
|
666
|
+
|
667
|
+
it('should correctly handle cancel reason and call abort method', () => {
|
668
|
+
const { result } = renderHook(() => useChatStore());
|
669
|
+
const mockAbortController = { abort: vi.fn() } as any;
|
670
|
+
|
671
|
+
result.current.internal_updateSendMessageOperation('test-key', {
|
672
|
+
isLoading: true,
|
673
|
+
abortController: mockAbortController,
|
674
|
+
});
|
675
|
+
|
676
|
+
result.current.internal_toggleSendMessageOperation('test-key', false, 'Test cancel reason');
|
677
|
+
|
678
|
+
expect(mockAbortController.abort).toHaveBeenCalledWith('Test cancel reason');
|
679
|
+
});
|
680
|
+
|
681
|
+
it('should support multiple parallel operations', () => {
|
682
|
+
const { result } = renderHook(() => useChatStore());
|
683
|
+
|
684
|
+
let abortController1, abortController2;
|
685
|
+
act(() => {
|
686
|
+
abortController1 = result.current.internal_toggleSendMessageOperation('pkey1', true);
|
687
|
+
abortController2 = result.current.internal_toggleSendMessageOperation('pkey2', true);
|
688
|
+
});
|
689
|
+
|
690
|
+
expect(result.current.mainSendMessageOperations['pkey1']?.isLoading).toBe(true);
|
691
|
+
expect(result.current.mainSendMessageOperations['pkey2']?.isLoading).toBe(true);
|
692
|
+
expect(abortController1).not.toBe(abortController2);
|
693
|
+
});
|
694
|
+
});
|
695
|
+
|
696
|
+
describe('Send operation state update tests', () => {
|
697
|
+
it('should correctly update operation state', () => {
|
698
|
+
const { result } = renderHook(() => useChatStore());
|
699
|
+
const mockAbortController = new AbortController();
|
700
|
+
|
701
|
+
act(() => {
|
702
|
+
result.current.internal_updateSendMessageOperation('abc', {
|
703
|
+
isLoading: true,
|
704
|
+
abortController: mockAbortController,
|
705
|
+
inputSendErrorMsg: 'test error',
|
706
|
+
});
|
707
|
+
});
|
708
|
+
|
709
|
+
expect(result.current.mainSendMessageOperations['abc']).toEqual({
|
710
|
+
isLoading: true,
|
711
|
+
abortController: mockAbortController,
|
712
|
+
inputSendErrorMsg: 'test error',
|
713
|
+
});
|
714
|
+
});
|
715
|
+
|
716
|
+
it('should support partial update of operation state', () => {
|
717
|
+
const { result } = renderHook(() => useChatStore());
|
718
|
+
const initialController = new AbortController();
|
719
|
+
|
720
|
+
act(() => {
|
721
|
+
result.current.internal_updateSendMessageOperation('test-key', {
|
722
|
+
isLoading: true,
|
723
|
+
abortController: initialController,
|
724
|
+
});
|
725
|
+
|
726
|
+
// Only update error message
|
727
|
+
result.current.internal_updateSendMessageOperation('test-key', {
|
728
|
+
inputSendErrorMsg: 'new error',
|
729
|
+
});
|
730
|
+
});
|
731
|
+
|
732
|
+
expect(result.current.mainSendMessageOperations['test-key']).toEqual({
|
733
|
+
isLoading: true,
|
734
|
+
abortController: initialController,
|
735
|
+
inputSendErrorMsg: 'new error',
|
736
|
+
});
|
737
|
+
});
|
738
|
+
});
|
739
|
+
|
740
|
+
describe('Editor state recovery tests', () => {
|
741
|
+
it('should restore editor content when cancelling operation', () => {
|
742
|
+
const { result } = renderHook(() => useChatStore());
|
743
|
+
const mockSetJSONState = vi.fn();
|
744
|
+
const mockAbort = vi.fn();
|
745
|
+
|
746
|
+
act(() => {
|
747
|
+
useChatStore.setState({
|
748
|
+
activeId: 'session-1',
|
749
|
+
activeTopicId: 'topic-1',
|
750
|
+
mainSendMessageOperations: {
|
751
|
+
[messageMapKey('session-1', 'topic-1')]: {
|
752
|
+
isLoading: true,
|
753
|
+
abortController: { abort: mockAbort, signal: {} as any },
|
754
|
+
inputEditorTempState: { content: 'saved content' },
|
755
|
+
},
|
756
|
+
},
|
757
|
+
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
758
|
+
});
|
759
|
+
});
|
760
|
+
|
761
|
+
act(() => {
|
762
|
+
result.current.cancelSendMessageInServer();
|
763
|
+
});
|
764
|
+
|
765
|
+
expect(mockSetJSONState).toHaveBeenCalledWith({ content: 'saved content' });
|
766
|
+
});
|
767
|
+
|
768
|
+
it('should not restore when no saved editor state exists', () => {
|
769
|
+
const { result } = renderHook(() => useChatStore());
|
770
|
+
const mockSetJSONState = vi.fn();
|
771
|
+
const mockAbort = vi.fn();
|
772
|
+
|
773
|
+
act(() => {
|
774
|
+
useChatStore.setState({
|
775
|
+
activeId: 'session-1',
|
776
|
+
activeTopicId: 'topic-1',
|
777
|
+
mainSendMessageOperations: {
|
778
|
+
[messageMapKey('session-1', 'topic-1')]: {
|
779
|
+
isLoading: true,
|
780
|
+
abortController: { abort: mockAbort, signal: {} as any },
|
781
|
+
},
|
782
|
+
},
|
783
|
+
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
784
|
+
});
|
785
|
+
result.current.cancelSendMessageInServer();
|
786
|
+
});
|
787
|
+
|
788
|
+
expect(mockSetJSONState).not.toHaveBeenCalled();
|
789
|
+
});
|
790
|
+
});
|
437
791
|
});
|