@lobehub/chat 0.154.6 → 0.155.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/locales/ar/common.json +6 -0
- package/locales/bg-BG/common.json +6 -0
- package/locales/de-DE/common.json +6 -0
- package/locales/en-US/common.json +6 -0
- package/locales/es-ES/common.json +6 -0
- package/locales/fr-FR/common.json +6 -0
- package/locales/it-IT/common.json +6 -0
- package/locales/ja-JP/common.json +6 -0
- package/locales/ko-KR/common.json +6 -0
- package/locales/nl-NL/common.json +6 -0
- package/locales/pl-PL/common.json +6 -0
- package/locales/pt-BR/common.json +6 -0
- package/locales/ru-RU/common.json +6 -0
- package/locales/tr-TR/common.json +6 -0
- package/locales/vi-VN/common.json +6 -0
- package/locales/zh-CN/common.json +6 -0
- package/locales/zh-TW/common.json +6 -0
- package/package.json +1 -1
- package/src/app/(main)/(mobile)/me/features/AvatarBanner.tsx +2 -3
- package/src/app/(main)/(mobile)/me/loading.tsx +19 -2
- package/src/app/(main)/(mobile)/me/page.tsx +6 -1
- package/src/database/client/models/topic.ts +4 -0
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +1 -1
- package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
- package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
- package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
- package/src/features/Conversation/Error/PluginSettings.tsx +1 -1
- package/src/features/User/DataStatistics.tsx +153 -0
- package/src/features/User/UserPanel/PanelContent.tsx +2 -0
- package/src/locales/default/common.ts +6 -1
- package/src/services/message/client.ts +9 -0
- package/src/services/session/client.ts +1 -0
- package/src/services/topic/client.test.ts +28 -0
- package/src/services/topic/client.ts +4 -0
- package/src/store/chat/slices/enchance/action.ts +4 -4
- package/src/store/chat/slices/message/action.test.ts +56 -39
- package/src/store/chat/slices/message/action.ts +66 -66
- package/src/store/chat/slices/plugin/action.test.ts +26 -26
- package/src/store/chat/slices/plugin/action.ts +14 -14
- package/src/store/chat/slices/tool/action.test.ts +3 -3
- package/src/store/chat/slices/tool/action.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { SessionModel } from '@/database/client/models/session';
|
|
3
4
|
import { CreateTopicParams, TopicModel } from '@/database/client/models/topic';
|
|
4
5
|
import { ChatTopic } from '@/types/topic';
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ vi.mock('@/database/client/models/topic', () => {
|
|
|
13
14
|
create: vi.fn(),
|
|
14
15
|
query: vi.fn(),
|
|
15
16
|
delete: vi.fn(),
|
|
17
|
+
count: vi.fn(),
|
|
16
18
|
batchDeleteBySessionId: vi.fn(),
|
|
17
19
|
batchDelete: vi.fn(),
|
|
18
20
|
clearTable: vi.fn(),
|
|
@@ -214,4 +216,30 @@ describe('TopicService', () => {
|
|
|
214
216
|
expect(result).toBe(mockTopics);
|
|
215
217
|
});
|
|
216
218
|
});
|
|
219
|
+
|
|
220
|
+
describe('countTopics', () => {
|
|
221
|
+
it('should return false if no topics exist', async () => {
|
|
222
|
+
// Setup
|
|
223
|
+
(TopicModel.count as Mock).mockResolvedValue(0);
|
|
224
|
+
|
|
225
|
+
// Execute
|
|
226
|
+
const result = await topicService.countTopics();
|
|
227
|
+
|
|
228
|
+
// Assert
|
|
229
|
+
expect(TopicModel.count).toHaveBeenCalled();
|
|
230
|
+
expect(result).toBe(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should return true if topics exist', async () => {
|
|
234
|
+
// Setup
|
|
235
|
+
(TopicModel.count as Mock).mockResolvedValue(1);
|
|
236
|
+
|
|
237
|
+
// Execute
|
|
238
|
+
const result = await topicService.countTopics();
|
|
239
|
+
|
|
240
|
+
// Assert
|
|
241
|
+
expect(TopicModel.count).toHaveBeenCalled();
|
|
242
|
+
expect(result).toBe(1);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
217
245
|
});
|
|
@@ -34,6 +34,10 @@ export class ClientService implements ITopicService {
|
|
|
34
34
|
return TopicModel.queryAll();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
async countTopics() {
|
|
38
|
+
return TopicModel.count();
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
async updateTopicFavorite(id: string, favorite?: boolean) {
|
|
38
42
|
return this.updateTopic(id, { favorite });
|
|
39
43
|
}
|
|
@@ -49,7 +49,7 @@ export const chatEnhance: StateCreator<
|
|
|
49
49
|
...data,
|
|
50
50
|
}),
|
|
51
51
|
translateMessage: async (id, targetLang) => {
|
|
52
|
-
const {
|
|
52
|
+
const { internal_toggleChatLoading, updateMessageTranslate, internal_dispatchMessage } = get();
|
|
53
53
|
|
|
54
54
|
const message = chatSelectors.getMessageById(id)(get());
|
|
55
55
|
if (!message) return;
|
|
@@ -57,7 +57,7 @@ export const chatEnhance: StateCreator<
|
|
|
57
57
|
// create translate extra
|
|
58
58
|
await updateMessageTranslate(id, { content: '', from: '', to: targetLang });
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
internal_toggleChatLoading(true, id, n('translateMessage(start)', { id }) as string);
|
|
61
61
|
|
|
62
62
|
let content = '';
|
|
63
63
|
let from = '';
|
|
@@ -77,7 +77,7 @@ export const chatEnhance: StateCreator<
|
|
|
77
77
|
// translate to target language
|
|
78
78
|
await chatService.fetchPresetTaskResult({
|
|
79
79
|
onMessageHandle: (text) => {
|
|
80
|
-
|
|
80
|
+
internal_dispatchMessage({
|
|
81
81
|
id,
|
|
82
82
|
key: 'translate',
|
|
83
83
|
type: 'updateMessageExtra',
|
|
@@ -93,7 +93,7 @@ export const chatEnhance: StateCreator<
|
|
|
93
93
|
|
|
94
94
|
await updateMessageTranslate(id, { content, from, to: targetLang });
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
internal_toggleChatLoading(false);
|
|
97
97
|
},
|
|
98
98
|
|
|
99
99
|
ttsMessage: async (id, state = {}) => {
|
|
@@ -53,7 +53,7 @@ vi.mock('@/store/chat/selectors', () => ({
|
|
|
53
53
|
},
|
|
54
54
|
}));
|
|
55
55
|
|
|
56
|
-
const realCoreProcessMessage = useChatStore.getState().
|
|
56
|
+
const realCoreProcessMessage = useChatStore.getState().internal_coreProcessMessage;
|
|
57
57
|
const realRefreshMessages = useChatStore.getState().refreshMessages;
|
|
58
58
|
// Mock state
|
|
59
59
|
const mockState = {
|
|
@@ -62,7 +62,7 @@ const mockState = {
|
|
|
62
62
|
messages: [],
|
|
63
63
|
refreshMessages: vi.fn(),
|
|
64
64
|
refreshTopic: vi.fn(),
|
|
65
|
-
|
|
65
|
+
internal_coreProcessMessage: vi.fn(),
|
|
66
66
|
saveToTopic: vi.fn(),
|
|
67
67
|
};
|
|
68
68
|
|
|
@@ -191,7 +191,7 @@ describe('chatMessage actions', () => {
|
|
|
191
191
|
|
|
192
192
|
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
193
193
|
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
|
194
|
-
expect(result.current.
|
|
194
|
+
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
it('should not send message if message is empty and there are no files', async () => {
|
|
@@ -204,7 +204,7 @@ describe('chatMessage actions', () => {
|
|
|
204
204
|
|
|
205
205
|
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
206
206
|
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
|
207
|
-
expect(result.current.
|
|
207
|
+
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
it('should not send message if message is empty and there are empty files', async () => {
|
|
@@ -217,10 +217,10 @@ describe('chatMessage actions', () => {
|
|
|
217
217
|
|
|
218
218
|
expect(messageService.createMessage).not.toHaveBeenCalled();
|
|
219
219
|
expect(result.current.refreshMessages).not.toHaveBeenCalled();
|
|
220
|
-
expect(result.current.
|
|
220
|
+
expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
it('should create message and call
|
|
223
|
+
it('should create message and call internal_coreProcessMessage if message or files are provided', async () => {
|
|
224
224
|
const { result } = renderHook(() => useChatStore());
|
|
225
225
|
const message = 'Test message';
|
|
226
226
|
const files = [{ id: 'file-id', url: 'file-url' }];
|
|
@@ -240,7 +240,7 @@ describe('chatMessage actions', () => {
|
|
|
240
240
|
topicId: mockState.activeTopicId,
|
|
241
241
|
});
|
|
242
242
|
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
243
|
-
expect(result.current.
|
|
243
|
+
expect(result.current.internal_coreProcessMessage).toHaveBeenCalled();
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
describe('auto-create topic', () => {
|
|
@@ -365,7 +365,7 @@ describe('chatMessage actions', () => {
|
|
|
365
365
|
});
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
-
describe('
|
|
368
|
+
describe('internal_resendMessage action', () => {
|
|
369
369
|
it('should resend a message by id and refresh messages', async () => {
|
|
370
370
|
const { result } = renderHook(() => useChatStore());
|
|
371
371
|
const messageId = 'message-id';
|
|
@@ -377,15 +377,19 @@ describe('chatMessage actions', () => {
|
|
|
377
377
|
// ... other messages
|
|
378
378
|
]);
|
|
379
379
|
|
|
380
|
-
// Mock the
|
|
381
|
-
mockState.
|
|
380
|
+
// Mock the internal_coreProcessMessage function to resolve immediately
|
|
381
|
+
mockState.internal_coreProcessMessage.mockResolvedValue(undefined);
|
|
382
382
|
|
|
383
383
|
await act(async () => {
|
|
384
|
-
await result.current.
|
|
384
|
+
await result.current.internal_resendMessage(messageId);
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
expect(messageService.removeMessage).not.toHaveBeenCalledWith(messageId);
|
|
388
|
-
expect(mockState.
|
|
388
|
+
expect(mockState.internal_coreProcessMessage).toHaveBeenCalledWith(
|
|
389
|
+
expect.any(Array),
|
|
390
|
+
messageId,
|
|
391
|
+
{},
|
|
392
|
+
);
|
|
389
393
|
});
|
|
390
394
|
|
|
391
395
|
it('should not perform any action if the message id does not exist', async () => {
|
|
@@ -398,23 +402,23 @@ describe('chatMessage actions', () => {
|
|
|
398
402
|
]);
|
|
399
403
|
|
|
400
404
|
await act(async () => {
|
|
401
|
-
await result.current.
|
|
405
|
+
await result.current.internal_resendMessage(messageId);
|
|
402
406
|
});
|
|
403
407
|
|
|
404
408
|
expect(messageService.removeMessage).not.toHaveBeenCalledWith(messageId);
|
|
405
|
-
expect(mockState.
|
|
409
|
+
expect(mockState.internal_coreProcessMessage).not.toHaveBeenCalled();
|
|
406
410
|
expect(mockState.refreshMessages).not.toHaveBeenCalled();
|
|
407
411
|
});
|
|
408
412
|
});
|
|
409
413
|
|
|
410
|
-
describe('
|
|
411
|
-
it('should call messageService.
|
|
414
|
+
describe('internal_updateMessageContent action', () => {
|
|
415
|
+
it('should call messageService.internal_updateMessageContent with correct parameters', async () => {
|
|
412
416
|
const { result } = renderHook(() => useChatStore());
|
|
413
417
|
const messageId = 'message-id';
|
|
414
418
|
const newContent = 'Updated content';
|
|
415
419
|
|
|
416
420
|
await act(async () => {
|
|
417
|
-
await result.current.
|
|
421
|
+
await result.current.internal_updateMessageContent(messageId, newContent);
|
|
418
422
|
});
|
|
419
423
|
|
|
420
424
|
expect(messageService.updateMessage).toHaveBeenCalledWith(messageId, { content: newContent });
|
|
@@ -424,13 +428,13 @@ describe('chatMessage actions', () => {
|
|
|
424
428
|
const { result } = renderHook(() => useChatStore());
|
|
425
429
|
const messageId = 'message-id';
|
|
426
430
|
const newContent = 'Updated content';
|
|
427
|
-
const
|
|
431
|
+
const internal_dispatchMessageSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
|
|
428
432
|
|
|
429
433
|
await act(async () => {
|
|
430
|
-
await result.current.
|
|
434
|
+
await result.current.internal_updateMessageContent(messageId, newContent);
|
|
431
435
|
});
|
|
432
436
|
|
|
433
|
-
expect(
|
|
437
|
+
expect(internal_dispatchMessageSpy).toHaveBeenCalledWith({
|
|
434
438
|
id: messageId,
|
|
435
439
|
key: 'content',
|
|
436
440
|
type: 'updateMessage',
|
|
@@ -444,16 +448,16 @@ describe('chatMessage actions', () => {
|
|
|
444
448
|
const newContent = 'Updated content';
|
|
445
449
|
|
|
446
450
|
await act(async () => {
|
|
447
|
-
await result.current.
|
|
451
|
+
await result.current.internal_updateMessageContent(messageId, newContent);
|
|
448
452
|
});
|
|
449
453
|
|
|
450
454
|
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
451
455
|
});
|
|
452
456
|
});
|
|
453
457
|
|
|
454
|
-
describe('
|
|
458
|
+
describe('internal_coreProcessMessage action', () => {
|
|
455
459
|
it('should handle the core AI message processing', async () => {
|
|
456
|
-
useChatStore.setState({
|
|
460
|
+
useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
|
|
457
461
|
|
|
458
462
|
const { result } = renderHook(() => useChatStore());
|
|
459
463
|
const userMessage = {
|
|
@@ -473,7 +477,7 @@ describe('chatMessage actions', () => {
|
|
|
473
477
|
(messageService.createMessage as Mock).mockResolvedValue('assistant-message-id');
|
|
474
478
|
|
|
475
479
|
await act(async () => {
|
|
476
|
-
await result.current.
|
|
480
|
+
await result.current.internal_coreProcessMessage(messages, userMessage.id);
|
|
477
481
|
});
|
|
478
482
|
|
|
479
483
|
// 验证是否创建了代表 AI 响应的消息
|
|
@@ -499,7 +503,7 @@ describe('chatMessage actions', () => {
|
|
|
499
503
|
describe('stopGenerateMessage action', () => {
|
|
500
504
|
it('should stop generating message and set loading states correctly', async () => {
|
|
501
505
|
const { result } = renderHook(() => useChatStore());
|
|
502
|
-
const
|
|
506
|
+
const internal_toggleChatLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
|
|
503
507
|
const abortController = new AbortController();
|
|
504
508
|
|
|
505
509
|
act(() => {
|
|
@@ -511,7 +515,11 @@ describe('chatMessage actions', () => {
|
|
|
511
515
|
});
|
|
512
516
|
|
|
513
517
|
expect(abortController.signal.aborted).toBe(true);
|
|
514
|
-
expect(
|
|
518
|
+
expect(internal_toggleChatLoadingSpy).toHaveBeenCalledWith(
|
|
519
|
+
false,
|
|
520
|
+
undefined,
|
|
521
|
+
expect.any(String),
|
|
522
|
+
);
|
|
515
523
|
});
|
|
516
524
|
|
|
517
525
|
it('should not do anything if there is no abortController', async () => {
|
|
@@ -598,7 +606,7 @@ describe('chatMessage actions', () => {
|
|
|
598
606
|
});
|
|
599
607
|
});
|
|
600
608
|
|
|
601
|
-
describe('
|
|
609
|
+
describe('internal_fetchAIChatMessage', () => {
|
|
602
610
|
it('should fetch AI chat message and return content', async () => {
|
|
603
611
|
const { result } = renderHook(() => useChatStore());
|
|
604
612
|
const messages = [{ id: 'message-id', content: 'Hello', role: 'user' }] as ChatMessage[];
|
|
@@ -608,7 +616,10 @@ describe('chatMessage actions', () => {
|
|
|
608
616
|
(fetch as Mock).mockResolvedValueOnce(new Response(aiResponse));
|
|
609
617
|
|
|
610
618
|
await act(async () => {
|
|
611
|
-
const response = await result.current.
|
|
619
|
+
const response = await result.current.internal_fetchAIChatMessage(
|
|
620
|
+
messages,
|
|
621
|
+
assistantMessageId,
|
|
622
|
+
);
|
|
612
623
|
expect(response.content).toEqual(aiResponse);
|
|
613
624
|
expect(response.isFunctionCall).toEqual(false);
|
|
614
625
|
expect(response.functionCallAtEnd).toEqual(false);
|
|
@@ -627,7 +638,10 @@ describe('chatMessage actions', () => {
|
|
|
627
638
|
vi.mocked(fetch).mockResolvedValueOnce(new Response(aiResponse));
|
|
628
639
|
|
|
629
640
|
await act(async () => {
|
|
630
|
-
const response = await result.current.
|
|
641
|
+
const response = await result.current.internal_fetchAIChatMessage(
|
|
642
|
+
messages,
|
|
643
|
+
assistantMessageId,
|
|
644
|
+
);
|
|
631
645
|
expect(response.content).toEqual(aiResponse);
|
|
632
646
|
expect(response.isFunctionCall).toEqual(true);
|
|
633
647
|
expect(response.functionCallAtEnd).toEqual(false);
|
|
@@ -646,7 +660,10 @@ describe('chatMessage actions', () => {
|
|
|
646
660
|
vi.mocked(fetch).mockResolvedValue(new Response(aiResponse));
|
|
647
661
|
|
|
648
662
|
await act(async () => {
|
|
649
|
-
const response = await result.current.
|
|
663
|
+
const response = await result.current.internal_fetchAIChatMessage(
|
|
664
|
+
messages,
|
|
665
|
+
assistantMessageId,
|
|
666
|
+
);
|
|
650
667
|
expect(response.content).toEqual(aiResponse);
|
|
651
668
|
expect(response.isFunctionCall).toEqual(true);
|
|
652
669
|
expect(response.functionCallAtEnd).toEqual(true);
|
|
@@ -667,19 +684,19 @@ describe('chatMessage actions', () => {
|
|
|
667
684
|
|
|
668
685
|
await act(async () => {
|
|
669
686
|
await expect(
|
|
670
|
-
result.current.
|
|
687
|
+
result.current.internal_fetchAIChatMessage(messages, assistantMessageId),
|
|
671
688
|
).rejects.toThrow(errorMessage);
|
|
672
689
|
});
|
|
673
690
|
});
|
|
674
691
|
});
|
|
675
692
|
|
|
676
|
-
describe('
|
|
693
|
+
describe('internal_toggleChatLoading', () => {
|
|
677
694
|
it('should set loading state and create an AbortController when loading is true', () => {
|
|
678
695
|
const { result } = renderHook(() => useChatStore());
|
|
679
696
|
const action = 'loading-action';
|
|
680
697
|
|
|
681
698
|
act(() => {
|
|
682
|
-
result.current.
|
|
699
|
+
result.current.internal_toggleChatLoading(true, 'message-id', action);
|
|
683
700
|
});
|
|
684
701
|
|
|
685
702
|
const state = useChatStore.getState();
|
|
@@ -693,12 +710,12 @@ describe('chatMessage actions', () => {
|
|
|
693
710
|
|
|
694
711
|
// Set initial loading state
|
|
695
712
|
act(() => {
|
|
696
|
-
result.current.
|
|
713
|
+
result.current.internal_toggleChatLoading(true, 'message-id', 'start-loading-action');
|
|
697
714
|
});
|
|
698
715
|
|
|
699
716
|
// Stop loading
|
|
700
717
|
act(() => {
|
|
701
|
-
result.current.
|
|
718
|
+
result.current.internal_toggleChatLoading(false, undefined, action);
|
|
702
719
|
});
|
|
703
720
|
|
|
704
721
|
const state = useChatStore.getState();
|
|
@@ -711,7 +728,7 @@ describe('chatMessage actions', () => {
|
|
|
711
728
|
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
|
712
729
|
|
|
713
730
|
act(() => {
|
|
714
|
-
result.current.
|
|
731
|
+
result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
|
|
715
732
|
});
|
|
716
733
|
|
|
717
734
|
expect(addEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
|
|
@@ -723,8 +740,8 @@ describe('chatMessage actions', () => {
|
|
|
723
740
|
|
|
724
741
|
// Start and then stop loading to trigger the removal of the event listener
|
|
725
742
|
act(() => {
|
|
726
|
-
result.current.
|
|
727
|
-
result.current.
|
|
743
|
+
result.current.internal_toggleChatLoading(true, 'message-id', 'start-loading-action');
|
|
744
|
+
result.current.internal_toggleChatLoading(false, undefined, 'stop-loading-action');
|
|
728
745
|
});
|
|
729
746
|
|
|
730
747
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
|
|
@@ -736,7 +753,7 @@ describe('chatMessage actions', () => {
|
|
|
736
753
|
|
|
737
754
|
act(() => {
|
|
738
755
|
useChatStore.setState({ abortController });
|
|
739
|
-
result.current.
|
|
756
|
+
result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
|
|
740
757
|
});
|
|
741
758
|
|
|
742
759
|
const state = useChatStore.getState();
|