@lobehub/chat 1.114.6 → 1.115.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 (59) hide show
  1. package/.cursor/rules/project-introduce.mdc +1 -15
  2. package/.cursor/rules/project-structure.mdc +227 -0
  3. package/.cursor/rules/testing-guide/db-model-test.mdc +5 -3
  4. package/.cursor/rules/testing-guide/testing-guide.mdc +153 -168
  5. package/.github/workflows/claude.yml +1 -1
  6. package/.github/workflows/test.yml +9 -0
  7. package/CHANGELOG.md +25 -0
  8. package/CLAUDE.md +11 -27
  9. package/changelog/v1.json +5 -0
  10. package/docs/development/basic/feature-development.mdx +1 -1
  11. package/docs/development/basic/feature-development.zh-CN.mdx +1 -1
  12. package/package.json +4 -4
  13. package/packages/const/src/image.ts +28 -0
  14. package/packages/const/src/index.ts +1 -0
  15. package/packages/database/package.json +4 -2
  16. package/packages/database/src/repositories/aiInfra/index.ts +1 -1
  17. package/packages/database/tests/setup-db.ts +3 -0
  18. package/packages/database/vitest.config.mts +33 -0
  19. package/packages/model-runtime/src/utils/modelParse.ts +1 -1
  20. package/packages/utils/src/client/imageDimensions.test.ts +95 -0
  21. package/packages/utils/src/client/imageDimensions.ts +54 -0
  22. package/packages/utils/src/number.test.ts +3 -1
  23. package/packages/utils/src/number.ts +1 -2
  24. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +1 -1
  25. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +0 -1
  26. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +16 -6
  27. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +14 -2
  28. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +27 -2
  29. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +23 -5
  30. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useAutoDimensions.ts +56 -0
  31. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +82 -5
  32. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/dimensionConstraints.test.ts +235 -0
  33. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/imageValidation.test.ts +401 -0
  34. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/dimensionConstraints.ts +54 -0
  35. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +3 -1
  36. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +15 -2
  37. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +5 -4
  38. package/src/libs/standard-parameters/index.ts +1 -1
  39. package/src/server/services/generation/index.ts +1 -1
  40. package/src/store/chat/slices/builtinTool/actions/dalle.test.ts +20 -13
  41. package/src/store/file/slices/upload/action.ts +18 -7
  42. package/src/store/image/slices/generationConfig/hooks.ts +1 -1
  43. package/tsconfig.json +1 -10
  44. package/packages/const/src/imageGeneration.ts +0 -16
  45. package/src/app/(backend)/trpc/desktop/[trpc]/route.ts +0 -26
  46. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +0 -24
  47. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +0 -15
  48. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +0 -91
  49. package/src/app/desktop/devtools/page.tsx +0 -89
  50. package/src/app/desktop/layout.tsx +0 -31
  51. /package/apps/desktop/{vitest.config.ts → vitest.config.mts} +0 -0
  52. /package/packages/database/{vitest.config.ts → vitest.config.server.mts} +0 -0
  53. /package/packages/electron-server-ipc/{vitest.config.ts → vitest.config.mts} +0 -0
  54. /package/packages/file-loaders/{vitest.config.ts → vitest.config.mts} +0 -0
  55. /package/packages/model-runtime/{vitest.config.ts → vitest.config.mts} +0 -0
  56. /package/packages/prompts/{vitest.config.ts → vitest.config.mts} +0 -0
  57. /package/packages/utils/{vitest.config.ts → vitest.config.mts} +0 -0
  58. /package/packages/web-crawler/{vitest.config.ts → vitest.config.mts} +0 -0
  59. /package/{vitest.config.ts → vitest.config.mts} +0 -0
