@lobehub/chat 1.124.4 → 1.126.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 (52) hide show
  1. package/.cursor/rules/react-component.mdc +1 -0
  2. package/.github/scripts/pr-comment.js +11 -2
  3. package/.github/workflows/auto-i18n.yml +1 -1
  4. package/.github/workflows/desktop-pr-build.yml +103 -23
  5. package/.github/workflows/docker-database.yml +1 -4
  6. package/.github/workflows/release-desktop-beta.yml +101 -24
  7. package/.github/workflows/release.yml +3 -2
  8. package/.github/workflows/test.yml +12 -9
  9. package/CHANGELOG.md +50 -0
  10. package/apps/desktop/electron-builder.js +8 -4
  11. package/changelog/v1.json +14 -0
  12. package/locales/ar/editor.json +7 -0
  13. package/locales/bg-BG/editor.json +7 -0
  14. package/locales/de-DE/editor.json +7 -0
  15. package/locales/en-US/editor.json +7 -0
  16. package/locales/es-ES/editor.json +7 -0
  17. package/locales/fa-IR/editor.json +7 -0
  18. package/locales/fr-FR/editor.json +7 -0
  19. package/locales/it-IT/editor.json +7 -0
  20. package/locales/ja-JP/editor.json +7 -0
  21. package/locales/ko-KR/editor.json +7 -0
  22. package/locales/nl-NL/editor.json +7 -0
  23. package/locales/pl-PL/editor.json +7 -0
  24. package/locales/pt-BR/editor.json +7 -0
  25. package/locales/ru-RU/editor.json +7 -0
  26. package/locales/tr-TR/editor.json +7 -0
  27. package/locales/vi-VN/editor.json +7 -0
  28. package/locales/zh-CN/editor.json +7 -0
  29. package/locales/zh-TW/editor.json +7 -0
  30. package/package.json +2 -2
  31. package/scripts/electronWorkflow/mergeMacReleaseFiles.js +179 -0
  32. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/ClassicChat.tsx +153 -0
  33. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/GroupChat.tsx +153 -0
  34. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +3 -145
  35. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageConfigSkeleton.tsx +53 -0
  36. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +14 -2
  37. package/src/features/ChatInput/InputEditor/index.tsx +20 -5
  38. package/src/features/ChatInput/TypoBar/index.tsx +17 -0
  39. package/src/hooks/useFetchAiImageConfig.ts +49 -0
  40. package/src/locales/default/editor.ts +7 -0
  41. package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
  42. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +199 -140
  43. package/src/store/aiInfra/slices/aiProvider/action.ts +11 -4
  44. package/src/store/aiInfra/slices/aiProvider/initialState.ts +2 -0
  45. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  46. package/src/store/global/initialState.ts +8 -0
  47. package/src/store/global/selectors/systemStatus.ts +5 -3
  48. package/src/store/image/slices/generationConfig/action.test.ts +331 -150
  49. package/src/store/image/slices/generationConfig/action.ts +100 -23
  50. package/src/store/image/slices/generationConfig/initialState.ts +6 -0
  51. package/src/store/image/utils/aspectRatio.test.ts +148 -0
  52. package/src/store/image/utils/aspectRatio.ts +45 -0
