@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.
Files changed (149) hide show
  1. package/.env.example +5 -0
  2. package/CHANGELOG.md +58 -0
  3. package/Dockerfile +2 -0
  4. package/Dockerfile.database +2 -0
  5. package/Dockerfile.pglite +2 -0
  6. package/changelog/v1.json +21 -0
  7. package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
  8. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
  9. package/locales/ar/chat.json +8 -2
  10. package/locales/ar/editor.json +47 -0
  11. package/locales/bg-BG/chat.json +8 -2
  12. package/locales/bg-BG/editor.json +47 -0
  13. package/locales/de-DE/chat.json +8 -2
  14. package/locales/de-DE/editor.json +47 -0
  15. package/locales/en-US/chat.json +8 -2
  16. package/locales/en-US/editor.json +47 -0
  17. package/locales/es-ES/chat.json +8 -2
  18. package/locales/es-ES/editor.json +47 -0
  19. package/locales/es-ES/models.json +3 -1
  20. package/locales/fa-IR/chat.json +8 -2
  21. package/locales/fa-IR/editor.json +47 -0
  22. package/locales/fr-FR/chat.json +8 -2
  23. package/locales/fr-FR/editor.json +47 -0
  24. package/locales/it-IT/chat.json +8 -2
  25. package/locales/it-IT/editor.json +47 -0
  26. package/locales/ja-JP/chat.json +8 -2
  27. package/locales/ja-JP/editor.json +47 -0
  28. package/locales/ko-KR/chat.json +8 -2
  29. package/locales/ko-KR/editor.json +47 -0
  30. package/locales/ko-KR/models.json +3 -1
  31. package/locales/nl-NL/chat.json +8 -2
  32. package/locales/nl-NL/editor.json +47 -0
  33. package/locales/nl-NL/models.json +3 -1
  34. package/locales/pl-PL/chat.json +8 -2
  35. package/locales/pl-PL/editor.json +47 -0
  36. package/locales/pt-BR/chat.json +8 -2
  37. package/locales/pt-BR/editor.json +47 -0
  38. package/locales/ru-RU/chat.json +8 -2
  39. package/locales/ru-RU/editor.json +47 -0
  40. package/locales/tr-TR/chat.json +8 -2
  41. package/locales/tr-TR/editor.json +47 -0
  42. package/locales/vi-VN/chat.json +8 -2
  43. package/locales/vi-VN/editor.json +47 -0
  44. package/locales/zh-CN/chat.json +8 -2
  45. package/locales/zh-CN/editor.json +47 -0
  46. package/locales/zh-CN/modelProvider.json +1 -1
  47. package/locales/zh-TW/chat.json +8 -2
  48. package/locales/zh-TW/editor.json +47 -0
  49. package/locales/zh-TW/models.json +3 -1
  50. package/next.config.ts +4 -0
  51. package/package.json +4 -2
  52. package/packages/const/src/layoutTokens.ts +1 -0
  53. package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
  54. package/packages/model-bank/src/aiModels/groq.ts +26 -8
  55. package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
  56. package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
  57. package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
  58. package/packages/model-bank/src/aiModels/novita.ts +40 -9
  59. package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
  60. package/packages/model-bank/src/aiModels/qwen.ts +62 -1
  61. package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
  62. package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
  63. package/packages/model-runtime/src/newapi/index.test.ts +49 -42
  64. package/packages/model-runtime/src/newapi/index.ts +124 -143
  65. package/packages/types/src/index.ts +1 -0
  66. package/packages/utils/src/index.ts +1 -0
  67. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
  68. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
  69. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
  70. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
  71. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
  72. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
  73. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
  74. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
  75. package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
  76. package/src/config/llm.ts +8 -0
  77. package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
  78. package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
  79. package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
  80. package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
  81. package/src/features/ChatInput/ActionBar/config.ts +7 -1
  82. package/src/features/ChatInput/ActionBar/index.tsx +40 -51
  83. package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
  84. package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
  85. package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
  86. package/src/features/ChatInput/Desktop/index.tsx +94 -69
  87. package/src/features/ChatInput/InputEditor/index.tsx +134 -0
  88. package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
  89. package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
  90. package/src/features/ChatInput/Mobile/index.tsx +72 -0
  91. package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
  92. package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
  93. package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
  94. package/src/features/ChatInput/SendArea/index.tsx +36 -0
  95. package/src/features/ChatInput/StoreUpdater.tsx +41 -0
  96. package/src/features/ChatInput/TypoBar/index.tsx +139 -0
  97. package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
  98. package/src/features/ChatInput/index.ts +7 -0
  99. package/src/features/ChatInput/store/action.ts +75 -0
  100. package/src/features/ChatInput/store/index.ts +23 -0
  101. package/src/features/ChatInput/store/initialState.ts +54 -0
  102. package/src/features/ChatInput/store/selectors.ts +5 -0
  103. package/src/features/Conversation/components/BackBottom/style.ts +1 -1
  104. package/src/features/Conversation/components/SkeletonList.tsx +10 -3
  105. package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
  106. package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
  107. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
  108. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
  109. package/src/features/Portal/Thread/Chat/index.tsx +2 -2
  110. package/src/features/Portal/Thread/Header/index.tsx +1 -1
  111. package/src/hooks/useHotkeys/chatScope.ts +5 -3
  112. package/src/layout/GlobalProvider/Editor.tsx +27 -0
  113. package/src/layout/GlobalProvider/Locale.tsx +3 -23
  114. package/src/locales/default/chat.ts +8 -2
  115. package/src/locales/default/editor.ts +47 -0
  116. package/src/locales/default/index.ts +2 -0
  117. package/src/locales/default/modelProvider.ts +1 -1
  118. package/src/services/aiChat.ts +8 -2
  119. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
  120. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
  121. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
  122. package/src/store/chat/slices/aiChat/initialState.ts +19 -0
  123. package/src/store/chat/slices/aiChat/selectors.ts +18 -0
  124. package/src/store/global/action.test.ts +6 -5
  125. package/src/store/global/actions/__tests__/general.test.ts +6 -6
  126. package/src/store/global/actions/workspacePane.ts +6 -0
  127. package/src/store/global/initialState.ts +2 -4
  128. package/src/store/global/selectors/systemStatus.test.ts +1 -2
  129. package/src/store/global/selectors/systemStatus.ts +2 -5
  130. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
  131. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
  132. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
  133. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
  134. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
  135. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
  136. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
  137. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
  138. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
  139. package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
  140. package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
  141. package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
  142. package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
  143. package/src/features/ChatInput/useSend.ts +0 -102
  144. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
  145. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
  146. package/src/libs/trpc/client/types.ts +0 -18
  147. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
  148. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
  149. /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