@@ -0,0 +1,401 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ formatFileSize,
5
+ validateImageCount,
6
+ validateImageFileSize,
7
+ validateImageFiles,
8
+ } from '../imageValidation';
9
+
10
+ describe('imageValidation', () => {
11
+ describe('formatFileSize', () => {
12
+ it('should format 0 bytes correctly', () => {
13
+ const result = formatFileSize(0);
14
+ expect(result).toBe('0 B');
15
+ });
16
+
17
+ it('should format bytes correctly', () => {
18
+ const result = formatFileSize(512);
19
+ expect(result).toBe('512 B');
20
+ });
21
+
22
+ it('should format kilobytes correctly', () => {
23
+ const result = formatFileSize(1024);
24
+ expect(result).toBe('1 KB'); // parseFloat removes trailing .0
25
+
26
+ const result2 = formatFileSize(1536);
27
+ expect(result2).toBe('1.5 KB');
28
+ });
29
+
30
+ it('should format megabytes correctly', () => {
31
+ const result = formatFileSize(1024 * 1024);
32
+ expect(result).toBe('1 MB'); // parseFloat removes trailing .0
33
+
34
+ const result2 = formatFileSize(2.5 * 1024 * 1024);
35
+ expect(result2).toBe('2.5 MB');
36
+ });
37
+
38
+ it('should format gigabytes correctly', () => {
39
+ const result = formatFileSize(1024 * 1024 * 1024);
40
+ expect(result).toBe('1 GB'); // parseFloat removes trailing .0
41
+
42
+ const result2 = formatFileSize(1.25 * 1024 * 1024 * 1024);
43
+ expect(result2).toBe('1.3 GB');
44
+ });
45
+
46
+ it('should handle decimal precision correctly', () => {
47
+ const result = formatFileSize(1536.7);
48
+ expect(result).toBe('1.5 KB');
49
+
50
+ const result2 = formatFileSize(1048576.123);
51
+ expect(result2).toBe('1 MB'); // parseFloat removes trailing .0
52
+ });
53
+
54
+ it('should handle edge cases with very small decimal values', () => {
55
+ // Note: Current implementation has a bug with values < 1 byte due to negative log
56
+ const result = formatFileSize(0.5);
57
+ expect(result).toContain('undefined'); // Known bug in implementation
58
+ });
59
+
60
+ it('should handle very large numbers', () => {
61
+ const result = formatFileSize(5 * 1024 * 1024 * 1024);
62
+ expect(result).toBe('5 GB'); // parseFloat removes trailing .0
63
+ });
64
+ });
65
+
66
+ describe('validateImageFileSize', () => {
67
+ const createMockFile = (name: string = 'test.jpg'): File => {
68
+ return new File([''], name, { type: 'image/jpeg' });
69
+ };
70
+
71
+ beforeEach(() => {
72
+ // Mock File.size property
73
+ Object.defineProperty(File.prototype, 'size', {
74
+ get() {
75
+ return this._size || 0;
76
+ },
77
+ configurable: true,
78
+ });
79
+ });
80
+
81
+ it('should pass validation for file within default size limit', () => {
82
+ const file = createMockFile(); // 5MB
83
+ Object.defineProperty(file, 'size', { value: 5 * 1024 * 1024, writable: false });
84
+
85
+ const result = validateImageFileSize(file);
86
+
87
+ expect(result).toEqual({
88
+ valid: true,
89
+ });
90
+ });
91
+
92
+ it('should fail validation for file exceeding default size limit', () => {
93
+ const file = createMockFile('large-image.png'); // 15MB
94
+ Object.defineProperty(file, 'size', { value: 15 * 1024 * 1024, writable: false });
95
+
96
+ const result = validateImageFileSize(file);
97
+
98
+ expect(result).toEqual({
99
+ actualSize: 15 * 1024 * 1024,
100
+ error: 'fileSizeExceeded',
101
+ fileName: 'large-image.png',
102
+ maxSize: 10 * 1024 * 1024, // 10MB default
103
+ valid: false,
104
+ });
105
+ });
106
+
107
+ it('should pass validation for file within custom size limit', () => {
108
+ const file = createMockFile(); // 3MB
109
+ Object.defineProperty(file, 'size', { value: 3 * 1024 * 1024, writable: false });
110
+ const customMaxSize = 5 * 1024 * 1024; // 5MB
111
+
112
+ const result = validateImageFileSize(file, customMaxSize);
113
+
114
+ expect(result).toEqual({
115
+ valid: true,
116
+ });
117
+ });
118
+
119
+ it('should fail validation for file exceeding custom size limit', () => {
120
+ const file = createMockFile('custom-large.gif'); // 8MB
121
+ Object.defineProperty(file, 'size', { value: 8 * 1024 * 1024, writable: false });
122
+ const customMaxSize = 5 * 1024 * 1024; // 5MB
123
+
124
+ const result = validateImageFileSize(file, customMaxSize);
125
+
126
+ expect(result).toEqual({
127
+ actualSize: 8 * 1024 * 1024,
128
+ error: 'fileSizeExceeded',
129
+ fileName: 'custom-large.gif',
130
+ maxSize: 5 * 1024 * 1024,
131
+ valid: false,
132
+ });
133
+ });
134
+
135
+ it('should handle zero size files', () => {
136
+ const file = createMockFile('empty.jpg');
137
+ Object.defineProperty(file, 'size', { value: 0, writable: false });
138
+
139
+ const result = validateImageFileSize(file);
140
+
141
+ expect(result).toEqual({
142
+ valid: true,
143
+ });
144
+ });
145
+
146
+ it('should handle files exactly at size limit', () => {
147
+ const maxSize = 10 * 1024 * 1024; // 10MB
148
+ const file = createMockFile('exact-limit.jpg');
149
+ Object.defineProperty(file, 'size', { value: maxSize, writable: false });
150
+
151
+ const result = validateImageFileSize(file, maxSize);
152
+
153
+ expect(result).toEqual({
154
+ valid: true,
155
+ });
156
+ });
157
+
158
+ it('should handle files just over size limit', () => {
159
+ const maxSize = 10 * 1024 * 1024; // 10MB
160
+ const file = createMockFile('over-limit.jpg');
161
+ Object.defineProperty(file, 'size', { value: maxSize + 1, writable: false });
162
+
163
+ const result = validateImageFileSize(file, maxSize);
164
+
165
+ expect(result).toEqual({
166
+ actualSize: maxSize + 1,
167
+ error: 'fileSizeExceeded',
168
+ fileName: 'over-limit.jpg',
169
+ maxSize: maxSize,
170
+ valid: false,
171
+ });
172
+ });
173
+ });
174
+
175
+ describe('validateImageCount', () => {
176
+ it('should pass validation when maxCount is not provided', () => {
177
+ const result = validateImageCount(5);
178
+
179
+ expect(result).toEqual({
180
+ valid: true,
181
+ });
182
+ });
183
+
184
+ it('should pass validation when maxCount is undefined', () => {
185
+ const result = validateImageCount(10, undefined);
186
+
187
+ expect(result).toEqual({
188
+ valid: true,
189
+ });
190
+ });
191
+
192
+ it('should pass validation for count within limit', () => {
193
+ const result = validateImageCount(3, 5);
194
+
195
+ expect(result).toEqual({
196
+ valid: true,
197
+ });
198
+ });
199
+
200
+ it('should pass validation for count exactly at limit', () => {
201
+ const result = validateImageCount(5, 5);
202
+
203
+ expect(result).toEqual({
204
+ valid: true,
205
+ });
206
+ });
207
+
208
+ it('should fail validation for count exceeding limit', () => {
209
+ const result = validateImageCount(6, 5);
210
+
211
+ expect(result).toEqual({
212
+ error: 'imageCountExceeded',
213
+ valid: false,
214
+ });
215
+ });
216
+
217
+ it('should handle zero count', () => {
218
+ const result = validateImageCount(0, 5);
219
+
220
+ expect(result).toEqual({
221
+ valid: true,
222
+ });
223
+ });
224
+
225
+ it('should handle zero max count', () => {
226
+ // Note: Current implementation has a bug - treats 0 as falsy
227
+ const result = validateImageCount(1, 0);
228
+
229
+ expect(result).toEqual({
230
+ valid: true, // Bug: should be false, but 0 is treated as falsy
231
+ });
232
+ });
233
+ });
234
+
235
+ describe('validateImageFiles', () => {
236
+ const createMockFiles = (sizes: number[], names?: string[]): File[] => {
237
+ return sizes.map((size, index) => {
238
+ const name = names?.[index] || `file${index}.jpg`;
239
+ const file = new File([''], name, { type: 'image/jpeg' });
240
+ Object.defineProperty(file, 'size', { value: size, writable: false });
241
+ return file;
242
+ });
243
+ };
244
+
245
+ it('should pass validation for valid files within all limits', () => {
246
+ const files = createMockFiles([1024 * 1024, 2 * 1024 * 1024]); // 1MB, 2MB
247
+ const constraints = {
248
+ maxAddedFiles: 3,
249
+ maxFileSize: 5 * 1024 * 1024, // 5MB
250
+ };
251
+
252
+ const result = validateImageFiles(files, constraints);
253
+
254
+ expect(result.valid).toBe(true);
255
+ expect(result.errors).toEqual([]);
256
+ expect(result.fileResults).toHaveLength(2);
257
+ expect(result.fileResults[0]).toEqual({ valid: true });
258
+ expect(result.fileResults[1]).toEqual({ valid: true });
259
+ expect(result.failedFiles).toEqual([]);
260
+ });
261
+
262
+ it('should fail validation when file count exceeds limit', () => {
263
+ const files = createMockFiles([1024, 1024, 1024]); // 3 small files
264
+ const constraints = {
265
+ maxAddedFiles: 2,
266
+ maxFileSize: 5 * 1024 * 1024,
267
+ };
268
+
269
+ const result = validateImageFiles(files, constraints);
270
+
271
+ expect(result.valid).toBe(false);
272
+ expect(result.errors).toContain('imageCountExceeded');
273
+ });
274
+
275
+ it('should fail validation when files exceed size limit', () => {
276
+ const files = createMockFiles([15 * 1024 * 1024], ['large.jpg']); // 15MB
277
+ const constraints = {
278
+ maxAddedFiles: 5,
279
+ maxFileSize: 10 * 1024 * 1024, // 10MB
280
+ };
281
+
282
+ const result = validateImageFiles(files, constraints);
283
+
284
+ expect(result.valid).toBe(false);
285
+ expect(result.errors).toContain('fileSizeExceeded');
286
+ expect(result.failedFiles).toHaveLength(1);
287
+ expect(result.failedFiles![0]).toEqual({
288
+ actualSize: 15 * 1024 * 1024,
289
+ error: 'fileSizeExceeded',
290
+ fileName: 'large.jpg',
291
+ maxSize: 10 * 1024 * 1024,
292
+ valid: false,
293
+ });
294
+ });
295
+
296
+ it('should handle multiple validation failures', () => {
297
+ const files = createMockFiles(
298
+ [15 * 1024 * 1024, 20 * 1024 * 1024, 8 * 1024 * 1024], // 15MB, 20MB, 8MB
299
+ ['large1.jpg', 'large2.jpg', 'ok.jpg'],
300
+ );
301
+ const constraints = {
302
+ maxAddedFiles: 2, // Exceeds count
303
+ maxFileSize: 10 * 1024 * 1024, // 10MB - first two exceed size
304
+ };
305
+
306
+ const result = validateImageFiles(files, constraints);
307
+
308
+ expect(result.valid).toBe(false);
309
+ expect(result.errors).toContain('imageCountExceeded');
310
+ expect(result.errors).toContain('fileSizeExceeded');
311
+ expect(result.failedFiles).toHaveLength(2); // Only files that failed size check
312
+ expect(result.fileResults).toHaveLength(3);
313
+ });
314
+
315
+ it('should remove duplicate error messages', () => {
316
+ const files = createMockFiles(
317
+ [15 * 1024 * 1024, 20 * 1024 * 1024], // Both exceed 10MB
318
+ ['large1.jpg', 'large2.jpg'],
319
+ );
320
+ const constraints = {
321
+ maxAddedFiles: 5,
322
+ maxFileSize: 10 * 1024 * 1024,
323
+ };
324
+
325
+ const result = validateImageFiles(files, constraints);
326
+
327
+ expect(result.valid).toBe(false);
328
+ expect(result.errors).toEqual(['fileSizeExceeded']); // No duplicates
329
+ expect(result.failedFiles).toHaveLength(2);
330
+ });
331
+
332
+ it('should handle empty file list', () => {
333
+ const files: File[] = [];
334
+ const constraints = {
335
+ maxAddedFiles: 5,
336
+ maxFileSize: 10 * 1024 * 1024,
337
+ };
338
+
339
+ const result = validateImageFiles(files, constraints);
340
+
341
+ expect(result.valid).toBe(true);
342
+ expect(result.errors).toEqual([]);
343
+ expect(result.fileResults).toEqual([]);
344
+ expect(result.failedFiles).toEqual([]);
345
+ });
346
+
347
+ it('should handle constraints with only maxAddedFiles', () => {
348
+ const files = createMockFiles([1024, 1024, 1024]);
349
+ const constraints = {
350
+ maxAddedFiles: 2,
351
+ };
352
+
353
+ const result = validateImageFiles(files, constraints);
354
+
355
+ expect(result.valid).toBe(false);
356
+ expect(result.errors).toContain('imageCountExceeded');
357
+ });
358
+
359
+ it('should handle constraints with only maxFileSize', () => {
360
+ const files = createMockFiles([15 * 1024 * 1024], ['large.jpg']);
361
+ const constraints = {
362
+ maxFileSize: 10 * 1024 * 1024,
363
+ };
364
+
365
+ const result = validateImageFiles(files, constraints);
366
+
367
+ expect(result.valid).toBe(false);
368
+ expect(result.errors).toContain('fileSizeExceeded');
369
+ });
370
+
371
+ it('should handle empty constraints object', () => {
372
+ const files = createMockFiles([1024 * 1024]);
373
+ const constraints = {};
374
+
375
+ const result = validateImageFiles(files, constraints);
376
+
377
+ expect(result.valid).toBe(true);
378
+ expect(result.errors).toEqual([]);
379
+ });
380
+
381
+ it('should validate each file independently', () => {
382
+ const files = createMockFiles(
383
+ [1 * 1024 * 1024, 15 * 1024 * 1024, 3 * 1024 * 1024], // 1MB, 15MB, 3MB
384
+ ['small.jpg', 'large.jpg', 'medium.jpg'],
385
+ );
386
+ const constraints = {
387
+ maxAddedFiles: 5,
388
+ maxFileSize: 10 * 1024 * 1024,
389
+ };
390
+
391
+ const result = validateImageFiles(files, constraints);
392
+
393
+ expect(result.valid).toBe(false);
394
+ expect(result.fileResults).toHaveLength(3);
395
+ expect(result.fileResults[0]).toEqual({ valid: true }); // 1MB file
396
+ expect(result.fileResults[1].valid).toBe(false); // 15MB file
397
+ expect(result.fileResults[2]).toEqual({ valid: true }); // 3MB file
398
+ expect(result.failedFiles).toHaveLength(1);
399
+ });
400
+ });
401
+ });
@@ -0,0 +1,54 @@
1
+ interface DimensionConstraints {
2
+ height: { max: number; min: number };
3
+ width: { max: number; min: number };
4
+ }
5
+
6
+ /**
7
+ * Adjust image dimensions to fit within model constraints while maintaining aspect ratio
8
+ * @param originalWidth Original image width
9
+ * @param originalHeight Original image height
10
+ * @param constraints Width and height constraints from model schema
11
+ * @returns Adjusted dimensions within constraints
12
+ */
13
+ export const constrainDimensions = (
14
+ originalWidth: number,
15
+ originalHeight: number,
16
+ constraints: DimensionConstraints,
17
+ ): { height: number; width: number } => {
18
+ let width = originalWidth;
19
+ let height = originalHeight;
20
+
21
+ // First, scale down if exceeding maximum values
22
+ if (width > constraints.width.max || height > constraints.height.max) {
23
+ const scaleX = constraints.width.max / width;
24
+ const scaleY = constraints.height.max / height;
25
+ const scale = Math.min(scaleX, scaleY);
26
+
27
+ width = Math.round(width * scale);
28
+ height = Math.round(height * scale);
29
+ }
30
+
31
+ // Then, scale up if below minimum values
32
+ if (width < constraints.width.min || height < constraints.height.min) {
33
+ const scaleX = constraints.width.min / width;
34
+ const scaleY = constraints.height.min / height;
35
+ const scale = Math.max(scaleX, scaleY);
36
+
37
+ width = Math.round(width * scale);
38
+ height = Math.round(height * scale);
39
+ }
40
+
41
+ // Ensure final values are within bounds (may need adjustment due to rounding)
42
+ width = Math.max(constraints.width.min, Math.min(constraints.width.max, width));
43
+ height = Math.max(constraints.height.min, Math.min(constraints.height.max, height));
44
+
45
+ // Round to nearest multiple of 8 (common model requirement)
46
+ width = Math.round(width / 8) * 8;
47
+ height = Math.round(height / 8) * 8;
48
+
49
+ // Final bounds check after rounding
50
+ width = Math.max(constraints.width.min, Math.min(constraints.width.max, width));
51
+ height = Math.max(constraints.height.min, Math.min(constraints.height.max, height));
52
+
53
+ return { height, width };
54
+ };
@@ -23,10 +23,11 @@ const formatTime = (date: Date, locale: string) => {
23
23
 
24
24
  interface TopicItemProps {
25
25
  showMoreInfo?: boolean;
26
+ style?: React.CSSProperties;
26
27
  topic: ImageGenerationTopic;
27
28
  }
28
29
 
29
- const TopicItem = memo<TopicItemProps>(({ topic, showMoreInfo }) => {
30
+ const TopicItem = memo<TopicItemProps>(({ topic, showMoreInfo, style }) => {
30
31
  const theme = useTheme();
31
32
  const { t } = useTranslation('image');
32
33
  const { modal } = App.useApp();
@@ -111,6 +112,7 @@ const TopicItem = memo<TopicItemProps>(({ topic, showMoreInfo }) => {
111
112
  onClick={handleClick}
112
113
  style={{
113
114
  cursor: 'pointer',
115
+ ...style,
114
116
  }}
115
117
  width={'100%'}
116
118
  >
@@ -47,8 +47,21 @@ const TopicsList = memo(() => {
47
47
  showMoreInfo={showMoreInfo}
48
48
  />
49
49
  <Flexbox align="center" gap={12} ref={parent} width={'100%'}>
50
- {generationTopics.map((topic) => (
51
- <TopicItem key={topic.id} showMoreInfo={showMoreInfo} topic={topic} />
50
+ {generationTopics.map((topic, index) => (
51
+ <TopicItem
52
+ key={topic.id}
53
+ showMoreInfo={showMoreInfo}
54
+ style={{
55
+ padding:
56
+ // fix the avatar border is clipped by overflow hidden
57
+ generationTopics.length === 1
58
+ ? '4px 0'
59
+ : index === generationTopics.length - 1
60
+ ? '0 0 4px'
61
+ : '0',
62
+ }}
63
+ topic={topic}
64
+ />
52
65
  ))}
53
66
  </Flexbox>
54
67
  </Flexbox>
@@ -111,13 +111,14 @@ export const getThumbnailMaxWidth = (
111
111
  ): number => {
112
112
  const dimensions = getImageDimensions(generation, generationBatch);
113
113
 
114
- // Return default width if dimensions are not available
115
- if (!dimensions.width || !dimensions.height) {
114
+ // Return default width if no dimension information is available
115
+ if (!dimensions.aspectRatio) {
116
116
  return DEFAULT_MAX_ITEM_WIDTH;
117
117
  }
118
118
 
119
- const { width: originalWidth, height: originalHeight } = dimensions;
120
- const aspectRatio = originalWidth / originalHeight;
119
+ // Parse aspect ratio string (format: "16 / 9")
120
+ const [widthStr, heightStr] = dimensions.aspectRatio.split(' / ');
121
+ const aspectRatio = Number(widthStr) / Number(heightStr);
121
122
 
122
123
  // Apply screen height constraint (half of screen height)
123
124
  // Note: window.innerHeight is safe to use here as this function is client-side only
@@ -1,7 +1,7 @@
1
1
  import type { Simplify } from 'type-fest';
2
2
  import { z } from 'zod';
3
3
 
4
- export const MAX_SEED = 2 ** 31 - 1;
4
+ import { MAX_SEED } from '@/const/image';
5
5
 
6
6
  // 定义顶层的元规范 - 平铺结构
7
7
  export const ModelParamsMetaSchema = z.object({
@@ -4,7 +4,7 @@ import mime from 'mime';
4
4
  import { nanoid } from 'nanoid';
5
5
  import sharp from 'sharp';
6
6
 
7
- import { IMAGE_GENERATION_CONFIG } from '@/const/imageGeneration';
7
+ import { IMAGE_GENERATION_CONFIG } from '@/const/image';
8
8
  import { LobeChatDatabase } from '@/database/type';
9
9
  import { parseDataUri } from '@/libs/model-runtime/utils/uriParser';
10
10
  import { FileService } from '@/server/services/file';
@@ -7,6 +7,7 @@ import { messageService } from '@/services/message';
7
7
  import { imageGenerationService } from '@/services/textToImage';
8
8
  import { uploadService } from '@/services/upload';
9
9
  import { chatSelectors } from '@/store/chat/selectors';
10
+ import { useFileStore } from '@/store/file';
10
11
  import { ChatMessage } from '@/types/message';
11
12
  import { DallEImageItem } from '@/types/tool/dalle';
12
13
 
@@ -41,24 +42,28 @@ describe('chatToolSlice - dalle', () => {
41
42
  vi.spyOn(uploadService, 'getImageFileByUrlWithCORS').mockResolvedValue(
42
43
  new File(['1'], 'file.png', { type: 'image/png' }),
43
44
  );
44
- // @ts-ignore
45
- vi.spyOn(uploadService, 'uploadToClientS3').mockResolvedValue({} as any);
46
- vi.spyOn(ClientService.prototype, 'createFile').mockResolvedValue({
47
- id: mockId,
48
- url: '',
49
- });
45
+
46
+ // Mock the new uploadWithProgress method from useFileStore
47
+ vi.spyOn(useFileStore, 'getState').mockReturnValue({
48
+ uploadWithProgress: vi.fn().mockResolvedValue({
49
+ id: mockId,
50
+ url: '',
51
+ dimensions: { width: 512, height: 512 },
52
+ filename: 'file.png',
53
+ }),
54
+ } as any);
55
+
56
+ // Mock store methods that are called in the implementation
50
57
  vi.spyOn(result.current, 'toggleDallEImageLoading');
51
- vi.spyOn(ClientService.prototype, 'checkFileHash').mockImplementation(
52
- async () => ({ isExist: false }) as any,
53
- );
58
+ vi.spyOn(result.current, 'updatePluginState').mockResolvedValue(undefined);
59
+ vi.spyOn(result.current, 'internal_updateMessageContent').mockResolvedValue(undefined);
54
60
 
55
61
  await act(async () => {
56
62
  await result.current.generateImageFromPrompts(prompts, messageId);
57
63
  });
58
64
  // For each prompt, loading is toggled on and then off
59
65
  expect(imageGenerationService.generateImage).toHaveBeenCalledTimes(prompts.length);
60
- // @ts-ignore
61
- expect(uploadService.uploadToClientS3).toHaveBeenCalledTimes(prompts.length);
66
+ expect(useFileStore.getState().uploadWithProgress).toHaveBeenCalledTimes(prompts.length);
62
67
  expect(result.current.toggleDallEImageLoading).toHaveBeenCalledTimes(prompts.length * 2);
63
68
  });
64
69
  });
@@ -74,7 +79,7 @@ describe('chatToolSlice - dalle', () => {
74
79
  draft[0].previewUrl = 'new-url';
75
80
  draft[0].imageId = 'new-id';
76
81
  };
77
- vi.spyOn(result.current, 'internal_updateMessageContent');
82
+ vi.spyOn(result.current, 'internal_updateMessageContent').mockResolvedValue(undefined);
78
83
 
79
84
  // 模拟 getMessageById 返回消息内容
80
85
  vi.spyOn(chatSelectors, 'getMessageById').mockImplementationOnce(
@@ -105,7 +110,9 @@ describe('chatToolSlice - dalle', () => {
105
110
  const data = [{ prompt: 'prompt 1' }, { prompt: 'prompt 2' }] as DallEImageItem[];
106
111
 
107
112
  // Mock generateImageFromPrompts
108
- const generateImageFromPromptsMock = vi.spyOn(result.current, 'generateImageFromPrompts');
113
+ const generateImageFromPromptsMock = vi
114
+ .spyOn(result.current, 'generateImageFromPrompts')
115
+ .mockResolvedValue(undefined);
109
116
 
110
117
  await act(async () => {
111
118
  await result.current.text2image(id, data);