@lobehub/chat 0.118.7 → 0.118.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.118.7",
3
+ "version": "0.118.9",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -32,6 +32,7 @@
32
32
  "dev": "next dev -p 3010",
33
33
  "docs": "lobe-i18n md && npm run workflow:docs",
34
34
  "i18n": "npm run workflow:i18n && lobe-i18n",
35
+ "i18n-md": "lobe-i18n md",
35
36
  "lint": "npm run lint:ts && npm run lint:style && npm run type-check && npm run lint:circular",
36
37
  "lint:circular": "dpdm src/**/*.ts --warning false --tree false --exit-code circular:1 -T true",
37
38
  "lint:md": "remark . --quiet --frail --output",
@@ -1,6 +1,6 @@
1
1
  import { ActionIcon, ChatInputArea, ChatSendButton } from '@lobehub/ui';
2
2
  import { Maximize2, Minimize2 } from 'lucide-react';
3
- import { memo, useCallback, useMemo } from 'react';
3
+ import { memo, useCallback, useEffect, useMemo } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import {
@@ -78,6 +78,23 @@ const ChatInputDesktopLayout = memo(() => {
78
78
  [expand],
79
79
  );
80
80
 
81
+ const hasValue = !!value;
82
+
83
+ useEffect(() => {
84
+ const fn = (e: BeforeUnloadEvent) => {
85
+ if (hasValue) {
86
+ // set returnValue to trigger alert modal
87
+ // Note: No matter what value is set, the browser will display the standard text
88
+ e.returnValue = '你有正在输入中的内容,确定要离开吗?';
89
+ }
90
+ };
91
+
92
+ window.addEventListener('beforeunload', fn);
93
+ return () => {
94
+ window.removeEventListener('beforeunload', fn);
95
+ };
96
+ }, [hasValue]);
97
+
81
98
  return (
82
99
  <>
83
100
  {canUpload && <DragUpload />}
@@ -12,6 +12,7 @@ export const locales = [
12
12
  'tr-TR',
13
13
  'zh-CN',
14
14
  'zh-TW',
15
+ 'vi-VN',
15
16
  ] as const;
16
17
 
17
18
  export type DefaultResources = typeof resources;
@@ -96,6 +97,10 @@ export const localeOptions: LocaleOptions = [
96
97
  label: 'Italiano',
97
98
  value: 'it-IT',
98
99
  },
100
+ {
101
+ label: 'Tiếng Việt',
102
+ value: 'vi-VN',
103
+ },
99
104
  ] as LocaleOptions;
100
105
 
101
106
  export const supportLocales: string[] = [...locales, 'en', 'zh'];
@@ -666,4 +666,75 @@ describe('chatMessage actions', () => {
666
666
  });
667
667
  });
668
668
  });
669
+
670
+ describe('toggleChatLoading', () => {
671
+ it('should set loading state and create an AbortController when loading is true', () => {
672
+ const { result } = renderHook(() => useChatStore());
673
+ const action = 'loading-action';
674
+
675
+ act(() => {
676
+ result.current.toggleChatLoading(true, 'message-id', action);
677
+ });
678
+
679
+ const state = useChatStore.getState();
680
+ expect(state.abortController).toBeInstanceOf(AbortController);
681
+ expect(state.chatLoadingId).toEqual('message-id');
682
+ });
683
+
684
+ it('should clear loading state and abort controller when loading is false', () => {
685
+ const { result } = renderHook(() => useChatStore());
686
+ const action = 'stop-loading-action';
687
+
688
+ // Set initial loading state
689
+ act(() => {
690
+ result.current.toggleChatLoading(true, 'message-id', 'start-loading-action');
691
+ });
692
+
693
+ // Stop loading
694
+ act(() => {
695
+ result.current.toggleChatLoading(false, undefined, action);
696
+ });
697
+
698
+ const state = useChatStore.getState();
699
+ expect(state.abortController).toBeUndefined();
700
+ expect(state.chatLoadingId).toBeUndefined();
701
+ });
702
+
703
+ it('should attach beforeunload event listener when loading starts', () => {
704
+ const { result } = renderHook(() => useChatStore());
705
+ const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
706
+
707
+ act(() => {
708
+ result.current.toggleChatLoading(true, 'message-id', 'loading-action');
709
+ });
710
+
711
+ expect(addEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
712
+ });
713
+
714
+ it('should remove beforeunload event listener when loading stops', () => {
715
+ const { result } = renderHook(() => useChatStore());
716
+ const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
717
+
718
+ // Start and then stop loading to trigger the removal of the event listener
719
+ act(() => {
720
+ result.current.toggleChatLoading(true, 'message-id', 'start-loading-action');
721
+ result.current.toggleChatLoading(false, undefined, 'stop-loading-action');
722
+ });
723
+
724
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
725
+ });
726
+
727
+ it('should not create a new AbortController if one already exists', () => {
728
+ const { result } = renderHook(() => useChatStore());
729
+ const abortController = new AbortController();
730
+
731
+ act(() => {
732
+ useChatStore.setState({ abortController });
733
+ result.current.toggleChatLoading(true, 'message-id', 'loading-action');
734
+ });
735
+
736
+ const state = useChatStore.getState();
737
+ expect(state.abortController).toEqual(abortController);
738
+ });
739
+ });
669
740
  });
@@ -75,6 +75,12 @@ export interface ChatMessageAction {
75
75
 
76
76
  const getAgentConfig = () => agentSelectors.currentAgentConfig(useSessionStore.getState());
77
77
 
78
+ const preventLeavingFn = (e: BeforeUnloadEvent) => {
79
+ // set returnValue to trigger alert modal
80
+ // Note: No matter what value is set, the browser will display the standard text
81
+ e.returnValue = '你有正在生成中的请求,确定要离开吗?';
82
+ };
83
+
78
84
  export const chatMessage: StateCreator<
79
85
  ChatStore,
80
86
  [['zustand/devtools', never]],
@@ -398,11 +404,16 @@ export const chatMessage: StateCreator<
398
404
  },
399
405
  toggleChatLoading: (loading, id, action) => {
400
406
  if (loading) {
407
+ window.addEventListener('beforeunload', preventLeavingFn);
408
+
401
409
  const abortController = new AbortController();
402
410
  set({ abortController, chatLoadingId: id }, false, action);
411
+
403
412
  return abortController;
404
413
  } else {
405
414
  set({ abortController: undefined, chatLoadingId: undefined }, false, action);
415
+
416
+ window.removeEventListener('beforeunload', preventLeavingFn);
406
417
  }
407
418
  },
408
419
  });