@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/ar/common.json +6 -0
  3. package/locales/bg-BG/common.json +6 -0
  4. package/locales/de-DE/common.json +6 -0
  5. package/locales/en-US/common.json +6 -0
  6. package/locales/es-ES/common.json +6 -0
  7. package/locales/fr-FR/common.json +6 -0
  8. package/locales/it-IT/common.json +6 -0
  9. package/locales/ja-JP/common.json +6 -0
  10. package/locales/ko-KR/common.json +6 -0
  11. package/locales/nl-NL/common.json +6 -0
  12. package/locales/pl-PL/common.json +6 -0
  13. package/locales/pt-BR/common.json +6 -0
  14. package/locales/ru-RU/common.json +6 -0
  15. package/locales/tr-TR/common.json +6 -0
  16. package/locales/vi-VN/common.json +6 -0
  17. package/locales/zh-CN/common.json +6 -0
  18. package/locales/zh-TW/common.json +6 -0
  19. package/package.json +1 -1
  20. package/src/app/(main)/(mobile)/me/features/AvatarBanner.tsx +2 -3
  21. package/src/app/(main)/(mobile)/me/loading.tsx +19 -2
  22. package/src/app/(main)/(mobile)/me/page.tsx +6 -1
  23. package/src/database/client/models/topic.ts +4 -0
  24. package/src/features/Conversation/Error/APIKeyForm/index.tsx +1 -1
  25. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  26. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  27. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  28. package/src/features/Conversation/Error/PluginSettings.tsx +1 -1
  29. package/src/features/User/DataStatistics.tsx +153 -0
  30. package/src/features/User/UserPanel/PanelContent.tsx +2 -0
  31. package/src/locales/default/common.ts +6 -1
  32. package/src/services/message/client.ts +9 -0
  33. package/src/services/session/client.ts +1 -0
  34. package/src/services/topic/client.test.ts +28 -0
  35. package/src/services/topic/client.ts +4 -0
  36. package/src/store/chat/slices/enchance/action.ts +4 -4
  37. package/src/store/chat/slices/message/action.test.ts +56 -39
  38. package/src/store/chat/slices/message/action.ts +66 -66
  39. package/src/store/chat/slices/plugin/action.test.ts +26 -26
  40. package/src/store/chat/slices/plugin/action.ts +14 -14
  41. package/src/store/chat/slices/tool/action.test.ts +3 -3
  42. 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 { toggleChatLoading, updateMessageTranslate, dispatchMessage } = get();
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
- toggleChatLoading(true, id, n('translateMessage(start)', { id }) as string);
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
- dispatchMessage({
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
- toggleChatLoading(false);
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().coreProcessMessage;
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
- coreProcessMessage: vi.fn(),
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.coreProcessMessage).not.toHaveBeenCalled();
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.coreProcessMessage).not.toHaveBeenCalled();
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.coreProcessMessage).not.toHaveBeenCalled();
220
+ expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
221
221
  });
222
222
 
223
- it('should create message and call coreProcessMessage if message or files are provided', async () => {
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.coreProcessMessage).toHaveBeenCalled();
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('internalResendMessage action', () => {
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 coreProcessMessage function to resolve immediately
381
- mockState.coreProcessMessage.mockResolvedValue(undefined);
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.internalResendMessage(messageId);
384
+ await result.current.internal_resendMessage(messageId);
385
385
  });
386
386
 
387
387
  expect(messageService.removeMessage).not.toHaveBeenCalledWith(messageId);
388
- expect(mockState.coreProcessMessage).toHaveBeenCalledWith(expect.any(Array), messageId, {});
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.internalResendMessage(messageId);
405
+ await result.current.internal_resendMessage(messageId);
402
406
  });
403
407
 
404
408
  expect(messageService.removeMessage).not.toHaveBeenCalledWith(messageId);
405
- expect(mockState.coreProcessMessage).not.toHaveBeenCalled();
409
+ expect(mockState.internal_coreProcessMessage).not.toHaveBeenCalled();
406
410
  expect(mockState.refreshMessages).not.toHaveBeenCalled();
407
411
  });
408
412
  });
409
413
 
410
- describe('internalUpdateMessageContent action', () => {
411
- it('should call messageService.internalUpdateMessageContent with correct parameters', async () => {
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.internalUpdateMessageContent(messageId, newContent);
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 dispatchMessageSpy = vi.spyOn(result.current, 'dispatchMessage');
431
+ const internal_dispatchMessageSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
428
432
 
429
433
  await act(async () => {
430
- await result.current.internalUpdateMessageContent(messageId, newContent);
434
+ await result.current.internal_updateMessageContent(messageId, newContent);
431
435
  });
432
436
 
433
- expect(dispatchMessageSpy).toHaveBeenCalledWith({
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.internalUpdateMessageContent(messageId, newContent);
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('coreProcessMessage action', () => {
458
+ describe('internal_coreProcessMessage action', () => {
455
459
  it('should handle the core AI message processing', async () => {
456
- useChatStore.setState({ coreProcessMessage: realCoreProcessMessage });
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.coreProcessMessage(messages, userMessage.id);
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 toggleChatLoadingSpy = vi.spyOn(result.current, 'toggleChatLoading');
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(toggleChatLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
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('fetchAIChatMessage', () => {
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.fetchAIChatMessage(messages, assistantMessageId);
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.fetchAIChatMessage(messages, assistantMessageId);
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.fetchAIChatMessage(messages, assistantMessageId);
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.fetchAIChatMessage(messages, assistantMessageId),
687
+ result.current.internal_fetchAIChatMessage(messages, assistantMessageId),
671
688
  ).rejects.toThrow(errorMessage);
672
689
  });
673
690
  });
674
691
  });
675
692
 
676
- describe('toggleChatLoading', () => {
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.toggleChatLoading(true, 'message-id', action);
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.toggleChatLoading(true, 'message-id', 'start-loading-action');
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.toggleChatLoading(false, undefined, action);
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.toggleChatLoading(true, 'message-id', 'loading-action');
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.toggleChatLoading(true, 'message-id', 'start-loading-action');
727
- result.current.toggleChatLoading(false, undefined, 'stop-loading-action');
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.toggleChatLoading(true, 'message-id', 'loading-action');
756
+ result.current.internal_toggleChatLoading(true, 'message-id', 'loading-action');
740
757
  });
741
758
 
742
759
  const state = useChatStore.getState();