@lobehub/chat 1.104.0 → 1.104.2
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/code-review.mdc +2 -0
- package/.cursor/rules/typescript.mdc +3 -1
- package/CHANGELOG.md +50 -0
- package/apps/desktop/src/main/core/ui/ShortcutManager.ts +61 -6
- package/apps/desktop/src/main/core/ui/__tests__/ShortcutManager.test.ts +539 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +6 -1
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +3 -2
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +27 -24
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +14 -3
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +4 -7
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +3 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.test.ts +600 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +126 -7
- package/src/const/imageGeneration.ts +18 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +3 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +7 -5
- package/src/libs/model-runtime/utils/streams/openai/openai.ts +8 -4
- package/src/libs/model-runtime/utils/usageConverter.test.ts +45 -1
- package/src/libs/model-runtime/utils/usageConverter.ts +6 -2
- package/src/server/services/generation/index.test.ts +848 -0
- package/src/server/services/generation/index.ts +90 -69
- package/src/utils/number.test.ts +101 -1
- package/src/utils/number.ts +42 -0
package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx
CHANGED
@@ -8,10 +8,20 @@ import ImageItem from '@/components/ImageItem';
|
|
8
8
|
import { ActionButtons } from './ActionButtons';
|
9
9
|
import { useStyles } from './styles';
|
10
10
|
import { SuccessStateProps } from './types';
|
11
|
+
import { getThumbnailMaxWidth } from './utils';
|
11
12
|
|
12
13
|
// 成功状态组件
|
13
14
|
export const SuccessState = memo<SuccessStateProps>(
|
14
|
-
({
|
15
|
+
({
|
16
|
+
generation,
|
17
|
+
generationBatch,
|
18
|
+
prompt,
|
19
|
+
aspectRatio,
|
20
|
+
onDelete,
|
21
|
+
onDownload,
|
22
|
+
onCopySeed,
|
23
|
+
seedTooltip,
|
24
|
+
}) => {
|
15
25
|
const { styles } = useStyles();
|
16
26
|
|
17
27
|
return (
|
@@ -21,7 +31,7 @@ export const SuccessState = memo<SuccessStateProps>(
|
|
21
31
|
justify={'center'}
|
22
32
|
style={{
|
23
33
|
aspectRatio,
|
24
|
-
maxWidth: generation
|
34
|
+
maxWidth: getThumbnailMaxWidth(generation, generationBatch),
|
25
35
|
}}
|
26
36
|
variant={'filled'}
|
27
37
|
>
|
@@ -31,7 +41,8 @@ export const SuccessState = memo<SuccessStateProps>(
|
|
31
41
|
src: generation.asset!.url,
|
32
42
|
}}
|
33
43
|
style={{ height: '100%', width: '100%' }}
|
34
|
-
|
44
|
+
// Thumbnail quality is too bad
|
45
|
+
url={generation.asset!.url}
|
35
46
|
/>
|
36
47
|
<ActionButtons
|
37
48
|
onCopySeed={onCopySeed}
|
@@ -37,13 +37,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
|
37
37
|
const shouldPoll = !isFinalized;
|
38
38
|
useCheckGenerationStatus(generation.id, generation.task.id, activeTopicId!, shouldPoll);
|
39
39
|
|
40
|
-
const aspectRatio = getAspectRatio(
|
41
|
-
generation.asset ?? {
|
42
|
-
height: generationBatch.config?.height,
|
43
|
-
type: 'image',
|
44
|
-
width: generationBatch.config?.width,
|
45
|
-
},
|
46
|
-
);
|
40
|
+
const aspectRatio = getAspectRatio(generation, generationBatch);
|
47
41
|
|
48
42
|
// 事件处理函数
|
49
43
|
const handleDeleteGeneration = async () => {
|
@@ -120,6 +114,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
|
120
114
|
<SuccessState
|
121
115
|
aspectRatio={aspectRatio}
|
122
116
|
generation={generation}
|
117
|
+
generationBatch={generationBatch}
|
123
118
|
onCopySeed={handleCopySeed}
|
124
119
|
onDelete={handleDeleteGeneration}
|
125
120
|
onDownload={handleDownloadImage}
|
@@ -134,6 +129,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
|
134
129
|
<ErrorState
|
135
130
|
aspectRatio={aspectRatio}
|
136
131
|
generation={generation}
|
132
|
+
generationBatch={generationBatch}
|
137
133
|
onCopyError={handleCopyError}
|
138
134
|
onDelete={handleDeleteGeneration}
|
139
135
|
/>
|
@@ -145,6 +141,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
|
145
141
|
<LoadingState
|
146
142
|
aspectRatio={aspectRatio}
|
147
143
|
generation={generation}
|
144
|
+
generationBatch={generationBatch}
|
148
145
|
onDelete={handleDeleteGeneration}
|
149
146
|
/>
|
150
147
|
);
|
@@ -18,6 +18,7 @@ export interface ActionButtonsProps {
|
|
18
18
|
export interface SuccessStateProps {
|
19
19
|
aspectRatio: string;
|
20
20
|
generation: Generation;
|
21
|
+
generationBatch: GenerationBatch;
|
21
22
|
onCopySeed?: () => void;
|
22
23
|
onDelete: () => void;
|
23
24
|
onDownload: () => void;
|
@@ -28,6 +29,7 @@ export interface SuccessStateProps {
|
|
28
29
|
export interface ErrorStateProps {
|
29
30
|
aspectRatio: string;
|
30
31
|
generation: Generation;
|
32
|
+
generationBatch: GenerationBatch;
|
31
33
|
onCopyError: () => void;
|
32
34
|
onDelete: () => void;
|
33
35
|
}
|
@@ -35,5 +37,6 @@ export interface ErrorStateProps {
|
|
35
37
|
export interface LoadingStateProps {
|
36
38
|
aspectRatio: string;
|
37
39
|
generation: Generation;
|
40
|
+
generationBatch: GenerationBatch;
|
38
41
|
onDelete: () => void;
|
39
42
|
}
|
@@ -0,0 +1,600 @@
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import type { Generation, GenerationBatch } from '@/types/generation';
|
4
|
+
|
5
|
+
// Import functions for testing
|
6
|
+
import {
|
7
|
+
DEFAULT_MAX_ITEM_WIDTH,
|
8
|
+
getAspectRatio,
|
9
|
+
getImageDimensions,
|
10
|
+
getThumbnailMaxWidth,
|
11
|
+
} from './utils';
|
12
|
+
|
13
|
+
describe('getImageDimensions', () => {
|
14
|
+
// Mock base generation object
|
15
|
+
const baseGeneration: Generation = {
|
16
|
+
id: 'test-gen-id',
|
17
|
+
seed: 12345,
|
18
|
+
createdAt: new Date(),
|
19
|
+
asyncTaskId: null,
|
20
|
+
task: {
|
21
|
+
id: 'task-id',
|
22
|
+
status: 'success' as any,
|
23
|
+
},
|
24
|
+
};
|
25
|
+
|
26
|
+
describe('with asset dimensions', () => {
|
27
|
+
it('should return width, height and aspect ratio from asset', () => {
|
28
|
+
const generation: Generation = {
|
29
|
+
...baseGeneration,
|
30
|
+
asset: {
|
31
|
+
type: 'image',
|
32
|
+
width: 1920,
|
33
|
+
height: 1080,
|
34
|
+
},
|
35
|
+
};
|
36
|
+
|
37
|
+
const result = getImageDimensions(generation);
|
38
|
+
expect(result).toEqual({
|
39
|
+
width: 1920,
|
40
|
+
height: 1080,
|
41
|
+
aspectRatio: '1920 / 1080',
|
42
|
+
});
|
43
|
+
});
|
44
|
+
|
45
|
+
it('should prioritize asset even when other sources exist', () => {
|
46
|
+
const generation: Generation = {
|
47
|
+
...baseGeneration,
|
48
|
+
asset: {
|
49
|
+
type: 'image',
|
50
|
+
width: 800,
|
51
|
+
height: 600,
|
52
|
+
},
|
53
|
+
};
|
54
|
+
|
55
|
+
const generationBatch: GenerationBatch = {
|
56
|
+
id: 'batch-id',
|
57
|
+
provider: 'test',
|
58
|
+
model: 'test-model',
|
59
|
+
prompt: 'test prompt',
|
60
|
+
width: 1024,
|
61
|
+
height: 1024,
|
62
|
+
config: {
|
63
|
+
prompt: 'test',
|
64
|
+
width: 512,
|
65
|
+
height: 512,
|
66
|
+
},
|
67
|
+
createdAt: new Date(),
|
68
|
+
generations: [],
|
69
|
+
};
|
70
|
+
|
71
|
+
const result = getImageDimensions(generation, generationBatch);
|
72
|
+
expect(result).toEqual({
|
73
|
+
width: 800,
|
74
|
+
height: 600,
|
75
|
+
aspectRatio: '800 / 600',
|
76
|
+
});
|
77
|
+
});
|
78
|
+
});
|
79
|
+
|
80
|
+
describe('with config dimensions', () => {
|
81
|
+
it('should return dimensions from config when asset is not available', () => {
|
82
|
+
const generation: Generation = {
|
83
|
+
...baseGeneration,
|
84
|
+
asset: null,
|
85
|
+
};
|
86
|
+
|
87
|
+
const generationBatch: GenerationBatch = {
|
88
|
+
id: 'batch-id',
|
89
|
+
provider: 'test',
|
90
|
+
model: 'test-model',
|
91
|
+
prompt: 'test prompt',
|
92
|
+
config: {
|
93
|
+
prompt: 'test',
|
94
|
+
width: 1024,
|
95
|
+
height: 768,
|
96
|
+
},
|
97
|
+
createdAt: new Date(),
|
98
|
+
generations: [],
|
99
|
+
};
|
100
|
+
|
101
|
+
const result = getImageDimensions(generation, generationBatch);
|
102
|
+
expect(result).toEqual({
|
103
|
+
width: 1024,
|
104
|
+
height: 768,
|
105
|
+
aspectRatio: '1024 / 768',
|
106
|
+
});
|
107
|
+
});
|
108
|
+
});
|
109
|
+
|
110
|
+
describe('with batch top-level dimensions', () => {
|
111
|
+
it('should return dimensions from batch when config is not available', () => {
|
112
|
+
const generation: Generation = {
|
113
|
+
...baseGeneration,
|
114
|
+
asset: null,
|
115
|
+
};
|
116
|
+
|
117
|
+
const generationBatch: GenerationBatch = {
|
118
|
+
id: 'batch-id',
|
119
|
+
provider: 'test',
|
120
|
+
model: 'test-model',
|
121
|
+
prompt: 'test prompt',
|
122
|
+
width: 1280,
|
123
|
+
height: 720,
|
124
|
+
createdAt: new Date(),
|
125
|
+
generations: [],
|
126
|
+
};
|
127
|
+
|
128
|
+
const result = getImageDimensions(generation, generationBatch);
|
129
|
+
expect(result).toEqual({
|
130
|
+
width: 1280,
|
131
|
+
height: 720,
|
132
|
+
aspectRatio: '1280 / 720',
|
133
|
+
});
|
134
|
+
});
|
135
|
+
});
|
136
|
+
|
137
|
+
describe('with size parameter', () => {
|
138
|
+
it('should parse dimensions from size parameter', () => {
|
139
|
+
const generation: Generation = {
|
140
|
+
...baseGeneration,
|
141
|
+
asset: null,
|
142
|
+
};
|
143
|
+
|
144
|
+
const generationBatch: GenerationBatch = {
|
145
|
+
id: 'batch-id',
|
146
|
+
provider: 'test',
|
147
|
+
model: 'test-model',
|
148
|
+
prompt: 'test prompt',
|
149
|
+
config: {
|
150
|
+
prompt: 'test',
|
151
|
+
size: '1920x1080',
|
152
|
+
},
|
153
|
+
createdAt: new Date(),
|
154
|
+
generations: [],
|
155
|
+
};
|
156
|
+
|
157
|
+
const result = getImageDimensions(generation, generationBatch);
|
158
|
+
expect(result).toEqual({
|
159
|
+
width: 1920,
|
160
|
+
height: 1080,
|
161
|
+
aspectRatio: '1920 / 1080',
|
162
|
+
});
|
163
|
+
});
|
164
|
+
|
165
|
+
it('should ignore size when it is "auto"', () => {
|
166
|
+
const generation: Generation = {
|
167
|
+
...baseGeneration,
|
168
|
+
asset: null,
|
169
|
+
};
|
170
|
+
|
171
|
+
const generationBatch: GenerationBatch = {
|
172
|
+
id: 'batch-id',
|
173
|
+
provider: 'test',
|
174
|
+
model: 'test-model',
|
175
|
+
prompt: 'test prompt',
|
176
|
+
config: {
|
177
|
+
prompt: 'test',
|
178
|
+
size: 'auto',
|
179
|
+
aspectRatio: '16:9',
|
180
|
+
},
|
181
|
+
createdAt: new Date(),
|
182
|
+
generations: [],
|
183
|
+
};
|
184
|
+
|
185
|
+
const result = getImageDimensions(generation, generationBatch);
|
186
|
+
expect(result).toEqual({
|
187
|
+
width: null,
|
188
|
+
height: null,
|
189
|
+
aspectRatio: '16 / 9',
|
190
|
+
});
|
191
|
+
});
|
192
|
+
});
|
193
|
+
|
194
|
+
describe('with aspectRatio parameter only', () => {
|
195
|
+
it('should return aspect ratio without dimensions', () => {
|
196
|
+
const generation: Generation = {
|
197
|
+
...baseGeneration,
|
198
|
+
asset: null,
|
199
|
+
};
|
200
|
+
|
201
|
+
const generationBatch: GenerationBatch = {
|
202
|
+
id: 'batch-id',
|
203
|
+
provider: 'test',
|
204
|
+
model: 'test-model',
|
205
|
+
prompt: 'test prompt',
|
206
|
+
config: {
|
207
|
+
prompt: 'test',
|
208
|
+
aspectRatio: '16:9',
|
209
|
+
},
|
210
|
+
createdAt: new Date(),
|
211
|
+
generations: [],
|
212
|
+
};
|
213
|
+
|
214
|
+
const result = getImageDimensions(generation, generationBatch);
|
215
|
+
expect(result).toEqual({
|
216
|
+
width: null,
|
217
|
+
height: null,
|
218
|
+
aspectRatio: '16 / 9',
|
219
|
+
});
|
220
|
+
});
|
221
|
+
|
222
|
+
it('should handle various aspect ratio formats', () => {
|
223
|
+
const testCases = [
|
224
|
+
{ aspectRatio: '1:1', expected: '1 / 1' },
|
225
|
+
{ aspectRatio: '4:3', expected: '4 / 3' },
|
226
|
+
{ aspectRatio: '16:9', expected: '16 / 9' },
|
227
|
+
{ aspectRatio: '21:9', expected: '21 / 9' },
|
228
|
+
];
|
229
|
+
|
230
|
+
testCases.forEach(({ aspectRatio, expected }) => {
|
231
|
+
const generation: Generation = {
|
232
|
+
...baseGeneration,
|
233
|
+
asset: null,
|
234
|
+
};
|
235
|
+
|
236
|
+
const generationBatch: GenerationBatch = {
|
237
|
+
id: 'batch-id',
|
238
|
+
provider: 'test',
|
239
|
+
model: 'test-model',
|
240
|
+
prompt: 'test prompt',
|
241
|
+
config: {
|
242
|
+
prompt: 'test',
|
243
|
+
aspectRatio,
|
244
|
+
},
|
245
|
+
createdAt: new Date(),
|
246
|
+
generations: [],
|
247
|
+
};
|
248
|
+
|
249
|
+
const result = getImageDimensions(generation, generationBatch);
|
250
|
+
expect(result.aspectRatio).toBe(expected);
|
251
|
+
});
|
252
|
+
});
|
253
|
+
});
|
254
|
+
|
255
|
+
describe('edge cases', () => {
|
256
|
+
it('should return all null when no dimensions are available', () => {
|
257
|
+
const generation: Generation = {
|
258
|
+
...baseGeneration,
|
259
|
+
asset: null,
|
260
|
+
};
|
261
|
+
|
262
|
+
const result = getImageDimensions(generation);
|
263
|
+
expect(result).toEqual({
|
264
|
+
width: null,
|
265
|
+
height: null,
|
266
|
+
aspectRatio: null,
|
267
|
+
});
|
268
|
+
});
|
269
|
+
|
270
|
+
it('should handle partial asset dimensions', () => {
|
271
|
+
const generation: Generation = {
|
272
|
+
...baseGeneration,
|
273
|
+
asset: {
|
274
|
+
type: 'image',
|
275
|
+
width: 1920,
|
276
|
+
// height is missing
|
277
|
+
},
|
278
|
+
};
|
279
|
+
|
280
|
+
const generationBatch: GenerationBatch = {
|
281
|
+
id: 'batch-id',
|
282
|
+
provider: 'test',
|
283
|
+
model: 'test-model',
|
284
|
+
prompt: 'test prompt',
|
285
|
+
config: {
|
286
|
+
prompt: 'test',
|
287
|
+
width: 1024,
|
288
|
+
height: 768,
|
289
|
+
},
|
290
|
+
createdAt: new Date(),
|
291
|
+
generations: [],
|
292
|
+
};
|
293
|
+
|
294
|
+
const result = getImageDimensions(generation, generationBatch);
|
295
|
+
expect(result).toEqual({
|
296
|
+
width: 1024,
|
297
|
+
height: 768,
|
298
|
+
aspectRatio: '1024 / 768',
|
299
|
+
});
|
300
|
+
});
|
301
|
+
|
302
|
+
it('should handle invalid size format', () => {
|
303
|
+
const generation: Generation = {
|
304
|
+
...baseGeneration,
|
305
|
+
asset: null,
|
306
|
+
};
|
307
|
+
|
308
|
+
const generationBatch: GenerationBatch = {
|
309
|
+
id: 'batch-id',
|
310
|
+
provider: 'test',
|
311
|
+
model: 'test-model',
|
312
|
+
prompt: 'test prompt',
|
313
|
+
config: {
|
314
|
+
prompt: 'test',
|
315
|
+
size: 'invalid-format',
|
316
|
+
},
|
317
|
+
createdAt: new Date(),
|
318
|
+
generations: [],
|
319
|
+
};
|
320
|
+
|
321
|
+
const result = getImageDimensions(generation, generationBatch);
|
322
|
+
expect(result).toEqual({
|
323
|
+
width: null,
|
324
|
+
height: null,
|
325
|
+
aspectRatio: null,
|
326
|
+
});
|
327
|
+
});
|
328
|
+
|
329
|
+
it('should handle invalid aspectRatio format', () => {
|
330
|
+
const generation: Generation = {
|
331
|
+
...baseGeneration,
|
332
|
+
asset: null,
|
333
|
+
};
|
334
|
+
|
335
|
+
const generationBatch: GenerationBatch = {
|
336
|
+
id: 'batch-id',
|
337
|
+
provider: 'test',
|
338
|
+
model: 'test-model',
|
339
|
+
prompt: 'test prompt',
|
340
|
+
config: {
|
341
|
+
prompt: 'test',
|
342
|
+
aspectRatio: 'invalid-format',
|
343
|
+
},
|
344
|
+
createdAt: new Date(),
|
345
|
+
generations: [],
|
346
|
+
};
|
347
|
+
|
348
|
+
const result = getImageDimensions(generation, generationBatch);
|
349
|
+
expect(result).toEqual({
|
350
|
+
width: null,
|
351
|
+
height: null,
|
352
|
+
aspectRatio: null,
|
353
|
+
});
|
354
|
+
});
|
355
|
+
|
356
|
+
it('should handle zero dimensions', () => {
|
357
|
+
const generation: Generation = {
|
358
|
+
...baseGeneration,
|
359
|
+
asset: {
|
360
|
+
type: 'image',
|
361
|
+
width: 0,
|
362
|
+
height: 0,
|
363
|
+
},
|
364
|
+
};
|
365
|
+
|
366
|
+
const result = getImageDimensions(generation);
|
367
|
+
expect(result).toEqual({
|
368
|
+
width: null,
|
369
|
+
height: null,
|
370
|
+
aspectRatio: null,
|
371
|
+
});
|
372
|
+
});
|
373
|
+
});
|
374
|
+
});
|
375
|
+
|
376
|
+
describe('getAspectRatio (isolated unit testing)', () => {
|
377
|
+
const mockGeneration: Generation = {
|
378
|
+
id: 'test-gen-id',
|
379
|
+
seed: 12345,
|
380
|
+
createdAt: new Date(),
|
381
|
+
asyncTaskId: null,
|
382
|
+
task: {
|
383
|
+
id: 'task-id',
|
384
|
+
status: 'success' as any,
|
385
|
+
},
|
386
|
+
};
|
387
|
+
const mockGenerationBatch: GenerationBatch = {
|
388
|
+
id: 'test-batch-id',
|
389
|
+
provider: 'test-provider',
|
390
|
+
model: 'test-model',
|
391
|
+
prompt: 'test prompt',
|
392
|
+
createdAt: new Date(),
|
393
|
+
generations: [],
|
394
|
+
};
|
395
|
+
|
396
|
+
beforeEach(() => {
|
397
|
+
vi.clearAllMocks();
|
398
|
+
});
|
399
|
+
|
400
|
+
it('should return aspectRatio from getImageDimensions when dimensions have aspectRatio', () => {
|
401
|
+
// Test the actual implementation directly with mock data
|
402
|
+
const mockGen: Generation = {
|
403
|
+
...mockGeneration,
|
404
|
+
asset: {
|
405
|
+
type: 'image',
|
406
|
+
width: 1920,
|
407
|
+
height: 1080,
|
408
|
+
},
|
409
|
+
};
|
410
|
+
|
411
|
+
const result = getAspectRatio(mockGen);
|
412
|
+
expect(result).toBe('1920 / 1080');
|
413
|
+
});
|
414
|
+
|
415
|
+
it('should return default "1 / 1" when no dimensions are available', () => {
|
416
|
+
const result = getAspectRatio(mockGeneration, mockGenerationBatch);
|
417
|
+
expect(result).toBe('1 / 1');
|
418
|
+
});
|
419
|
+
|
420
|
+
it('should work with different aspectRatio sources', () => {
|
421
|
+
const mockBatch: GenerationBatch = {
|
422
|
+
id: 'test-batch',
|
423
|
+
provider: 'test-provider',
|
424
|
+
model: 'test-model',
|
425
|
+
prompt: 'test prompt',
|
426
|
+
createdAt: new Date(),
|
427
|
+
generations: [],
|
428
|
+
config: {
|
429
|
+
prompt: 'test prompt',
|
430
|
+
aspectRatio: '16:9',
|
431
|
+
},
|
432
|
+
};
|
433
|
+
|
434
|
+
const result = getAspectRatio(mockGeneration, mockBatch);
|
435
|
+
expect(result).toBe('16 / 9');
|
436
|
+
});
|
437
|
+
});
|
438
|
+
|
439
|
+
describe('getThumbnailMaxWidth (isolated unit testing)', () => {
|
440
|
+
const mockGeneration: Generation = {
|
441
|
+
id: 'test-gen-id',
|
442
|
+
seed: 12345,
|
443
|
+
createdAt: new Date(),
|
444
|
+
asyncTaskId: null,
|
445
|
+
task: {
|
446
|
+
id: 'task-id',
|
447
|
+
status: 'success' as any,
|
448
|
+
},
|
449
|
+
};
|
450
|
+
const mockGenerationBatch: GenerationBatch = {
|
451
|
+
id: 'test-batch-id',
|
452
|
+
provider: 'test-provider',
|
453
|
+
model: 'test-model',
|
454
|
+
prompt: 'test prompt',
|
455
|
+
createdAt: new Date(),
|
456
|
+
generations: [],
|
457
|
+
};
|
458
|
+
|
459
|
+
// Mock window.innerHeight for tests
|
460
|
+
const originalWindow = global.window;
|
461
|
+
|
462
|
+
beforeEach(() => {
|
463
|
+
vi.clearAllMocks();
|
464
|
+
Object.defineProperty(global, 'window', {
|
465
|
+
writable: true,
|
466
|
+
value: {
|
467
|
+
innerHeight: 800,
|
468
|
+
},
|
469
|
+
});
|
470
|
+
});
|
471
|
+
|
472
|
+
afterEach(() => {
|
473
|
+
global.window = originalWindow;
|
474
|
+
});
|
475
|
+
|
476
|
+
it('should return DEFAULT_MAX_ITEM_WIDTH when no dimensions available', () => {
|
477
|
+
const result = getThumbnailMaxWidth(mockGeneration, mockGenerationBatch);
|
478
|
+
expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
|
479
|
+
});
|
480
|
+
|
481
|
+
it('should return DEFAULT_MAX_ITEM_WIDTH when width is missing', () => {
|
482
|
+
const mockGen: Generation = {
|
483
|
+
...mockGeneration,
|
484
|
+
// No asset with width/height, should fall back to default
|
485
|
+
};
|
486
|
+
const result = getThumbnailMaxWidth(mockGen);
|
487
|
+
expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
|
488
|
+
});
|
489
|
+
|
490
|
+
it('should return DEFAULT_MAX_ITEM_WIDTH when height is missing', () => {
|
491
|
+
const mockGen: Generation = {
|
492
|
+
...mockGeneration,
|
493
|
+
// No asset with valid dimensions
|
494
|
+
};
|
495
|
+
const result = getThumbnailMaxWidth(mockGen);
|
496
|
+
expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
|
497
|
+
});
|
498
|
+
|
499
|
+
it('should calculate width based on screen height constraint', () => {
|
500
|
+
const mockGen: Generation = {
|
501
|
+
...mockGeneration,
|
502
|
+
asset: {
|
503
|
+
type: 'image',
|
504
|
+
width: 300,
|
505
|
+
height: 200,
|
506
|
+
},
|
507
|
+
};
|
508
|
+
|
509
|
+
// aspectRatio = 300/200 = 1.5
|
510
|
+
// maxScreenHeight = 800/2 = 400
|
511
|
+
// maxWidthFromHeight = 400 * 1.5 = 600
|
512
|
+
// maxReasonableWidth = 200 * 2 = 400
|
513
|
+
// min(600, 400) = 400
|
514
|
+
const result = getThumbnailMaxWidth(mockGen);
|
515
|
+
expect(result).toBe(400);
|
516
|
+
});
|
517
|
+
|
518
|
+
it('should apply maxReasonableWidth limit', () => {
|
519
|
+
const mockGen: Generation = {
|
520
|
+
...mockGeneration,
|
521
|
+
asset: {
|
522
|
+
type: 'image',
|
523
|
+
width: 600,
|
524
|
+
height: 200,
|
525
|
+
},
|
526
|
+
};
|
527
|
+
|
528
|
+
// aspectRatio = 600/200 = 3
|
529
|
+
// maxScreenHeight = 800/2 = 400
|
530
|
+
// maxWidthFromHeight = 400 * 3 = 1200
|
531
|
+
// maxReasonableWidth = 200 * 2 = 400
|
532
|
+
// min(1200, 400) = 400
|
533
|
+
const result = getThumbnailMaxWidth(mockGen);
|
534
|
+
expect(result).toBe(400);
|
535
|
+
});
|
536
|
+
|
537
|
+
it('should use screen height constraint when smaller', () => {
|
538
|
+
const mockGen: Generation = {
|
539
|
+
...mockGeneration,
|
540
|
+
asset: {
|
541
|
+
type: 'image',
|
542
|
+
width: 200,
|
543
|
+
height: 400,
|
544
|
+
},
|
545
|
+
};
|
546
|
+
|
547
|
+
// aspectRatio = 200/400 = 0.5
|
548
|
+
// maxScreenHeight = 800/2 = 400
|
549
|
+
// maxWidthFromHeight = 400 * 0.5 = 200
|
550
|
+
// maxReasonableWidth = 200 * 2 = 400
|
551
|
+
// min(200, 400) = 200
|
552
|
+
const result = getThumbnailMaxWidth(mockGen);
|
553
|
+
expect(result).toBe(200);
|
554
|
+
});
|
555
|
+
|
556
|
+
it('should handle different window.innerHeight values', () => {
|
557
|
+
Object.defineProperty(global, 'window', {
|
558
|
+
writable: true,
|
559
|
+
value: {
|
560
|
+
innerHeight: 600,
|
561
|
+
},
|
562
|
+
});
|
563
|
+
|
564
|
+
const mockGen: Generation = {
|
565
|
+
...mockGeneration,
|
566
|
+
asset: {
|
567
|
+
type: 'image',
|
568
|
+
width: 400,
|
569
|
+
height: 200,
|
570
|
+
},
|
571
|
+
};
|
572
|
+
|
573
|
+
// aspectRatio = 400/200 = 2
|
574
|
+
// maxScreenHeight = 600/2 = 300
|
575
|
+
// maxWidthFromHeight = 300 * 2 = 600
|
576
|
+
// maxReasonableWidth = 200 * 2 = 400
|
577
|
+
// min(600, 400) = 400
|
578
|
+
const result = getThumbnailMaxWidth(mockGen);
|
579
|
+
expect(result).toBe(400);
|
580
|
+
});
|
581
|
+
|
582
|
+
it('should round calculated width correctly', () => {
|
583
|
+
const mockGen: Generation = {
|
584
|
+
...mockGeneration,
|
585
|
+
asset: {
|
586
|
+
type: 'image',
|
587
|
+
width: 512,
|
588
|
+
height: 1000,
|
589
|
+
},
|
590
|
+
};
|
591
|
+
|
592
|
+
// aspectRatio = 512/1000 = 0.512
|
593
|
+
// maxScreenHeight = 800/2 = 400
|
594
|
+
// maxWidthFromHeight = Math.round(400 * 0.512) = Math.round(204.8) = 205
|
595
|
+
// maxReasonableWidth = 200 * 2 = 400
|
596
|
+
// min(205, 400) = 205
|
597
|
+
const result = getThumbnailMaxWidth(mockGen);
|
598
|
+
expect(result).toBe(205);
|
599
|
+
});
|
600
|
+
});
|