@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,40 +0,0 @@
1
- import { Hotkey, combineKeys } from '@lobehub/ui';
2
- import { useTheme } from 'antd-style';
3
- import { memo } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
- import { Flexbox } from 'react-layout-kit';
6
-
7
- import { useUserStore } from '@/store/user';
8
- import { preferenceSelectors } from '@/store/user/selectors';
9
- import { KeyEnum } from '@/types/hotkey';
10
-
11
- const ShortcutHint = memo(() => {
12
- const { t } = useTranslation('chat');
13
- const theme = useTheme();
14
- const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
15
-
16
- const sendShortcut = useCmdEnterToSend
17
- ? combineKeys([KeyEnum.Mod, KeyEnum.Enter])
18
- : KeyEnum.Enter;
19
-
20
- const wrapperShortcut = useCmdEnterToSend
21
- ? KeyEnum.Enter
22
- : combineKeys([KeyEnum.Mod, KeyEnum.Enter]);
23
-
24
- return (
25
- <Flexbox
26
- align={'center'}
27
- gap={4}
28
- horizontal
29
- style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
30
- >
31
- <Hotkey keys={sendShortcut} style={{ color: 'inherit' }} variant={'borderless'} />
32
- <span>{t('input.send')}</span>
33
- <span>/</span>
34
- <Hotkey keys={wrapperShortcut} style={{ color: 'inherit' }} variant={'borderless'} />
35
- <span>{t('input.warp')}</span>
36
- </Flexbox>
37
- );
38
- });
39
-
40
- export default ShortcutHint;
@@ -1,125 +0,0 @@
1
- import { Button } from '@lobehub/ui';
2
- import { Space } from 'antd';
3
- import { createStyles } from 'antd-style';
4
- import { rgba } from 'polished';
5
- import { Suspense, memo, useEffect, useState } from 'react';
6
- import { useTranslation } from 'react-i18next';
7
- import { Flexbox } from 'react-layout-kit';
8
-
9
- import StopLoadingIcon from '@/components/StopLoading';
10
- import LocalFiles from '@/features/ChatInput/Desktop/FilePreview';
11
- import SaveTopic from '@/features/ChatInput/Topic';
12
- import { useSendMessage } from '@/features/ChatInput/useSend';
13
- import { useChatStore } from '@/store/chat';
14
- import { chatSelectors } from '@/store/chat/selectors';
15
- import { isMacOS } from '@/utils/platform';
16
-
17
- import MessageFromUrl from './MessageFromUrl';
18
- import SendMore from './SendMore';
19
- import ShortcutHint from './ShortcutHint';
20
-
21
- const useStyles = createStyles(({ css, prefixCls, token }) => {
22
- return {
23
- arrow: css`
24
- &.${prefixCls}-btn.${prefixCls}-btn-icon-only {
25
- width: 28px;
26
- }
27
- `,
28
- loadingButton: css`
29
- display: flex;
30
- align-items: center;
31
- `,
32
- overrideAntdIcon: css`
33
- .${prefixCls}-btn.${prefixCls}-btn-icon-only {
34
- display: flex;
35
- align-items: center;
36
- justify-content: center;
37
- }
38
-
39
- .${prefixCls}-btn.${prefixCls}-dropdown-trigger {
40
- &::before {
41
- background-color: ${rgba(token.colorBgLayout, 0.1)} !important;
42
- }
43
- }
44
- `,
45
- };
46
- });
47
-
48
- interface FooterProps {
49
- expand: boolean;
50
- onExpandChange: (expand: boolean) => void;
51
- }
52
-
53
- const Footer = memo<FooterProps>(({ onExpandChange, expand }) => {
54
- const { t } = useTranslation('chat');
55
-
56
- const { styles } = useStyles();
57
-
58
- const [isAIGenerating, stopGenerateMessage] = useChatStore((s) => [
59
- chatSelectors.isAIGenerating(s),
60
- s.stopGenerateMessage,
61
- ]);
62
-
63
- const { send: sendMessage, canSend } = useSendMessage();
64
-
65
- const [isMac, setIsMac] = useState<boolean>();
66
-
67
- useEffect(() => {
68
- setIsMac(isMacOS());
69
- }, [setIsMac]);
70
-
71
- return (
72
- <>
73
- <Suspense>
74
- <MessageFromUrl />
75
- </Suspense>
76
- <Flexbox
77
- align={'end'}
78
- className={styles.overrideAntdIcon}
79
- distribution={'space-between'}
80
- flex={'none'}
81
- gap={8}
82
- horizontal
83
- paddingInline={16}
84
- >
85
- <Flexbox align={'center'} gap={8} horizontal style={{ overflow: 'hidden' }}>
86
- {expand && <LocalFiles />}
87
- </Flexbox>
88
- <Flexbox align={'center'} flex={'none'} gap={8} horizontal>
89
- <ShortcutHint />
90
- <SaveTopic />
91
- <Flexbox style={{ minWidth: 92 }}>
92
- {isAIGenerating ? (
93
- <Button
94
- className={styles.loadingButton}
95
- icon={StopLoadingIcon}
96
- onClick={stopGenerateMessage}
97
- >
98
- {t('input.stop')}
99
- </Button>
100
- ) : (
101
- <Space.Compact>
102
- <Button
103
- disabled={!canSend}
104
- loading={!canSend}
105
- onClick={() => {
106
- sendMessage();
107
- onExpandChange?.(false);
108
- }}
109
- type={'primary'}
110
- >
111
- {t('input.send')}
112
- </Button>
113
- <SendMore disabled={!canSend} isMac={isMac} />
114
- </Space.Compact>
115
- )}
116
- </Flexbox>
117
- </Flexbox>
118
- </Flexbox>
119
- </>
120
- );
121
- });
122
-
123
- Footer.displayName = 'Footer';
124
-
125
- export default Footer;
@@ -1,332 +0,0 @@
1
- import { act, fireEvent, render, screen } from '@testing-library/react';
2
- import React from 'react';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
-
5
- import { useChatStore } from '@/store/chat';
6
- import { useUserStore } from '@/store/user';
7
-
8
- import InputArea from './TextArea';
9
-
10
- let sendMessageMock: () => Promise<void>;
11
-
12
- // Mock the useSendMessage hook to return our mock function
13
- vi.mock('@/features/ChatInput/useSend', () => ({
14
- useSendMessage: () => ({
15
- send: sendMessageMock,
16
- canSend: true,
17
- }),
18
- }));
19
-
20
- // Mock the Chinese warning hook to always allow sending
21
- vi.mock('@/hooks/useGeminiChineseWarning', () => ({
22
- useGeminiChineseWarning: () => () => Promise.resolve(true),
23
- }));
24
-
25
- let onSendMock: () => void;
26
-
27
- beforeEach(() => {
28
- onSendMock = vi.fn();
29
- sendMessageMock = vi.fn().mockResolvedValue(undefined);
30
- vi.clearAllMocks();
31
- });
32
-
33
- describe('<InputArea />', () => {
34
- it('renders the TextArea component correctly', () => {
35
- render(<InputArea />);
36
- const textArea = screen.getByRole('textbox');
37
- expect(textArea).toBeInTheDocument();
38
- });
39
-
40
- it('auto-focuses the TextArea component on mount', () => {
41
- render(<InputArea />);
42
- const textArea = screen.getByRole('textbox');
43
-
44
- // The document's active element should be the textarea if it was auto-focused
45
- expect(document.activeElement).toBe(textArea);
46
- });
47
-
48
- it('renders with correct placeholder text', () => {
49
- render(<InputArea onSend={onSendMock} />);
50
- const textArea = screen.getByPlaceholderText('sendPlaceholder');
51
- expect(textArea).toBeInTheDocument();
52
- });
53
-
54
- it('has the correct initial value', () => {
55
- render(<InputArea onSend={onSendMock} />);
56
- const textArea = screen.getByRole('textbox');
57
- expect(textArea).toHaveValue('');
58
- });
59
-
60
- describe('input behavior', () => {
61
- it('calls updateInputMessage on input change', async () => {
62
- const updateInputMessageMock = vi.fn();
63
- act(() => {
64
- useChatStore.setState({ updateInputMessage: updateInputMessageMock });
65
- });
66
-
67
- render(<InputArea />);
68
-
69
- const textArea = screen.getByRole('textbox');
70
- const newValue = 'New message';
71
- fireEvent.change(textArea, { target: { value: newValue } });
72
-
73
- expect(updateInputMessageMock).toHaveBeenCalledWith(newValue);
74
- });
75
-
76
- it('handles composition events for IME input correctly', () => {
77
- const updateInputMessageMock = vi.fn();
78
- act(() => {
79
- useChatStore.setState({ updateInputMessage: updateInputMessageMock });
80
- });
81
-
82
- render(<InputArea />);
83
- const textArea = screen.getByRole('textbox');
84
-
85
- // Start composition (IME input starts)
86
- fireEvent.compositionStart(textArea);
87
- fireEvent.change(textArea, { target: { value: '正在' } });
88
- expect(updateInputMessageMock).toHaveBeenCalledWith('正在');
89
-
90
- // End composition (IME input ends)
91
- fireEvent.compositionEnd(textArea);
92
- fireEvent.change(textArea, { target: { value: '正在输入' } });
93
- expect(updateInputMessageMock).toHaveBeenCalledWith('正在输入');
94
- });
95
-
96
- it('does not send a message when Enter is pressed during IME composition', () => {
97
- const updateInputMessageMock = vi.fn();
98
- act(() => {
99
- useChatStore.setState({ updateInputMessage: updateInputMessageMock });
100
- });
101
-
102
- render(<InputArea onSend={onSendMock} />);
103
- const textArea = screen.getByRole('textbox');
104
-
105
- // Start composition (IME input starts)
106
- fireEvent.compositionStart(textArea);
107
-
108
- // Simulate pressing Enter during IME composition
109
- fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
110
-
111
- // Since we are in the middle of IME composition, the message should not be sent
112
- expect(onSendMock).not.toHaveBeenCalled();
113
- expect(updateInputMessageMock).not.toHaveBeenCalled();
114
-
115
- // End composition (IME input ends)
116
- fireEvent.compositionEnd(textArea);
117
-
118
- // Now simulate pressing Enter after IME composition
119
- fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
120
-
121
- // Since IME composition has ended, now the message should be sent
122
- expect(onSendMock).toHaveBeenCalled();
123
- expect(updateInputMessageMock).toHaveBeenCalled();
124
- });
125
-
126
- it('updates the input message when TextArea loses focus', () => {
127
- const updateInputMessageMock = vi.fn();
128
- act(() => {
129
- useChatStore.setState({ updateInputMessage: updateInputMessageMock });
130
- });
131
-
132
- render(<InputArea onSend={onSendMock} />);
133
- const textArea = screen.getByRole('textbox');
134
- const newText = 'New input text';
135
-
136
- fireEvent.change(textArea, { target: { value: newText } });
137
- fireEvent.blur(textArea);
138
-
139
- expect(updateInputMessageMock).toHaveBeenCalledWith(newText);
140
- });
141
- });
142
-
143
- describe('leaving protect', () => {
144
- it('triggers a warning when trying to leave the page with unsaved input', () => {
145
- const beforeUnloadEvent = new Event('beforeunload', { cancelable: true });
146
-
147
- act(() => {
148
- useChatStore.setState({ inputMessage: 'Unsaved input' });
149
- });
150
-
151
- render(<InputArea />);
152
-
153
- // trigger beforeunload
154
- window.dispatchEvent(beforeUnloadEvent);
155
-
156
- // 检查 returnValue 是否被设置为 true,这是触发警告的标识
157
- expect(beforeUnloadEvent.returnValue).toBeTruthy();
158
- });
159
-
160
- it('does not trigger a warning when trying to leave the page with empty input', () => {
161
- act(() => {
162
- useChatStore.setState({ inputMessage: '' });
163
- });
164
-
165
- // 模拟 window.addEventListener 来捕获 beforeunload 事件处理程序
166
- const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
167
- const beforeUnloadHandler = vi.fn();
168
-
169
- addEventListenerSpy.mockImplementation((event, handler) => {
170
- // @ts-ignore
171
- if (event === 'beforeunload') {
172
- beforeUnloadHandler.mockImplementation(handler as any);
173
- }
174
- });
175
-
176
- // 渲染组件
177
- render(<InputArea />);
178
-
179
- // 触发 beforeunload 事件
180
- const event = new Event('beforeunload', { cancelable: true });
181
- window.dispatchEvent(event);
182
-
183
- // 检查 beforeunload 事件的处理程序是否没有被调用
184
- expect(beforeUnloadHandler).not.toHaveBeenCalled();
185
-
186
- // 清理模拟
187
- addEventListenerSpy.mockRestore();
188
- });
189
-
190
- describe('cleanup', () => {
191
- it('removes beforeunload listener on unmount', () => {
192
- const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
193
- const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
194
-
195
- // 渲染并立即卸载组件
196
- const { unmount } = render(<InputArea />);
197
- unmount();
198
-
199
- // 检查是否为 beforeunload 事件添加了监听器
200
- expect(addEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
201
-
202
- // 检查是否移除了对应的监听器
203
- expect(removeEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
204
-
205
- // 清理 spy
206
- addEventListenerSpy.mockRestore();
207
- removeEventListenerSpy.mockRestore();
208
- });
209
- });
210
- });
211
-
212
- describe('message sending behavior', () => {
213
- it('does not send message when loading or shift key is pressed', () => {
214
- act(() => {
215
- useChatStore.setState({ chatLoadingIds: ['123'] });
216
- });
217
-
218
- render(<InputArea onSend={onSendMock} />);
219
- const textArea = screen.getByRole('textbox');
220
-
221
- fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', shiftKey: true });
222
- expect(sendMessageMock).not.toHaveBeenCalled();
223
- });
224
-
225
- it('sends message on Enter press when not loading and no shift key', async () => {
226
- act(() => {
227
- useChatStore.setState({
228
- chatLoadingIds: [],
229
- inputMessage: 'abc',
230
- });
231
- });
232
-
233
- render(<InputArea />);
234
- const textArea = screen.getByRole('textbox');
235
- fireEvent.change(textArea, { target: { value: 'Test message' } });
236
-
237
- fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
238
-
239
- await vi.waitFor(() => {
240
- expect(sendMessageMock).toHaveBeenCalled();
241
- });
242
- });
243
-
244
- describe('metaKey behavior for sending messages', () => {
245
- it('windows: sends message on ctrl + enter when useCmdEnterToSend is true', async () => {
246
- act(() => {
247
- useChatStore.setState({
248
- chatLoadingIds: [],
249
- inputMessage: '123',
250
- });
251
- useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
252
- });
253
-
254
- render(<InputArea onSend={onSendMock} />);
255
- const textArea = screen.getByRole('textbox');
256
-
257
- fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
258
-
259
- await vi.waitFor(() => {
260
- expect(sendMessageMock).toHaveBeenCalled();
261
- });
262
- });
263
-
264
- it('windows: inserts a new line on ctrl + enter when useCmdEnterToSend is false', () => {
265
- const updateInputMessageMock = vi.fn();
266
- act(() => {
267
- useChatStore.setState({
268
- chatLoadingIds: [],
269
- inputMessage: 'Test',
270
- updateInputMessage: updateInputMessageMock,
271
- });
272
- useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
273
- });
274
-
275
- render(<InputArea onSend={onSendMock} />);
276
- const textArea = screen.getByRole('textbox');
277
-
278
- fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
279
- expect(updateInputMessageMock).toHaveBeenCalledWith('Test\n');
280
- expect(sendMessageMock).not.toHaveBeenCalled(); // sendMessage should not be called
281
- });
282
-
283
- it('macOS: sends message on cmd + enter when useCmdEnterToSend is true', async () => {
284
- vi.stubGlobal('navigator', {
285
- userAgent:
286
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
287
- });
288
- act(() => {
289
- useChatStore.setState({
290
- chatLoadingIds: [],
291
- inputMessage: '123',
292
- });
293
- useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
294
- });
295
-
296
- render(<InputArea onSend={onSendMock} />);
297
- const textArea = screen.getByRole('textbox');
298
-
299
- fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
300
-
301
- await vi.waitFor(() => {
302
- expect(sendMessageMock).toHaveBeenCalled();
303
- });
304
- vi.restoreAllMocks();
305
- });
306
-
307
- it('macOS: inserts a new line on cmd + enter when useCmdEnterToSend is false', () => {
308
- vi.stubGlobal('navigator', {
309
- userAgent:
310
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
311
- });
312
- const updateInputMessageMock = vi.fn();
313
- act(() => {
314
- useChatStore.setState({
315
- chatLoadingIds: [],
316
- inputMessage: 'Test',
317
- updateInputMessage: updateInputMessageMock,
318
- });
319
- useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
320
- });
321
-
322
- render(<InputArea onSend={onSendMock} />);
323
- const textArea = screen.getByRole('textbox');
324
-
325
- fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
326
- expect(updateInputMessageMock).toHaveBeenCalledWith('Test\n');
327
- expect(sendMessageMock).not.toHaveBeenCalled(); // sendMessage should not be called
328
- vi.restoreAllMocks();
329
- });
330
- });
331
- });
332
- });
@@ -1,29 +0,0 @@
1
- import { memo } from 'react';
2
-
3
- import InputArea from '@/features/ChatInput/Desktop/InputArea';
4
- import { useSendMessage } from '@/features/ChatInput/useSend';
5
- import { useChatStore } from '@/store/chat';
6
- import { chatSelectors } from '@/store/chat/slices/message/selectors';
7
-
8
- const TextArea = memo<{ onSend?: () => void }>(({ onSend }) => {
9
- const [loading, value, updateInputMessage] = useChatStore((s) => [
10
- chatSelectors.isAIGenerating(s),
11
- s.inputMessage,
12
- s.updateInputMessage,
13
- ]);
14
- const { send: sendMessage } = useSendMessage();
15
-
16
- return (
17
- <InputArea
18
- loading={loading}
19
- onChange={updateInputMessage}
20
- onSend={() => {
21
- sendMessage();
22
- onSend?.();
23
- }}
24
- value={value}
25
- />
26
- );
27
- });
28
-
29
- export default TextArea;
@@ -1,33 +0,0 @@
1
- import { PreviewGroup } from '@lobehub/ui';
2
- import isEqual from 'fast-deep-equal';
3
- import { memo } from 'react';
4
- import { Flexbox } from 'react-layout-kit';
5
-
6
- import { filesSelectors, useFileStore } from '@/store/file';
7
-
8
- import FileItem from './FileItem';
9
-
10
- const Files = memo(() => {
11
- const list = useFileStore(filesSelectors.chatUploadFileList, isEqual);
12
-
13
- if (!list || list?.length === 0) return null;
14
-
15
- return (
16
- <Flexbox paddingBlock={4} style={{ position: 'relative' }}>
17
- <Flexbox
18
- gap={4}
19
- horizontal
20
- padding={'4px 8px 8px'}
21
- style={{ overflow: 'scroll', width: '100%' }}
22
- >
23
- <PreviewGroup>
24
- {list.map((i) => (
25
- <FileItem {...i} key={i.id} loading={i.status === 'pending'} />
26
- ))}
27
- </PreviewGroup>
28
- </Flexbox>
29
- </Flexbox>
30
- );
31
- });
32
-
33
- export default Files;
@@ -1,41 +0,0 @@
1
- import { css, cx } from 'antd-style';
2
- import { FC, ReactNode, memo } from 'react';
3
- import { Flexbox } from 'react-layout-kit';
4
-
5
- const container = css`
6
- height: inherit;
7
- padding-block: 0;
8
- padding-inline: 8px;
9
- `;
10
-
11
- interface InnerContainerProps {
12
- bottomAddons?: ReactNode;
13
- children: ReactNode;
14
- expand?: boolean;
15
- textAreaLeftAddons?: ReactNode;
16
- textAreaRightAddons?: ReactNode;
17
- topAddons?: ReactNode;
18
- }
19
-
20
- const InnerContainer: FC<InnerContainerProps> = memo(
21
- ({ children, expand, textAreaRightAddons, textAreaLeftAddons, bottomAddons, topAddons }) =>
22
- expand ? (
23
- <Flexbox className={cx(container)} gap={8}>
24
- <Flexbox gap={8} horizontal justify={'flex-end'}>
25
- {textAreaLeftAddons}
26
- {textAreaRightAddons}
27
- </Flexbox>
28
- {children}
29
- {topAddons}
30
- {bottomAddons}
31
- </Flexbox>
32
- ) : (
33
- <Flexbox align={'flex-end'} className={cx(container)} gap={8} horizontal>
34
- {textAreaLeftAddons}
35
- {children}
36
- {textAreaRightAddons}
37
- </Flexbox>
38
- ),
39
- );
40
-
41
- export default InnerContainer;