@lobehub/chat 1.120.7 → 1.121.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/.cursor/rules/project-structure.mdc +54 -42
- package/.cursor/rules/testing-guide/testing-guide.mdc +28 -17
- package/.env.development +122 -0
- package/.vscode/settings.json +0 -1
- package/CHANGELOG.md +25 -0
- package/CLAUDE.md +3 -4
- package/changelog/v1.json +9 -0
- package/docker-compose/local/init_data.json +981 -1024
- package/docker-compose.development.yml +40 -0
- package/docs/development/basic/work-with-server-side-database.mdx +77 -0
- package/docs/development/basic/work-with-server-side-database.zh-CN.mdx +77 -0
- package/docs/self-hosting/advanced/s3/cloudflare-r2.mdx +1 -1
- package/docs/self-hosting/advanced/s3/cloudflare-r2.zh-CN.mdx +2 -2
- package/locales/zh-CN/common.json +7 -0
- package/package.json +2 -1
- package/packages/database/src/repositories/aiInfra/index.ts +3 -1
- package/packages/model-runtime/src/RouterRuntime/createRuntime.test.ts +6 -91
- package/packages/model-runtime/src/RouterRuntime/createRuntime.ts +6 -28
- package/packages/model-runtime/src/openrouter/index.ts +15 -12
- package/packages/model-runtime/src/openrouter/type.ts +10 -0
- package/packages/model-runtime/src/utils/modelParse.test.ts +66 -0
- package/packages/model-runtime/src/utils/modelParse.ts +15 -3
- package/packages/model-runtime/src/utils/postProcessModelList.ts +1 -0
- package/packages/utils/src/detectChinese.test.ts +37 -0
- package/packages/utils/src/detectChinese.ts +12 -0
- package/packages/utils/src/index.ts +1 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +33 -18
- package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +12 -0
- package/src/features/ChatInput/useSend.ts +14 -2
- package/src/hooks/useGeminiChineseWarning.tsx +91 -0
- package/src/locales/default/common.ts +7 -0
- package/src/store/global/initialState.ts +2 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
/**
|
2
|
+
* Detect if text contains Chinese characters
|
3
|
+
* @param text - The text to check
|
4
|
+
* @returns true if text contains Chinese characters, false otherwise
|
5
|
+
*/
|
6
|
+
export const containsChinese = (text: string): boolean => {
|
7
|
+
// Enhanced regex to cover more Chinese character ranges:
|
8
|
+
// \u4e00-\u9fa5: CJK Unified Ideographs (basic)
|
9
|
+
// \u3400-\u4dbf: CJK Unified Ideographs Extension A
|
10
|
+
// \uf900-\ufaff: CJK Compatibility Ideographs
|
11
|
+
return /[\u3400-\u4DBF\u4E00-\u9FA5\uF900-\uFAFF]/.test(text);
|
12
|
+
};
|
@@ -7,10 +7,27 @@ import { useUserStore } from '@/store/user';
|
|
7
7
|
|
8
8
|
import InputArea from './TextArea';
|
9
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
|
+
|
10
25
|
let onSendMock: () => void;
|
11
26
|
|
12
27
|
beforeEach(() => {
|
13
28
|
onSendMock = vi.fn();
|
29
|
+
sendMessageMock = vi.fn().mockResolvedValue(undefined);
|
30
|
+
vi.clearAllMocks();
|
14
31
|
});
|
15
32
|
|
16
33
|
describe('<InputArea />', () => {
|
@@ -194,9 +211,8 @@ describe('<InputArea />', () => {
|
|
194
211
|
|
195
212
|
describe('message sending behavior', () => {
|
196
213
|
it('does not send message when loading or shift key is pressed', () => {
|
197
|
-
const sendMessageMock = vi.fn();
|
198
214
|
act(() => {
|
199
|
-
useChatStore.setState({ chatLoadingIds: ['123']
|
215
|
+
useChatStore.setState({ chatLoadingIds: ['123'] });
|
200
216
|
});
|
201
217
|
|
202
218
|
render(<InputArea onSend={onSendMock} />);
|
@@ -206,13 +222,11 @@ describe('<InputArea />', () => {
|
|
206
222
|
expect(sendMessageMock).not.toHaveBeenCalled();
|
207
223
|
});
|
208
224
|
|
209
|
-
it('sends message on Enter press when not loading and no shift key', () => {
|
210
|
-
const sendMessageMock = vi.fn();
|
225
|
+
it('sends message on Enter press when not loading and no shift key', async () => {
|
211
226
|
act(() => {
|
212
227
|
useChatStore.setState({
|
213
228
|
chatLoadingIds: [],
|
214
229
|
inputMessage: 'abc',
|
215
|
-
sendMessage: sendMessageMock,
|
216
230
|
});
|
217
231
|
});
|
218
232
|
|
@@ -221,17 +235,18 @@ describe('<InputArea />', () => {
|
|
221
235
|
fireEvent.change(textArea, { target: { value: 'Test message' } });
|
222
236
|
|
223
237
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter' });
|
224
|
-
|
238
|
+
|
239
|
+
await vi.waitFor(() => {
|
240
|
+
expect(sendMessageMock).toHaveBeenCalled();
|
241
|
+
});
|
225
242
|
});
|
226
243
|
|
227
244
|
describe('metaKey behavior for sending messages', () => {
|
228
|
-
it('windows: sends message on ctrl + enter when useCmdEnterToSend is true', () => {
|
229
|
-
const sendMessageMock = vi.fn();
|
245
|
+
it('windows: sends message on ctrl + enter when useCmdEnterToSend is true', async () => {
|
230
246
|
act(() => {
|
231
247
|
useChatStore.setState({
|
232
248
|
chatLoadingIds: [],
|
233
249
|
inputMessage: '123',
|
234
|
-
sendMessage: sendMessageMock,
|
235
250
|
});
|
236
251
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
|
237
252
|
});
|
@@ -240,17 +255,18 @@ describe('<InputArea />', () => {
|
|
240
255
|
const textArea = screen.getByRole('textbox');
|
241
256
|
|
242
257
|
fireEvent.keyDown(textArea, { code: 'Enter', ctrlKey: true, key: 'Enter' });
|
243
|
-
|
258
|
+
|
259
|
+
await vi.waitFor(() => {
|
260
|
+
expect(sendMessageMock).toHaveBeenCalled();
|
261
|
+
});
|
244
262
|
});
|
245
263
|
|
246
264
|
it('windows: inserts a new line on ctrl + enter when useCmdEnterToSend is false', () => {
|
247
|
-
const sendMessageMock = vi.fn();
|
248
265
|
const updateInputMessageMock = vi.fn();
|
249
266
|
act(() => {
|
250
267
|
useChatStore.setState({
|
251
268
|
chatLoadingIds: [],
|
252
269
|
inputMessage: 'Test',
|
253
|
-
sendMessage: sendMessageMock,
|
254
270
|
updateInputMessage: updateInputMessageMock,
|
255
271
|
});
|
256
272
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
|
@@ -264,17 +280,15 @@ describe('<InputArea />', () => {
|
|
264
280
|
expect(sendMessageMock).not.toHaveBeenCalled(); // sendMessage should not be called
|
265
281
|
});
|
266
282
|
|
267
|
-
it('macOS: sends message on cmd + enter when useCmdEnterToSend is true', () => {
|
283
|
+
it('macOS: sends message on cmd + enter when useCmdEnterToSend is true', async () => {
|
268
284
|
vi.stubGlobal('navigator', {
|
269
285
|
userAgent:
|
270
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',
|
271
287
|
});
|
272
|
-
const sendMessageMock = vi.fn();
|
273
288
|
act(() => {
|
274
289
|
useChatStore.setState({
|
275
290
|
chatLoadingIds: [],
|
276
291
|
inputMessage: '123',
|
277
|
-
sendMessage: sendMessageMock,
|
278
292
|
});
|
279
293
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: true });
|
280
294
|
});
|
@@ -283,7 +297,10 @@ describe('<InputArea />', () => {
|
|
283
297
|
const textArea = screen.getByRole('textbox');
|
284
298
|
|
285
299
|
fireEvent.keyDown(textArea, { code: 'Enter', key: 'Enter', metaKey: true });
|
286
|
-
|
300
|
+
|
301
|
+
await vi.waitFor(() => {
|
302
|
+
expect(sendMessageMock).toHaveBeenCalled();
|
303
|
+
});
|
287
304
|
vi.restoreAllMocks();
|
288
305
|
});
|
289
306
|
|
@@ -292,13 +309,11 @@ describe('<InputArea />', () => {
|
|
292
309
|
userAgent:
|
293
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',
|
294
311
|
});
|
295
|
-
const sendMessageMock = vi.fn();
|
296
312
|
const updateInputMessageMock = vi.fn();
|
297
313
|
act(() => {
|
298
314
|
useChatStore.setState({
|
299
315
|
chatLoadingIds: [],
|
300
316
|
inputMessage: 'Test',
|
301
|
-
sendMessage: sendMessageMock,
|
302
317
|
updateInputMessage: updateInputMessageMock,
|
303
318
|
});
|
304
319
|
useUserStore.getState().updatePreference({ useCmdEnterToSend: false });
|
@@ -8,9 +8,11 @@ import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
9
9
|
|
10
10
|
import { loginRequired } from '@/components/Error/loginRequiredNotification';
|
11
|
+
import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
|
11
12
|
import { useImageStore } from '@/store/image';
|
12
13
|
import { createImageSelectors } from '@/store/image/selectors';
|
13
14
|
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
15
|
+
import { imageGenerationConfigSelectors } from '@/store/image/slices/generationConfig/selectors';
|
14
16
|
import { useUserStore } from '@/store/user';
|
15
17
|
import { authSelectors } from '@/store/user/slices/auth/selectors';
|
16
18
|
|
@@ -49,13 +51,23 @@ const PromptInput = ({ showTitle = false }: PromptInputProps) => {
|
|
49
51
|
const { value, setValue } = useGenerationConfigParam('prompt');
|
50
52
|
const isCreating = useImageStore(createImageSelectors.isCreating);
|
51
53
|
const createImage = useImageStore((s) => s.createImage);
|
54
|
+
const currentModel = useImageStore(imageGenerationConfigSelectors.model);
|
52
55
|
const isLogin = useUserStore(authSelectors.isLogin);
|
56
|
+
const checkGeminiChineseWarning = useGeminiChineseWarning();
|
53
57
|
|
54
58
|
const handleGenerate = async () => {
|
55
59
|
if (!isLogin) {
|
56
60
|
loginRequired.redirect({ timeout: 2000 });
|
57
61
|
return;
|
58
62
|
}
|
63
|
+
// Check for Chinese text warning with Gemini model
|
64
|
+
const shouldContinue = await checkGeminiChineseWarning({
|
65
|
+
model: currentModel,
|
66
|
+
prompt: value,
|
67
|
+
scenario: 'image',
|
68
|
+
});
|
69
|
+
|
70
|
+
if (!shouldContinue) return;
|
59
71
|
|
60
72
|
await createImage();
|
61
73
|
};
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { useAnalytics } from '@lobehub/analytics/react';
|
2
2
|
import { useCallback, useMemo } from 'react';
|
3
3
|
|
4
|
+
import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
|
4
5
|
import { getAgentStoreState } from '@/store/agent';
|
5
6
|
import { agentSelectors } from '@/store/agent/selectors';
|
6
7
|
import { useChatStore } from '@/store/chat';
|
@@ -20,6 +21,7 @@ export const useSendMessage = () => {
|
|
20
21
|
s.updateInputMessage,
|
21
22
|
]);
|
22
23
|
const { analytics } = useAnalytics();
|
24
|
+
const checkGeminiChineseWarning = useGeminiChineseWarning();
|
23
25
|
|
24
26
|
const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
|
25
27
|
|
@@ -28,7 +30,7 @@ export const useSendMessage = () => {
|
|
28
30
|
|
29
31
|
const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
|
30
32
|
|
31
|
-
const send = useCallback((params: UseSendMessageParams = {}) => {
|
33
|
+
const send = useCallback(async (params: UseSendMessageParams = {}) => {
|
32
34
|
const store = useChatStore.getState();
|
33
35
|
if (chatSelectors.isAIGenerating(store)) return;
|
34
36
|
|
@@ -45,6 +47,17 @@ export const useSendMessage = () => {
|
|
45
47
|
// if there is no message and no image, then we should not send the message
|
46
48
|
if (!store.inputMessage && fileList.length === 0) return;
|
47
49
|
|
50
|
+
// Check for Chinese text warning with Gemini model
|
51
|
+
const agentStore = getAgentStoreState();
|
52
|
+
const currentModel = agentSelectors.currentAgentModel(agentStore);
|
53
|
+
const shouldContinue = await checkGeminiChineseWarning({
|
54
|
+
model: currentModel,
|
55
|
+
prompt: store.inputMessage,
|
56
|
+
scenario: 'chat',
|
57
|
+
});
|
58
|
+
|
59
|
+
if (!shouldContinue) return;
|
60
|
+
|
48
61
|
sendMessage({
|
49
62
|
files: fileList,
|
50
63
|
message: store.inputMessage,
|
@@ -56,7 +69,6 @@ export const useSendMessage = () => {
|
|
56
69
|
|
57
70
|
// 获取分析数据
|
58
71
|
const userStore = getUserStoreState();
|
59
|
-
const agentStore = getAgentStoreState();
|
60
72
|
|
61
73
|
// 直接使用现有数据结构判断消息类型
|
62
74
|
const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import { containsChinese } from '@lobechat/utils';
|
2
|
+
import { App, Checkbox } from 'antd';
|
3
|
+
import React, { useCallback } from 'react';
|
4
|
+
import { useTranslation } from 'react-i18next';
|
5
|
+
|
6
|
+
import { useGlobalStore } from '@/store/global';
|
7
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
8
|
+
|
9
|
+
const shouldShowChineseWarning = (
|
10
|
+
model: string,
|
11
|
+
prompt: string,
|
12
|
+
hasWarningBeenDismissed: boolean,
|
13
|
+
): boolean => {
|
14
|
+
return (
|
15
|
+
model.includes('gemini-2.5-flash-image-preview') &&
|
16
|
+
!hasWarningBeenDismissed &&
|
17
|
+
Boolean(prompt) &&
|
18
|
+
containsChinese(prompt)
|
19
|
+
);
|
20
|
+
};
|
21
|
+
|
22
|
+
interface UseGeminiChineseWarningOptions {
|
23
|
+
model: string;
|
24
|
+
prompt: string;
|
25
|
+
scenario?: 'chat' | 'image';
|
26
|
+
}
|
27
|
+
|
28
|
+
export const useGeminiChineseWarning = () => {
|
29
|
+
const { t } = useTranslation('common');
|
30
|
+
const { modal } = App.useApp();
|
31
|
+
|
32
|
+
const [hideGeminiChineseWarning, updateSystemStatus] = useGlobalStore((s) => [
|
33
|
+
systemStatusSelectors.systemStatus(s).hideGemini2_5FlashImagePreviewChineseWarning ?? false,
|
34
|
+
s.updateSystemStatus,
|
35
|
+
]);
|
36
|
+
|
37
|
+
const checkWarning = useCallback(
|
38
|
+
async ({
|
39
|
+
model,
|
40
|
+
prompt,
|
41
|
+
scenario = 'chat',
|
42
|
+
}: UseGeminiChineseWarningOptions): Promise<boolean> => {
|
43
|
+
if (!shouldShowChineseWarning(model, prompt, hideGeminiChineseWarning)) {
|
44
|
+
return true;
|
45
|
+
}
|
46
|
+
|
47
|
+
return new Promise<boolean>((resolve) => {
|
48
|
+
let doNotShowAgain = false;
|
49
|
+
|
50
|
+
// 根据场景选择不同的按钮文案
|
51
|
+
const continueText =
|
52
|
+
scenario === 'image'
|
53
|
+
? t('geminiImageChineseWarning.continueGenerate')
|
54
|
+
: t('geminiImageChineseWarning.continueSend');
|
55
|
+
|
56
|
+
modal.confirm({
|
57
|
+
cancelText: t('cancel', { ns: 'common' }),
|
58
|
+
centered: true,
|
59
|
+
content: (
|
60
|
+
<div>
|
61
|
+
<p>{t('geminiImageChineseWarning.content')}</p>
|
62
|
+
<div style={{ marginTop: 16 }}>
|
63
|
+
<Checkbox
|
64
|
+
onChange={(e) => {
|
65
|
+
doNotShowAgain = e.target.checked;
|
66
|
+
}}
|
67
|
+
>
|
68
|
+
{t('geminiImageChineseWarning.doNotShowAgain')}
|
69
|
+
</Checkbox>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
),
|
73
|
+
okText: continueText,
|
74
|
+
onCancel: () => {
|
75
|
+
resolve(false);
|
76
|
+
},
|
77
|
+
onOk: () => {
|
78
|
+
if (doNotShowAgain) {
|
79
|
+
updateSystemStatus({ hideGemini2_5FlashImagePreviewChineseWarning: true });
|
80
|
+
}
|
81
|
+
resolve(true);
|
82
|
+
},
|
83
|
+
title: t('geminiImageChineseWarning.title'),
|
84
|
+
});
|
85
|
+
});
|
86
|
+
},
|
87
|
+
[modal, t, hideGeminiChineseWarning, updateSystemStatus],
|
88
|
+
);
|
89
|
+
|
90
|
+
return checkWarning;
|
91
|
+
};
|
@@ -185,6 +185,13 @@ export default {
|
|
185
185
|
title: '喜欢我们的产品?',
|
186
186
|
},
|
187
187
|
fullscreen: '全屏模式',
|
188
|
+
geminiImageChineseWarning: {
|
189
|
+
content: 'Nano Banana 使用中文有概率性生成图片失败。建议使用英文以获得更好的效果。',
|
190
|
+
continueGenerate: '继续生成',
|
191
|
+
continueSend: '继续发送',
|
192
|
+
doNotShowAgain: '不再提示',
|
193
|
+
title: '中文输入提示',
|
194
|
+
},
|
188
195
|
historyRange: '历史范围',
|
189
196
|
import: '导入',
|
190
197
|
importData: '导入数据',
|
@@ -50,6 +50,7 @@ export interface SystemStatus {
|
|
50
50
|
// which sessionGroup should expand
|
51
51
|
expandSessionGroupKeys: string[];
|
52
52
|
filePanelWidth: number;
|
53
|
+
hideGemini2_5FlashImagePreviewChineseWarning?: boolean;
|
53
54
|
hidePWAInstaller?: boolean;
|
54
55
|
hideThreadLimitAlert?: boolean;
|
55
56
|
imagePanelWidth: number;
|
@@ -108,6 +109,7 @@ export interface GlobalState {
|
|
108
109
|
export const INITIAL_STATUS = {
|
109
110
|
expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
|
110
111
|
filePanelWidth: 320,
|
112
|
+
hideGemini2_5FlashImagePreviewChineseWarning: false,
|
111
113
|
hidePWAInstaller: false,
|
112
114
|
hideThreadLimitAlert: false,
|
113
115
|
imagePanelWidth: 320,
|