@lobehub/chat 1.124.0 → 1.124.2
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/.github/scripts/pr-comment.js +11 -2
- package/.github/workflows/desktop-pr-build.yml +86 -12
- package/.github/workflows/release-desktop-beta.yml +91 -20
- package/CHANGELOG.md +58 -0
- package/Dockerfile +2 -0
- package/Dockerfile.database +2 -0
- package/Dockerfile.pglite +2 -0
- package/apps/desktop/electron-builder.js +8 -4
- 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 +2 -0
- package/locales/bg-BG/chat.json +2 -0
- package/locales/de-DE/chat.json +2 -0
- package/locales/en-US/chat.json +2 -0
- package/locales/es-ES/chat.json +2 -0
- package/locales/fa-IR/chat.json +2 -0
- package/locales/fr-FR/chat.json +2 -0
- package/locales/it-IT/chat.json +2 -0
- package/locales/ja-JP/chat.json +2 -0
- package/locales/ko-KR/chat.json +2 -0
- package/locales/nl-NL/chat.json +2 -0
- package/locales/pl-PL/chat.json +2 -0
- package/locales/pt-BR/chat.json +2 -0
- package/locales/ru-RU/chat.json +2 -0
- package/locales/tr-TR/chat.json +2 -0
- package/locales/vi-VN/chat.json +2 -0
- package/locales/zh-CN/chat.json +2 -0
- package/locales/zh-CN/modelProvider.json +1 -1
- package/locales/zh-TW/chat.json +2 -0
- package/package.json +1 -1
- package/packages/const/src/hotkeys.ts +1 -1
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/settings/hotkey.ts +3 -2
- package/packages/const/src/trace.ts +1 -1
- package/packages/const/src/user.ts +1 -2
- package/packages/database/src/client/db.test.ts +19 -13
- package/packages/electron-server-ipc/src/ipcClient.test.ts +783 -1
- package/packages/file-loaders/src/loadFile.test.ts +61 -0
- package/packages/file-loaders/src/utils/isTextReadableFile.test.ts +43 -0
- package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -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/package.json +2 -1
- package/packages/model-runtime/src/ai21/index.test.ts +2 -2
- package/packages/model-runtime/src/ai360/index.test.ts +2 -2
- package/packages/model-runtime/src/akashchat/index.test.ts +19 -0
- package/packages/model-runtime/src/anthropic/index.test.ts +1 -2
- package/packages/model-runtime/src/baichuan/index.test.ts +1 -2
- package/packages/model-runtime/src/bedrock/index.test.ts +1 -2
- package/packages/model-runtime/src/bfl/createImage.test.ts +1 -2
- package/packages/model-runtime/src/bfl/index.test.ts +1 -2
- package/packages/model-runtime/src/cloudflare/index.test.ts +1 -2
- package/packages/model-runtime/src/cohere/index.test.ts +19 -0
- package/packages/model-runtime/src/deepseek/index.test.ts +2 -2
- package/packages/model-runtime/src/fireworksai/index.test.ts +2 -2
- package/packages/model-runtime/src/giteeai/index.test.ts +2 -2
- package/packages/model-runtime/src/github/index.test.ts +2 -2
- package/packages/model-runtime/src/google/createImage.test.ts +1 -2
- package/packages/model-runtime/src/google/index.test.ts +1 -1
- package/packages/model-runtime/src/groq/index.test.ts +2 -3
- package/packages/model-runtime/src/huggingface/index.test.ts +40 -0
- package/packages/model-runtime/src/hunyuan/index.test.ts +2 -3
- package/packages/model-runtime/src/internlm/index.test.ts +2 -2
- package/packages/model-runtime/src/jina/index.test.ts +19 -0
- package/packages/model-runtime/src/lmstudio/index.test.ts +2 -2
- package/packages/model-runtime/src/minimax/index.test.ts +19 -0
- package/packages/model-runtime/src/mistral/index.test.ts +2 -3
- package/packages/model-runtime/src/modelscope/index.test.ts +19 -0
- package/packages/model-runtime/src/moonshot/index.test.ts +1 -2
- package/packages/model-runtime/src/nebius/index.test.ts +19 -0
- package/packages/model-runtime/src/newapi/index.test.ts +49 -42
- package/packages/model-runtime/src/newapi/index.ts +124 -143
- package/packages/model-runtime/src/novita/index.test.ts +3 -4
- package/packages/model-runtime/src/nvidia/index.test.ts +19 -0
- package/packages/model-runtime/src/openrouter/index.test.ts +2 -3
- package/packages/model-runtime/src/perplexity/index.test.ts +2 -3
- package/packages/model-runtime/src/ppio/index.test.ts +3 -4
- package/packages/model-runtime/src/qwen/index.test.ts +2 -2
- package/packages/model-runtime/src/sambanova/index.test.ts +19 -0
- package/packages/model-runtime/src/search1api/index.test.ts +19 -0
- package/packages/model-runtime/src/sensenova/index.test.ts +2 -2
- package/packages/model-runtime/src/spark/index.test.ts +2 -2
- package/packages/model-runtime/src/stepfun/index.test.ts +2 -2
- package/packages/model-runtime/src/taichu/index.test.ts +4 -5
- package/packages/model-runtime/src/tencentcloud/index.test.ts +1 -1
- package/packages/model-runtime/src/togetherai/index.test.ts +1 -2
- package/packages/model-runtime/src/upstage/index.test.ts +1 -2
- package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.test.ts +9 -7
- package/packages/model-runtime/src/utils/streams/anthropic.ts +2 -2
- package/packages/model-runtime/src/utils/streams/openai/openai.ts +20 -13
- package/packages/model-runtime/src/utils/streams/openai/responsesStream.test.ts +1 -2
- package/packages/model-runtime/src/utils/streams/openai/responsesStream.ts +2 -2
- package/packages/model-runtime/src/utils/streams/protocol.ts +2 -2
- package/packages/model-runtime/src/wenxin/index.test.ts +2 -3
- package/packages/model-runtime/src/xai/index.test.ts +2 -2
- package/packages/model-runtime/src/zeroone/index.test.ts +1 -2
- package/packages/model-runtime/src/zhipu/index.test.ts +2 -3
- package/packages/model-runtime/vitest.config.mts +0 -7
- package/packages/types/src/index.ts +2 -0
- package/packages/types/src/message/base.ts +1 -1
- package/packages/types/src/openai/chat.ts +2 -3
- package/packages/utils/package.json +2 -1
- package/packages/utils/src/_deprecated/parseModels.test.ts +1 -1
- package/packages/utils/src/_deprecated/parseModels.ts +1 -1
- package/packages/utils/src/client/topic.test.ts +1 -2
- package/packages/utils/src/client/topic.ts +1 -2
- package/packages/utils/src/electron/desktopRemoteRPCFetch.ts +1 -1
- package/packages/utils/src/fetch/fetchSSE.ts +7 -8
- package/packages/utils/src/fetch/parseError.ts +1 -3
- package/packages/utils/src/format.test.ts +1 -2
- package/packages/utils/src/index.ts +1 -0
- package/packages/utils/src/toolManifest.ts +1 -2
- package/packages/utils/src/trace.ts +1 -1
- package/packages/utils/vitest.config.mts +1 -1
- package/packages/web-crawler/src/__tests__/urlRules.test.ts +275 -0
- package/packages/web-crawler/src/crawImpl/__tests__/exa.test.ts +269 -0
- package/packages/web-crawler/src/crawImpl/__tests__/firecrawl.test.ts +284 -0
- package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +234 -0
- package/packages/web-crawler/src/crawImpl/__tests__/tavily.test.ts +359 -0
- package/packages/web-crawler/src/utils/__tests__/errorType.test.ts +217 -0
- package/packages/web-crawler/vitest.config.mts +3 -0
- package/scripts/electronWorkflow/mergeMacReleaseFiles.ts +207 -0
- package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
- package/src/components/Thinking/index.tsx +2 -3
- package/src/config/llm.ts +8 -0
- package/src/features/ChatInput/Desktop/index.tsx +16 -4
- package/src/features/ChatInput/StoreUpdater.tsx +2 -0
- package/src/libs/traces/index.ts +1 -1
- package/src/locales/default/chat.ts +1 -0
- package/src/locales/default/modelProvider.ts +1 -1
- package/src/server/modules/ModelRuntime/trace.ts +1 -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 +352 -7
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +2 -1
- package/packages/model-runtime/src/openrouter/__snapshots__/index.test.ts.snap +0 -113
@@ -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
|
|
@@ -252,7 +257,7 @@ describe('generateAIChatV2 actions', () => {
|
|
252
257
|
expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
|
253
258
|
});
|
254
259
|
|
255
|
-
it('
|
260
|
+
it('should pass isWelcomeQuestion correctly to internal_execAgentRuntime when isWelcomeQuestion is true', async () => {
|
256
261
|
const { result } = renderHook(() => useChatStore());
|
257
262
|
|
258
263
|
await act(async () => {
|
@@ -266,7 +271,7 @@ describe('generateAIChatV2 actions', () => {
|
|
266
271
|
);
|
267
272
|
});
|
268
273
|
|
269
|
-
it('
|
274
|
+
it('should send message correctly when only files are provided without message content', async () => {
|
270
275
|
const { result } = renderHook(() => useChatStore());
|
271
276
|
|
272
277
|
await act(async () => {
|
@@ -290,7 +295,7 @@ describe('generateAIChatV2 actions', () => {
|
|
290
295
|
);
|
291
296
|
});
|
292
297
|
|
293
|
-
it('
|
298
|
+
it('should send message correctly when both files and message content are provided', async () => {
|
294
299
|
const { result } = renderHook(() => useChatStore());
|
295
300
|
|
296
301
|
await act(async () => {
|
@@ -314,7 +319,7 @@ describe('generateAIChatV2 actions', () => {
|
|
314
319
|
);
|
315
320
|
});
|
316
321
|
|
317
|
-
it('
|
322
|
+
it('should handle errors correctly when createMessage throws error without affecting the app', async () => {
|
318
323
|
const { result } = renderHook(() => useChatStore());
|
319
324
|
vi.spyOn(aiChatService, 'sendMessageInServer').mockRejectedValue(
|
320
325
|
new Error('create message error'),
|
@@ -443,4 +448,344 @@ describe('generateAIChatV2 actions', () => {
|
|
443
448
|
expect(mockState.refreshMessages).toHaveBeenCalled();
|
444
449
|
});
|
445
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
|
+
});
|
446
791
|
});
|
@@ -260,7 +260,8 @@ export const generateAIChatV2: StateCreator<
|
|
260
260
|
// Only clear creating message state if it's the active session
|
261
261
|
if (operationKey === messageMapKey(activeId, activeTopicId)) {
|
262
262
|
const editorTempState = get().mainSendMessageOperations[operationKey]?.inputEditorTempState;
|
263
|
-
|
263
|
+
|
264
|
+
if (editorTempState) get().mainInputEditor?.setJSONState(editorTempState);
|
264
265
|
}
|
265
266
|
},
|
266
267
|
clearSendMessageError: () => {
|
@@ -1,113 +0,0 @@
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
2
|
-
|
3
|
-
exports[`LobeOpenRouterAI > chat > should handle fetch error gracefully 1`] = `
|
4
|
-
[
|
5
|
-
{
|
6
|
-
"contextWindowTokens": 131072,
|
7
|
-
"description": "Reflection Llama-3.1 70B is trained with a new technique called Reflection-Tuning that teaches a LLM to detect mistakes in its reasoning and correct course.
|
8
|
-
|
9
|
-
The model was trained on synthetic data.
|
10
|
-
|
11
|
-
_These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/reflection-70b). Outputs may be cached. Read about rate limits [here](/docs/limits)._",
|
12
|
-
"displayName": "Reflection 70B",
|
13
|
-
"enabled": false,
|
14
|
-
"functionCall": false,
|
15
|
-
"id": "mattshumer/reflection-70b:free",
|
16
|
-
"maxOutput": undefined,
|
17
|
-
"pricing": undefined,
|
18
|
-
"reasoning": false,
|
19
|
-
"releasedAt": "1970-01-20",
|
20
|
-
"type": "chat",
|
21
|
-
"vision": false,
|
22
|
-
},
|
23
|
-
]
|
24
|
-
`;
|
25
|
-
|
26
|
-
exports[`LobeOpenRouterAI > chat > should handle fetch failure gracefully 1`] = `
|
27
|
-
[
|
28
|
-
{
|
29
|
-
"contextWindowTokens": 131072,
|
30
|
-
"description": "Reflection Llama-3.1 70B is trained with a new technique called Reflection-Tuning that teaches a LLM to detect mistakes in its reasoning and correct course.
|
31
|
-
|
32
|
-
The model was trained on synthetic data.
|
33
|
-
|
34
|
-
_These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/reflection-70b). Outputs may be cached. Read about rate limits [here](/docs/limits)._",
|
35
|
-
"displayName": "Reflection 70B",
|
36
|
-
"enabled": false,
|
37
|
-
"functionCall": false,
|
38
|
-
"id": "mattshumer/reflection-70b:free",
|
39
|
-
"maxOutput": undefined,
|
40
|
-
"pricing": undefined,
|
41
|
-
"reasoning": false,
|
42
|
-
"releasedAt": "1970-01-20",
|
43
|
-
"type": "chat",
|
44
|
-
"vision": false,
|
45
|
-
},
|
46
|
-
]
|
47
|
-
`;
|
48
|
-
|
49
|
-
exports[`LobeOpenRouterAI > models > should get models with frontend models data 1`] = `
|
50
|
-
[
|
51
|
-
{
|
52
|
-
"contextWindowTokens": 131072,
|
53
|
-
"description": "Reflection Llama-3.1 70B is trained with a new technique called Reflection-Tuning that teaches a LLM to detect mistakes in its reasoning and correct course.
|
54
|
-
|
55
|
-
The model was trained on synthetic data.
|
56
|
-
|
57
|
-
_These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/reflection-70b). Outputs may be cached. Read about rate limits [here](/docs/limits)._",
|
58
|
-
"displayName": "Reflection 70B",
|
59
|
-
"enabled": false,
|
60
|
-
"functionCall": true,
|
61
|
-
"id": "mattshumer/reflection-70b:free",
|
62
|
-
"maxOutput": 4096,
|
63
|
-
"reasoning": true,
|
64
|
-
"releasedAt": "2024-09-06",
|
65
|
-
"type": "chat",
|
66
|
-
"vision": false,
|
67
|
-
},
|
68
|
-
]
|
69
|
-
`;
|
70
|
-
|
71
|
-
exports[`LobeOpenRouterAI > models > should handle fetch error gracefully 1`] = `
|
72
|
-
[
|
73
|
-
{
|
74
|
-
"contextWindowTokens": 131072,
|
75
|
-
"description": "Reflection Llama-3.1 70B is trained with a new technique called Reflection-Tuning that teaches a LLM to detect mistakes in its reasoning and correct course.
|
76
|
-
|
77
|
-
The model was trained on synthetic data.
|
78
|
-
|
79
|
-
_These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/reflection-70b). Outputs may be cached. Read about rate limits [here](/docs/limits)._",
|
80
|
-
"displayName": "Reflection 70B",
|
81
|
-
"enabled": false,
|
82
|
-
"functionCall": false,
|
83
|
-
"id": "mattshumer/reflection-70b:free",
|
84
|
-
"maxOutput": 4096,
|
85
|
-
"reasoning": false,
|
86
|
-
"releasedAt": "2024-09-06",
|
87
|
-
"type": "chat",
|
88
|
-
"vision": false,
|
89
|
-
},
|
90
|
-
]
|
91
|
-
`;
|
92
|
-
|
93
|
-
exports[`LobeOpenRouterAI > models > should handle fetch failure gracefully 1`] = `
|
94
|
-
[
|
95
|
-
{
|
96
|
-
"contextWindowTokens": 131072,
|
97
|
-
"description": "Reflection Llama-3.1 70B is trained with a new technique called Reflection-Tuning that teaches a LLM to detect mistakes in its reasoning and correct course.
|
98
|
-
|
99
|
-
The model was trained on synthetic data.
|
100
|
-
|
101
|
-
_These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/reflection-70b). Outputs may be cached. Read about rate limits [here](/docs/limits)._",
|
102
|
-
"displayName": "Reflection 70B",
|
103
|
-
"enabled": false,
|
104
|
-
"functionCall": false,
|
105
|
-
"id": "mattshumer/reflection-70b:free",
|
106
|
-
"maxOutput": 4096,
|
107
|
-
"reasoning": false,
|
108
|
-
"releasedAt": "2024-09-06",
|
109
|
-
"type": "chat",
|
110
|
-
"vision": false,
|
111
|
-
},
|
112
|
-
]
|
113
|
-
`;
|