@@ -9,8 +9,12 @@ import {
9
9
  import { StateCreator } from 'zustand/vanilla';
10
10
 
11
11
  import { aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
12
+ import { useGlobalStore } from '@/store/global';
13
+ import { useUserStore } from '@/store/user';
14
+ import { authSelectors } from '@/store/user/selectors';
12
15
 
13
16
  import type { ImageStore } from '../../store';
17
+ import { calculateInitialAspectRatio } from '../../utils/aspectRatio';
14
18
  import { adaptSizeToRatio, parseRatio } from '../../utils/size';
15
19
 
16
20
  export interface GenerationConfigAction {
@@ -34,6 +38,13 @@ export interface GenerationConfigAction {
34
38
  setHeight(height: number): void;
35
39
  toggleAspectRatioLock(): void;
36
40
  setAspectRatio(aspectRatio: string): void;
41
+
42
+ // 初始化相关方法
43
+ initializeImageConfig(
44
+ isLogin?: boolean,
45
+ lastSelectedImageModel?: string,
46
+ lastSelectedImageProvider?: string,
47
+ ): void;
37
48
  }
38
49
 
39
50
  /**
@@ -43,9 +54,22 @@ export interface GenerationConfigAction {
43
54
  */
44
55
  export function getModelAndDefaults(model: string, provider: string) {
45
56
  const enabledImageModelList = aiProviderSelectors.enabledImageModelList(getAiInfraStoreState());
46
- const activeModel = enabledImageModelList
47
- .find((providerItem) => providerItem.id === provider)
48
- ?.children.find((modelItem) => modelItem.id === model) as unknown as AIImageModelCard;
57
+
58
+ const providerItem = enabledImageModelList.find((providerItem) => providerItem.id === provider);
59
+ if (!providerItem) {
60
+ throw new Error(
61
+ `Provider "${provider}" not found in enabled image provider list. Available providers: ${enabledImageModelList.map((p) => p.id).join(', ')}`,
62
+ );
63
+ }
64
+
65
+ const activeModel = providerItem.children.find(
66
+ (modelItem) => modelItem.id === model,
67
+ ) as unknown as AIImageModelCard;
68
+ if (!activeModel) {
69
+ throw new Error(
70
+ `Model "${model}" not found in provider "${provider}". Available models: ${providerItem.children.map((m) => m.id).join(', ')}`,
71
+ );
72
+ }
49
73
 
50
74
  const parametersSchema = activeModel.parameters as ModelParamsSchema;
51
75
  const defaultValues = extractDefaultValues(parametersSchema);
@@ -53,6 +77,22 @@ export function getModelAndDefaults(model: string, provider: string) {
53
77
  return { defaultValues, activeModel, parametersSchema };
54
78
  }
55
79
 
80
+ /**
81
+ * @internal Helper
82
+ * Internal utility to derive initial config for a given provider/model.
83
+ * Not exported; tests should cover through public actions.
84
+ */
85
+ function prepareModelConfigState(model: string, provider: string) {
86
+ const { defaultValues, parametersSchema } = getModelAndDefaults(model, provider);
87
+ const initialActiveRatio = calculateInitialAspectRatio(parametersSchema, defaultValues);
88
+
89
+ return {
90
+ defaultValues,
91
+ parametersSchema,
92
+ initialActiveRatio,
93
+ };
94
+ }
95
+
56
96
  export const createGenerationConfigSlice: StateCreator<
57
97
  ImageStore,
58
98
  [['zustand/devtools', never]],
@@ -237,38 +277,32 @@ export const createGenerationConfigSlice: StateCreator<
237
277
  },
238
278
 
239
279
  setModelAndProviderOnSelect: (model, provider) => {
240
- const { defaultValues, activeModel } = getModelAndDefaults(model, provider);
241
- const parametersSchema = activeModel.parameters;
242
-
243
- let initialActiveRatio: string | null = null;
244
-
245
- // 如果模型没有原生比例或尺寸参数,但有宽高,则启用虚拟比例控制
246
- if (
247
- !parametersSchema?.aspectRatio &&
248
- !parametersSchema?.size &&
249
- parametersSchema?.width &&
250
- parametersSchema?.height
251
- ) {
252
- const { width, height } = defaultValues;
253
- if (typeof width === 'number' && typeof height === 'number' && width > 0 && height > 0) {
254
- initialActiveRatio = `${width}:${height}`;
255
- } else {
256
- initialActiveRatio = '1:1';
257
- }
258
- }
280
+ const { defaultValues, parametersSchema, initialActiveRatio } = prepareModelConfigState(
281
+ model,
282
+ provider,
283
+ );
259
284
 
260
285
  set(
261
286
  {
262
287
  model,
263
288
  provider,
264
289
  parameters: defaultValues,
265
- parametersSchema: parametersSchema,
290
+ parametersSchema,
266
291
  isAspectRatioLocked: false,
267
292
  activeAspectRatio: initialActiveRatio,
268
293
  },
269
294
  false,
270
295
  `setModelAndProviderOnSelect/${model}/${provider}`,
271
296
  );
297
+
298
+ // 仅在登录用户下记忆上次选择,保持与恢复策略一致
299
+ const isLogin = authSelectors.isLogin(useUserStore.getState());
300
+ if (isLogin) {
301
+ useGlobalStore.getState().updateSystemStatus({
302
+ lastSelectedImageModel: model,
303
+ lastSelectedImageProvider: provider,
304
+ });
305
+ }
272
306
  },
273
307
 
274
308
  setImageNum: (imageNum) => {
@@ -292,4 +326,47 @@ export const createGenerationConfigSlice: StateCreator<
292
326
  reuseSeed: (seed: number) => {
293
327
  set((state) => ({ parameters: { ...state.parameters, seed } }), false, `reuseSeed/${seed}`);
294
328
  },
329
+
330
+ initializeImageConfig: (isLogin, lastSelectedImageModel, lastSelectedImageProvider) => {
331
+ // If no parameters are passed, get from store (backward compatibility)
332
+ let actualIsLogin = isLogin;
333
+ let actualLastSelectedImageModel = lastSelectedImageModel;
334
+ let actualLastSelectedImageProvider = lastSelectedImageProvider;
335
+
336
+ if (typeof isLogin === 'undefined') {
337
+ const globalStatus = useGlobalStore.getState().status;
338
+ actualIsLogin = authSelectors.isLogin(useUserStore.getState());
339
+ actualLastSelectedImageModel = globalStatus.lastSelectedImageModel;
340
+ actualLastSelectedImageProvider = globalStatus.lastSelectedImageProvider;
341
+ }
342
+
343
+ if (actualIsLogin && actualLastSelectedImageModel && actualLastSelectedImageProvider) {
344
+ try {
345
+ const { defaultValues, parametersSchema, initialActiveRatio } = prepareModelConfigState(
346
+ actualLastSelectedImageModel,
347
+ actualLastSelectedImageProvider,
348
+ );
349
+
350
+ set(
351
+ {
352
+ model: actualLastSelectedImageModel,
353
+ provider: actualLastSelectedImageProvider,
354
+ parameters: defaultValues,
355
+ parametersSchema,
356
+ isAspectRatioLocked: false,
357
+ activeAspectRatio: initialActiveRatio,
358
+ isInit: true,
359
+ },
360
+ false,
361
+ `initializeImageConfig/${actualLastSelectedImageModel}/${actualLastSelectedImageProvider}`,
362
+ );
363
+ } catch {
364
+ // If restoration fails, simply mark as initialized to use default configuration
365
+ set({ isInit: true }, false, 'initializeImageConfig/fallback');
366
+ }
367
+ } else {
368
+ // No remembered model, directly mark as initialized (use default values)
369
+ set({ isInit: true }, false, 'initializeImageConfig/default');
370
+ }
371
+ },
295
372
  });
@@ -21,6 +21,11 @@ export interface GenerationConfigState {
21
21
 
22
22
  isAspectRatioLocked: boolean;
23
23
  activeAspectRatio: string | null; // string - 虚拟比例; null - 原生比例
24
+
25
+ /**
26
+ * 标记配置是否已初始化(包括从记忆中恢复)
27
+ */
28
+ isInit: boolean;
24
29
  }
25
30
 
26
31
  export const DEFAULT_IMAGE_GENERATION_PARAMETERS: RuntimeImageGenParams =
@@ -34,4 +39,5 @@ export const initialGenerationConfigState: GenerationConfigState = {
34
39
  parametersSchema: gptImage1ParamsSchema,
35
40
  isAspectRatioLocked: false,
36
41
  activeAspectRatio: null,
42
+ isInit: false,
37
43
  };
@@ -0,0 +1,148 @@
1
+ import { ModelParamsSchema } from 'model-bank';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { calculateInitialAspectRatio, supportsVirtualAspectRatio } from './aspectRatio';
5
+
6
+ // Test data fixtures
7
+ const createBaseSchema = (overrides: Partial<ModelParamsSchema> = {}): ModelParamsSchema => ({
8
+ prompt: { default: '' },
9
+ ...overrides,
10
+ });
11
+
12
+ const createDimensionSchema = (overrides: Partial<ModelParamsSchema> = {}): ModelParamsSchema =>
13
+ createBaseSchema({
14
+ width: { default: 512, min: 256, max: 2048 },
15
+ height: { default: 512, min: 256, max: 2048 },
16
+ ...overrides,
17
+ });
18
+
19
+ const createDefaultValues = (values: Record<string, any> = {}) => ({
20
+ prompt: '',
21
+ ...values,
22
+ });
23
+
24
+ describe('aspectRatio utils', () => {
25
+ describe('calculateInitialAspectRatio', () => {
26
+ it('should return null when native aspect controls are present', () => {
27
+ // Models with native aspectRatio parameter
28
+ const aspectRatioSchema = createBaseSchema({
29
+ aspectRatio: { default: '1:1', enum: ['1:1', '16:9', '4:3'] },
30
+ });
31
+ const aspectRatioValues = createDefaultValues({ aspectRatio: '1:1' });
32
+
33
+ expect(calculateInitialAspectRatio(aspectRatioSchema, aspectRatioValues)).toBeNull();
34
+
35
+ // Models with native size parameter
36
+ const sizeSchema = createBaseSchema({
37
+ size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
38
+ });
39
+ const sizeValues = createDefaultValues({ size: '1024x1024' });
40
+
41
+ expect(calculateInitialAspectRatio(sizeSchema, sizeValues)).toBeNull();
42
+ });
43
+
44
+ it('should return null when width or height parameters are missing', () => {
45
+ const schemaWithoutWidth = createBaseSchema({
46
+ height: { default: 512, min: 256, max: 2048 },
47
+ });
48
+ const valuesWithoutWidth = createDefaultValues({ height: 512 });
49
+
50
+ expect(calculateInitialAspectRatio(schemaWithoutWidth, valuesWithoutWidth)).toBeNull();
51
+
52
+ const schemaWithoutHeight = createBaseSchema({
53
+ width: { default: 512, min: 256, max: 2048 },
54
+ });
55
+ const valuesWithoutHeight = createDefaultValues({ width: 512 });
56
+
57
+ expect(calculateInitialAspectRatio(schemaWithoutHeight, valuesWithoutHeight)).toBeNull();
58
+ });
59
+
60
+ it('should calculate aspect ratio from width and height values', () => {
61
+ const schema = createDimensionSchema({
62
+ width: { default: 1024, min: 256, max: 2048 },
63
+ height: { default: 768, min: 256, max: 2048 },
64
+ });
65
+ const values = createDefaultValues({ width: 1024, height: 768 });
66
+
67
+ expect(calculateInitialAspectRatio(schema, values)).toBe('1024:768');
68
+ });
69
+
70
+ it('should handle square dimensions correctly', () => {
71
+ const schema = createDimensionSchema();
72
+ const values = createDefaultValues({ width: 512, height: 512 });
73
+
74
+ expect(calculateInitialAspectRatio(schema, values)).toBe('512:512');
75
+ });
76
+
77
+ it('should return fallback ratio for invalid dimension values', () => {
78
+ const schema = createDimensionSchema();
79
+
80
+ // Invalid values should fallback to 1:1
81
+ const testCases = [
82
+ { width: NaN, height: NaN },
83
+ { width: 0, height: 512 },
84
+ { width: -512, height: 512 },
85
+ { height: 512 }, // missing width
86
+ { width: 512 }, // missing height
87
+ ];
88
+
89
+ testCases.forEach((testCase) => {
90
+ const values = createDefaultValues(testCase);
91
+ expect(calculateInitialAspectRatio(schema, values)).toBe('1:1');
92
+ });
93
+ });
94
+ });
95
+
96
+ describe('supportsVirtualAspectRatio', () => {
97
+ it('should return true for models with width/height but no native aspect controls', () => {
98
+ const schema = createDimensionSchema();
99
+
100
+ expect(supportsVirtualAspectRatio(schema)).toBe(true);
101
+ });
102
+
103
+ it('should return false when native aspect controls are present', () => {
104
+ // Schema with native aspectRatio parameter
105
+ const aspectRatioSchema = createDimensionSchema({
106
+ aspectRatio: { default: '1:1', enum: ['1:1', '16:9', '4:3'] },
107
+ });
108
+
109
+ expect(supportsVirtualAspectRatio(aspectRatioSchema)).toBe(false);
110
+
111
+ // Schema with native size parameter
112
+ const sizeSchema = createDimensionSchema({
113
+ size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
114
+ });
115
+
116
+ expect(supportsVirtualAspectRatio(sizeSchema)).toBe(false);
117
+
118
+ // Schema with both aspectRatio and size parameters
119
+ const bothSchema = createDimensionSchema({
120
+ aspectRatio: { default: '1:1', enum: ['1:1', '16:9', '4:3'] },
121
+ size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
122
+ });
123
+
124
+ expect(supportsVirtualAspectRatio(bothSchema)).toBe(false);
125
+ });
126
+
127
+ it('should return false when required dimension parameters are missing', () => {
128
+ // Missing width parameter
129
+ const schemaWithoutWidth = createBaseSchema({
130
+ height: { default: 512, min: 256, max: 2048 },
131
+ });
132
+
133
+ expect(supportsVirtualAspectRatio(schemaWithoutWidth)).toBe(false);
134
+
135
+ // Missing height parameter
136
+ const schemaWithoutHeight = createBaseSchema({
137
+ width: { default: 512, min: 256, max: 2048 },
138
+ });
139
+
140
+ expect(supportsVirtualAspectRatio(schemaWithoutHeight)).toBe(false);
141
+
142
+ // Missing both width and height parameters
143
+ const emptySchema = createBaseSchema();
144
+
145
+ expect(supportsVirtualAspectRatio(emptySchema)).toBe(false);
146
+ });
147
+ });
148
+ });
@@ -0,0 +1,45 @@
1
+ import { ModelParamsSchema } from 'model-bank';
2
+
3
+ /**
4
+ * Calculate initial aspect ratio for image generation models
5
+ * @param parametersSchema - The model's parameter schema
6
+ * @param defaultValues - Default parameter values from the model
7
+ * @returns Initial aspect ratio string or null if not applicable
8
+ */
9
+ export const calculateInitialAspectRatio = (
10
+ parametersSchema: ModelParamsSchema,
11
+ defaultValues: Record<string, any>,
12
+ ): string | null => {
13
+ // If model has native aspect ratio or size parameters, don't use virtual ratio control
14
+ if (parametersSchema?.aspectRatio || parametersSchema?.size) {
15
+ return null;
16
+ }
17
+
18
+ // If model doesn't have width/height parameters, no virtual ratio needed
19
+ if (!parametersSchema?.width || !parametersSchema?.height) {
20
+ return null;
21
+ }
22
+
23
+ const { width, height } = defaultValues;
24
+
25
+ // Ensure we have valid numeric width and height values
26
+ if (typeof width === 'number' && typeof height === 'number' && width > 0 && height > 0) {
27
+ return `${width}:${height}`;
28
+ }
29
+
30
+ // Default fallback ratio
31
+ return '1:1';
32
+ };
33
+
34
+ /**
35
+ * Check if a model supports virtual aspect ratio control
36
+ * Virtual aspect ratio is enabled when model has width/height but no native aspect ratio/size controls
37
+ */
38
+ export const supportsVirtualAspectRatio = (parametersSchema: ModelParamsSchema): boolean => {
39
+ return (
40
+ !parametersSchema?.aspectRatio &&
41
+ !parametersSchema?.size &&
42
+ !!parametersSchema?.width &&
43
+ !!parametersSchema?.height
44
+ );
45
+ };