@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.
- package/.cursor/rules/project-introduce.mdc +1 -15
- package/.cursor/rules/project-structure.mdc +227 -0
- package/.cursor/rules/testing-guide/db-model-test.mdc +5 -3
- package/.cursor/rules/testing-guide/testing-guide.mdc +153 -168
- package/.github/workflows/claude.yml +1 -1
- package/.github/workflows/test.yml +9 -0
- package/CHANGELOG.md +25 -0
- package/CLAUDE.md +11 -27
- package/changelog/v1.json +5 -0
- package/docs/development/basic/feature-development.mdx +1 -1
- package/docs/development/basic/feature-development.zh-CN.mdx +1 -1
- package/package.json +4 -4
- package/packages/const/src/image.ts +28 -0
- package/packages/const/src/index.ts +1 -0
- package/packages/database/package.json +4 -2
- package/packages/database/src/repositories/aiInfra/index.ts +1 -1
- package/packages/database/tests/setup-db.ts +3 -0
- package/packages/database/vitest.config.mts +33 -0
- package/packages/model-runtime/src/utils/modelParse.ts +1 -1
- package/packages/utils/src/client/imageDimensions.test.ts +95 -0
- package/packages/utils/src/client/imageDimensions.ts +54 -0
- package/packages/utils/src/number.test.ts +3 -1
- package/packages/utils/src/number.ts +1 -2
- package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +1 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +0 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +16 -6
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +14 -2
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +27 -2
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +23 -5
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useAutoDimensions.ts +56 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +82 -5
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/dimensionConstraints.test.ts +235 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/imageValidation.test.ts +401 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/dimensionConstraints.ts +54 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +3 -1
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +15 -2
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +5 -4
- package/src/libs/standard-parameters/index.ts +1 -1
- package/src/server/services/generation/index.ts +1 -1
- package/src/store/chat/slices/builtinTool/actions/dalle.test.ts +20 -13
- package/src/store/file/slices/upload/action.ts +18 -7
- package/src/store/image/slices/generationConfig/hooks.ts +1 -1
- package/tsconfig.json +1 -10
- package/packages/const/src/imageGeneration.ts +0 -16
- package/src/app/(backend)/trpc/desktop/[trpc]/route.ts +0 -26
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +0 -24
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +0 -15
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +0 -91
- package/src/app/desktop/devtools/page.tsx +0 -89
- package/src/app/desktop/layout.tsx +0 -31
- /package/apps/desktop/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/database/{vitest.config.ts → vitest.config.server.mts} +0 -0
- /package/packages/electron-server-ipc/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/file-loaders/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/model-runtime/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/prompts/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/utils/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/web-crawler/{vitest.config.ts → vitest.config.mts} +0 -0
- /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
|
+
});
|
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/dimensionConstraints.ts
ADDED
@@ -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
|
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
|
115
|
-
if (!dimensions.
|
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
|
-
|
120
|
-
const
|
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
|
@@ -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/
|
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
|
-
|
45
|
-
|
46
|
-
vi.spyOn(
|
47
|
-
|
48
|
-
|
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(
|
52
|
-
|
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
|
-
|
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
|
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);
|