- newAssistantMessage: {
192
- model: DEFAULT_MODEL,
193
- provider: DEFAULT_PROVIDER,
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
- newUserMessage: {
196
- content: message,
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(' isWelcomeQuestion true 时,正确地传递给 internal_execAgentRuntime', async () => {
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('当只有文件而没有消息内容时,正确发送消息', async () => {
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
- newAssistantMessage: {
275
- model: DEFAULT_MODEL,
276
- provider: DEFAULT_PROVIDER,
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
- newUserMessage: {
279
- content: '',
280
- files: ['file-1'],
281
- },
282
- sessionId: 'session-id',
283
- topicId: 'topic-id',
284
- });
294
+ expect.anything(),
295
+ );
285
296
  });
286
297
 
287
- it('当同时有文件和消息内容时,正确发送消息并关联文件', async () => {
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
- newAssistantMessage: {
296
- model: DEFAULT_MODEL,
297
- provider: DEFAULT_PROVIDER,
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
- newUserMessage: {
300
- content: 'test',
301
- files: ['file-1'],
302
- },
303
- sessionId: 'session-id',
304
- topicId: 'topic-id',
305
- });
318
+ expect.anything(),
319
+ );
306
320
  });
307
321
 
308
- it(' createMessage 抛出错误时,正确处理错误而不影响整个应用', async () => {
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
  });