@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.
Files changed (25) hide show
  1. package/.cursor/rules/code-review.mdc +2 -0
  2. package/.cursor/rules/typescript.mdc +3 -1
  3. package/CHANGELOG.md +50 -0
  4. package/apps/desktop/src/main/core/ui/ShortcutManager.ts +61 -6
  5. package/apps/desktop/src/main/core/ui/__tests__/ShortcutManager.test.ts +539 -0
  6. package/changelog/v1.json +18 -0
  7. package/package.json +1 -1
  8. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +6 -1
  9. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +3 -2
  10. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +27 -24
  11. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +14 -3
  12. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +4 -7
  13. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +3 -0
  14. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.test.ts +600 -0
  15. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +126 -7
  16. package/src/const/imageGeneration.ts +18 -0
  17. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +3 -0
  18. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +7 -5
  19. package/src/libs/model-runtime/utils/streams/openai/openai.ts +8 -4
  20. package/src/libs/model-runtime/utils/usageConverter.test.ts +45 -1
  21. package/src/libs/model-runtime/utils/usageConverter.ts +6 -2
  22. package/src/server/services/generation/index.test.ts +848 -0
  23. package/src/server/services/generation/index.ts +90 -69
  24. package/src/utils/number.test.ts +101 -1
  25. package/src/utils/number.ts +42 -0
@@ -0,0 +1,539 @@
1
+ import { globalShortcut } from 'electron';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
5
+
6
+ import type { App } from '../../App';
7
+ import { ShortcutManager } from '../ShortcutManager';
8
+
9
+ // Mock electron
10
+ vi.mock('electron', () => ({
11
+ globalShortcut: {
12
+ register: vi.fn(),
13
+ unregister: vi.fn(),
14
+ unregisterAll: vi.fn(),
15
+ isRegistered: vi.fn(),
16
+ },
17
+ }));
18
+
19
+ // Mock Logger
20
+ vi.mock('@/utils/logger', () => ({
21
+ createLogger: () => ({
22
+ debug: vi.fn(),
23
+ info: vi.fn(),
24
+ warn: vi.fn(),
25
+ error: vi.fn(),
26
+ }),
27
+ }));
28
+
29
+ // Mock DEFAULT_SHORTCUTS_CONFIG
30
+ vi.mock('@/shortcuts', () => ({
31
+ DEFAULT_SHORTCUTS_CONFIG: {
32
+ showApp: 'Control+E',
33
+ openSettings: 'CommandOrControl+,',
34
+ },
35
+ }));
36
+
37
+ describe('ShortcutManager', () => {
38
+ let shortcutManager: ShortcutManager;
39
+ let mockApp: App;
40
+ let mockStoreManager: any;
41
+ let mockShortcutMethodMap: Map<string, () => void>;
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+
46
+ // Reset all mocks to their default behavior
47
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
48
+ vi.mocked(globalShortcut.unregister).mockReturnValue(undefined);
49
+ vi.mocked(globalShortcut.unregisterAll).mockReturnValue(undefined);
50
+ vi.mocked(globalShortcut.isRegistered).mockReturnValue(false);
51
+
52
+ // Mock store manager
53
+ mockStoreManager = {
54
+ get: vi.fn(),
55
+ set: vi.fn(),
56
+ };
57
+
58
+ // Mock shortcut method map
59
+ mockShortcutMethodMap = new Map();
60
+ const showAppMethod = vi.fn();
61
+ const openSettingsMethod = vi.fn();
62
+ mockShortcutMethodMap.set('showApp', showAppMethod);
63
+ mockShortcutMethodMap.set('openSettings', openSettingsMethod);
64
+
65
+ // Mock App
66
+ mockApp = {
67
+ storeManager: mockStoreManager,
68
+ shortcutMethodMap: mockShortcutMethodMap,
69
+ } as unknown as App;
70
+
71
+ shortcutManager = new ShortcutManager(mockApp);
72
+ });
73
+
74
+ describe('constructor', () => {
75
+ it('should initialize shortcut manager with app', () => {
76
+ expect(shortcutManager).toBeDefined();
77
+ expect(shortcutManager['app']).toBe(mockApp);
78
+ });
79
+
80
+ it('should populate shortcuts map from app shortcut method map', () => {
81
+ expect(shortcutManager['shortcuts'].size).toBe(2);
82
+ expect(shortcutManager['shortcuts'].has('showApp')).toBe(true);
83
+ expect(shortcutManager['shortcuts'].has('openSettings')).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe('convertAcceleratorFormat', () => {
88
+ it('should convert mod to CommandOrControl', () => {
89
+ const result = shortcutManager['convertAcceleratorFormat']('mod+e');
90
+ expect(result).toBe('CommandOrControl+E');
91
+ });
92
+
93
+ it('should preserve other keys as is except single characters', () => {
94
+ const result = shortcutManager['convertAcceleratorFormat']('ctrl+alt+f12');
95
+ expect(result).toBe('ctrl+alt+f12');
96
+ });
97
+
98
+ it('should handle single character keys with uppercase', () => {
99
+ const result = shortcutManager['convertAcceleratorFormat']('ctrl + a');
100
+ expect(result).toBe('ctrl+A');
101
+ });
102
+
103
+ it('should handle complex combinations', () => {
104
+ const result = shortcutManager['convertAcceleratorFormat']('mod+shift+delete');
105
+ expect(result).toBe('CommandOrControl+shift+delete');
106
+ });
107
+ });
108
+
109
+ describe('initialize', () => {
110
+ it('should load shortcuts config and register shortcuts', () => {
111
+ // Mock store to return empty config (will use defaults)
112
+ mockStoreManager.get.mockReturnValue({});
113
+
114
+ shortcutManager.initialize();
115
+
116
+ expect(mockStoreManager.get).toHaveBeenCalledWith('shortcuts');
117
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
118
+ expect(globalShortcut.register).toHaveBeenCalledWith('Control+E', expect.any(Function));
119
+ expect(globalShortcut.register).toHaveBeenCalledWith(
120
+ 'CommandOrControl+,',
121
+ expect.any(Function),
122
+ );
123
+ });
124
+
125
+ it('should handle stored config with filtering', () => {
126
+ const storedConfig = {
127
+ showApp: 'Alt+E',
128
+ openSettings: 'Ctrl+Shift+P',
129
+ invalidKey: 'Ctrl+I', // Should be filtered out
130
+ };
131
+ mockStoreManager.get.mockReturnValue(storedConfig);
132
+
133
+ shortcutManager.initialize();
134
+
135
+ const config = shortcutManager.getShortcutsConfig();
136
+ expect(config.showApp).toBe('Alt+E');
137
+ expect(config.openSettings).toBe('Ctrl+Shift+P');
138
+ expect(config.invalidKey).toBeUndefined();
139
+ });
140
+ });
141
+
142
+ describe('getShortcutsConfig', () => {
143
+ it('should return current shortcuts configuration', () => {
144
+ mockStoreManager.get.mockReturnValue({});
145
+ shortcutManager.initialize();
146
+
147
+ const config = shortcutManager.getShortcutsConfig();
148
+ expect(config).toEqual(DEFAULT_SHORTCUTS_CONFIG);
149
+ });
150
+ });
151
+
152
+ describe('updateShortcutConfig', () => {
153
+ beforeEach(() => {
154
+ mockStoreManager.get.mockReturnValue({});
155
+ shortcutManager.initialize();
156
+ });
157
+
158
+ it('should successfully update valid shortcut', () => {
159
+ const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
160
+
161
+ expect(result.success).toBe(true);
162
+ expect(result.errorType).toBeUndefined();
163
+ expect(mockStoreManager.set).toHaveBeenCalledWith(
164
+ 'shortcuts',
165
+ expect.objectContaining({
166
+ showApp: 'Alt+E',
167
+ }),
168
+ );
169
+ });
170
+
171
+ it('should reject invalid shortcut ID', () => {
172
+ const result = shortcutManager.updateShortcutConfig('invalidId', 'Alt+E');
173
+
174
+ expect(result.success).toBe(false);
175
+ expect(result.errorType).toBe('INVALID_ID');
176
+ });
177
+
178
+ it('should reject empty accelerator', () => {
179
+ const result = shortcutManager.updateShortcutConfig('showApp', '');
180
+
181
+ expect(result.success).toBe(false);
182
+ expect(result.errorType).toBe('INVALID_FORMAT');
183
+ });
184
+
185
+ it('should reject accelerator without modifier keys', () => {
186
+ const result = shortcutManager.updateShortcutConfig('showApp', 'E');
187
+
188
+ expect(result.success).toBe(false);
189
+ expect(result.errorType).toBe('INVALID_FORMAT');
190
+ });
191
+
192
+ it('should reject accelerator without proper modifiers', () => {
193
+ const result = shortcutManager.updateShortcutConfig('showApp', 'F1+E');
194
+
195
+ expect(result.success).toBe(false);
196
+ expect(result.errorType).toBe('NO_MODIFIER');
197
+ });
198
+
199
+ it('should detect conflicts with existing shortcuts', () => {
200
+ // First set a shortcut
201
+ shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
202
+
203
+ // Try to set the same accelerator for another shortcut
204
+ const result = shortcutManager.updateShortcutConfig('openSettings', 'Alt+E');
205
+
206
+ expect(result.success).toBe(false);
207
+ expect(result.errorType).toBe('CONFLICT');
208
+ });
209
+
210
+ it('should detect system occupied shortcuts', () => {
211
+ vi.mocked(globalShortcut.register).mockReturnValue(false);
212
+
213
+ const result = shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
214
+
215
+ expect(result.success).toBe(false);
216
+ expect(result.errorType).toBe('SYSTEM_OCCUPIED');
217
+ });
218
+
219
+ it('should handle registration test cleanup', () => {
220
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
221
+
222
+ shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
223
+
224
+ // Should unregister the test registration
225
+ expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+Alt+T');
226
+ });
227
+
228
+ it('should handle conversion from react-hotkey format', () => {
229
+ const result = shortcutManager.updateShortcutConfig('showApp', 'mod+shift+e');
230
+
231
+ expect(result.success).toBe(true);
232
+ const config = shortcutManager.getShortcutsConfig();
233
+ expect(config.showApp).toBe('CommandOrControl+shift+E');
234
+ });
235
+
236
+ it('should handle errors gracefully', () => {
237
+ // Mock globalShortcut.register to throw an error during testing
238
+ vi.mocked(globalShortcut.register).mockImplementation(() => {
239
+ throw new Error('Register error');
240
+ });
241
+
242
+ const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
243
+
244
+ expect(result.success).toBe(false);
245
+ expect(result.errorType).toBe('UNKNOWN');
246
+ });
247
+ });
248
+
249
+ describe('registerShortcut', () => {
250
+ it('should register new shortcut successfully', () => {
251
+ const callback = vi.fn();
252
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
253
+
254
+ const result = shortcutManager.registerShortcut('Ctrl+T', callback);
255
+
256
+ expect(result).toBe(true);
257
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback);
258
+ expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(true);
259
+ });
260
+
261
+ it('should unregister existing shortcut before registering new one', () => {
262
+ const callback1 = vi.fn();
263
+ const callback2 = vi.fn();
264
+
265
+ // First registration
266
+ shortcutManager['shortcuts'].set('Ctrl+T', callback1);
267
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
268
+
269
+ shortcutManager.registerShortcut('Ctrl+T', callback2);
270
+
271
+ expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
272
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback2);
273
+ });
274
+
275
+ it('should handle registration failure', () => {
276
+ const callback = vi.fn();
277
+ vi.mocked(globalShortcut.register).mockReturnValue(false);
278
+
279
+ const result = shortcutManager.registerShortcut('Ctrl+T', callback);
280
+
281
+ expect(result).toBe(false);
282
+ expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
283
+ });
284
+
285
+ it('should handle registration errors', () => {
286
+ const callback = vi.fn();
287
+ vi.mocked(globalShortcut.register).mockImplementation(() => {
288
+ throw new Error('Registration error');
289
+ });
290
+
291
+ const result = shortcutManager.registerShortcut('Ctrl+T', callback);
292
+
293
+ expect(result).toBe(false);
294
+ });
295
+ });
296
+
297
+ describe('unregisterShortcut', () => {
298
+ it('should unregister shortcut successfully', () => {
299
+ const callback = vi.fn();
300
+ shortcutManager['shortcuts'].set('Ctrl+T', callback);
301
+
302
+ shortcutManager.unregisterShortcut('Ctrl+T');
303
+
304
+ expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
305
+ expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
306
+ });
307
+
308
+ it('should handle unregistration errors', () => {
309
+ vi.mocked(globalShortcut.unregister).mockImplementation(() => {
310
+ throw new Error('Unregister error');
311
+ });
312
+
313
+ // Should not throw
314
+ expect(() => shortcutManager.unregisterShortcut('Ctrl+T')).not.toThrow();
315
+ });
316
+ });
317
+
318
+ describe('isRegistered', () => {
319
+ it('should check if shortcut is registered', () => {
320
+ vi.mocked(globalShortcut.isRegistered).mockReturnValue(true);
321
+
322
+ const result = shortcutManager.isRegistered('Ctrl+T');
323
+
324
+ expect(result).toBe(true);
325
+ expect(globalShortcut.isRegistered).toHaveBeenCalledWith('Ctrl+T');
326
+ });
327
+ });
328
+
329
+ describe('unregisterAll', () => {
330
+ it('should unregister all shortcuts', () => {
331
+ shortcutManager.unregisterAll();
332
+
333
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
334
+ });
335
+ });
336
+
337
+ describe('loadShortcutsConfig', () => {
338
+ it('should use defaults when no config exists', () => {
339
+ mockStoreManager.get.mockReturnValue(null);
340
+
341
+ shortcutManager['loadShortcutsConfig']();
342
+
343
+ expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
344
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
345
+ });
346
+
347
+ it('should use defaults when config is empty', () => {
348
+ mockStoreManager.get.mockReturnValue({});
349
+
350
+ shortcutManager['loadShortcutsConfig']();
351
+
352
+ expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
353
+ });
354
+
355
+ it('should filter invalid keys from stored config', () => {
356
+ const storedConfig = {
357
+ showApp: 'Alt+E',
358
+ openSettings: 'Ctrl+P',
359
+ invalidKey1: 'Ctrl+I',
360
+ invalidKey2: 'Ctrl+J',
361
+ };
362
+ mockStoreManager.get.mockReturnValue(storedConfig);
363
+
364
+ shortcutManager['loadShortcutsConfig']();
365
+
366
+ const config = shortcutManager['shortcutsConfig'];
367
+ expect(config.showApp).toBe('Alt+E');
368
+ expect(config.openSettings).toBe('Ctrl+P');
369
+ expect(config.invalidKey1).toBeUndefined();
370
+ expect(config.invalidKey2).toBeUndefined();
371
+
372
+ // Should save filtered config
373
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
374
+ });
375
+
376
+ it('should add missing default shortcuts', () => {
377
+ const incompleteConfig = {
378
+ showApp: 'Alt+E',
379
+ // Missing openSettings
380
+ };
381
+ mockStoreManager.get.mockReturnValue(incompleteConfig);
382
+
383
+ shortcutManager['loadShortcutsConfig']();
384
+
385
+ const config = shortcutManager['shortcutsConfig'];
386
+ expect(config.showApp).toBe('Alt+E');
387
+ expect(config.openSettings).toBe('CommandOrControl+,'); // Default value
388
+ });
389
+
390
+ it('should not save config if no invalid keys were found', () => {
391
+ const validConfig = {
392
+ showApp: 'Alt+E',
393
+ openSettings: 'Ctrl+P',
394
+ };
395
+ mockStoreManager.get.mockReturnValue(validConfig);
396
+
397
+ shortcutManager['loadShortcutsConfig']();
398
+
399
+ // Should not call set since no changes were made
400
+ expect(mockStoreManager.set).not.toHaveBeenCalled();
401
+ });
402
+
403
+ it('should handle store errors gracefully', () => {
404
+ mockStoreManager.get.mockImplementation(() => {
405
+ throw new Error('Store error');
406
+ });
407
+
408
+ shortcutManager['loadShortcutsConfig']();
409
+
410
+ expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
411
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
412
+ });
413
+ });
414
+
415
+ describe('saveShortcutsConfig', () => {
416
+ it('should save shortcuts config to store', () => {
417
+ shortcutManager['shortcutsConfig'] = { showApp: 'Alt+E', openSettings: 'Ctrl+P' };
418
+
419
+ shortcutManager['saveShortcutsConfig']();
420
+
421
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', {
422
+ showApp: 'Alt+E',
423
+ openSettings: 'Ctrl+P',
424
+ });
425
+ });
426
+
427
+ it('should handle save errors gracefully', () => {
428
+ mockStoreManager.set.mockImplementation(() => {
429
+ throw new Error('Save error');
430
+ });
431
+
432
+ // Should not throw
433
+ expect(() => shortcutManager['saveShortcutsConfig']()).not.toThrow();
434
+ });
435
+ });
436
+
437
+ describe('registerConfiguredShortcuts', () => {
438
+ beforeEach(() => {
439
+ shortcutManager['shortcutsConfig'] = {
440
+ showApp: 'Alt+E',
441
+ openSettings: 'Ctrl+P',
442
+ };
443
+ });
444
+
445
+ it('should register all configured shortcuts', () => {
446
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
447
+
448
+ shortcutManager['registerConfiguredShortcuts']();
449
+
450
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
451
+ expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
452
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
453
+ });
454
+
455
+ it('should skip shortcuts not in DEFAULT_SHORTCUTS_CONFIG', () => {
456
+ shortcutManager['shortcutsConfig'] = {
457
+ showApp: 'Alt+E',
458
+ invalidKey: 'Ctrl+I',
459
+ };
460
+
461
+ shortcutManager['registerConfiguredShortcuts']();
462
+
463
+ expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
464
+ expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+I', expect.any(Function));
465
+ });
466
+
467
+ it('should skip shortcuts with empty accelerator', () => {
468
+ shortcutManager['shortcutsConfig'] = {
469
+ showApp: '',
470
+ openSettings: 'Ctrl+P',
471
+ };
472
+
473
+ shortcutManager['registerConfiguredShortcuts']();
474
+
475
+ expect(globalShortcut.register).not.toHaveBeenCalledWith('', expect.any(Function));
476
+ expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
477
+ });
478
+
479
+ it('should skip shortcuts without corresponding methods', () => {
480
+ // Remove method from map
481
+ mockShortcutMethodMap.delete('openSettings');
482
+ shortcutManager = new ShortcutManager(mockApp);
483
+ shortcutManager['shortcutsConfig'] = {
484
+ showApp: 'Alt+E',
485
+ openSettings: 'Ctrl+P',
486
+ };
487
+
488
+ shortcutManager['registerConfiguredShortcuts']();
489
+
490
+ expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
491
+ expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
492
+ });
493
+ });
494
+
495
+ describe('integration tests', () => {
496
+ it('should complete full initialization flow', () => {
497
+ const storedConfig = {
498
+ showApp: 'Alt+E',
499
+ openSettings: 'Ctrl+Shift+P',
500
+ invalidKey: 'Ctrl+I',
501
+ };
502
+ mockStoreManager.get.mockReturnValue(storedConfig);
503
+ vi.mocked(globalShortcut.register).mockReturnValue(true);
504
+
505
+ shortcutManager.initialize();
506
+
507
+ // Should filter config and register valid shortcuts
508
+ const config = shortcutManager.getShortcutsConfig();
509
+ expect(config.showApp).toBe('Alt+E');
510
+ expect(config.openSettings).toBe('Ctrl+Shift+P');
511
+ expect(config.invalidKey).toBeUndefined();
512
+
513
+ expect(globalShortcut.register).toHaveBeenCalledTimes(2);
514
+ expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
515
+ });
516
+
517
+ it('should handle complete update workflow', () => {
518
+ mockStoreManager.get.mockReturnValue({});
519
+ shortcutManager.initialize();
520
+
521
+ // Update a shortcut
522
+ const result = shortcutManager.updateShortcutConfig('showApp', 'mod+alt+e');
523
+
524
+ expect(result.success).toBe(true);
525
+
526
+ // Should convert format and register
527
+ const config = shortcutManager.getShortcutsConfig();
528
+ expect(config.showApp).toBe('CommandOrControl+alt+E');
529
+
530
+ // Should have saved and re-registered shortcuts
531
+ expect(mockStoreManager.set).toHaveBeenCalled();
532
+ expect(globalShortcut.unregisterAll).toHaveBeenCalled();
533
+ expect(globalShortcut.register).toHaveBeenCalledWith(
534
+ 'CommandOrControl+alt+E',
535
+ expect.any(Function),
536
+ );
537
+ });
538
+ });
539
+ });
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix update hotkey invalid when input mod in desktop."
6
+ ]
7
+ },
8
+ "date": "2025-07-26",
9
+ "version": "1.104.2"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider."
15
+ ]
16
+ },
17
+ "date": "2025-07-25",
18
+ "version": "1.104.1"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.104.0",
3
+ "version": "1.104.2",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -20,6 +20,7 @@ import { AsyncTaskErrorType } from '@/types/asyncTask';
20
20
  import { GenerationBatch } from '@/types/generation';
21
21
 
22
22
  import { GenerationItem } from './GenerationItem';
23
+ import { DEFAULT_MAX_ITEM_WIDTH } from './GenerationItem/utils';
23
24
  import { ReferenceImages } from './ReferenceImages';
24
25
 
25
26
  const useStyles = createStyles(({ cx, css, token }) => ({
@@ -182,7 +183,11 @@ export const GenerationBatchItem = memo<GenerationBatchItemProps>(({ batch }) =>
182
183
  {promptAndMetadata}
183
184
  </>
184
185
  )}
185
- <Grid maxItemWidth={200} ref={imageGridRef} rows={batch.generations.length || 4}>
186
+ <Grid
187
+ maxItemWidth={DEFAULT_MAX_ITEM_WIDTH}
188
+ ref={imageGridRef}
189
+ rows={batch.generations.length}
190
+ >
186
191
  {batch.generations.map((generation) => (
187
192
  <GenerationItem
188
193
  generation={generation}
@@ -9,10 +9,11 @@ import { Center } from 'react-layout-kit';
9
9
  import { ActionButtons } from './ActionButtons';
10
10
  import { useStyles } from './styles';
11
11
  import { ErrorStateProps } from './types';
12
+ import { getThumbnailMaxWidth } from './utils';
12
13
 
13
14
  // 错误状态组件
14
15
  export const ErrorState = memo<ErrorStateProps>(
15
- ({ generation, aspectRatio, onDelete, onCopyError }) => {
16
+ ({ generation, generationBatch, aspectRatio, onDelete, onCopyError }) => {
16
17
  const { styles, theme } = useStyles();
17
18
  const { t } = useTranslation('image');
18
19
 
@@ -32,7 +33,7 @@ export const ErrorState = memo<ErrorStateProps>(
32
33
  style={{
33
34
  aspectRatio,
34
35
  cursor: 'pointer',
35
- maxWidth: generation.asset?.width ? generation.asset.width / 2 : 'unset',
36
+ maxWidth: getThumbnailMaxWidth(generation, generationBatch),
36
37
  }}
37
38
  variant={'filled'}
38
39
  >
@@ -12,33 +12,36 @@ import { ActionButtons } from './ActionButtons';
12
12
  import { ElapsedTime } from './ElapsedTime';
13
13
  import { useStyles } from './styles';
14
14
  import { LoadingStateProps } from './types';
15
+ import { getThumbnailMaxWidth } from './utils';
15
16
 
16
17
  // 加载状态组件
17
- export const LoadingState = memo<LoadingStateProps>(({ generation, aspectRatio, onDelete }) => {
18
- const { styles } = useStyles();
18
+ export const LoadingState = memo<LoadingStateProps>(
19
+ ({ generation, generationBatch, aspectRatio, onDelete }) => {
20
+ const { styles } = useStyles();
19
21
 
20
- const isGenerating =
21
- generation.task.status === AsyncTaskStatus.Processing ||
22
- generation.task.status === AsyncTaskStatus.Pending;
22
+ const isGenerating =
23
+ generation.task.status === AsyncTaskStatus.Processing ||
24
+ generation.task.status === AsyncTaskStatus.Pending;
23
25
 
24
- return (
25
- <Block
26
- align={'center'}
27
- className={styles.placeholderContainer}
28
- justify={'center'}
29
- style={{
30
- aspectRatio,
31
- maxWidth: generation.asset?.width ? generation.asset.width / 2 : 'unset',
32
- }}
33
- variant={'filled'}
34
- >
35
- <Center gap={8}>
36
- <Spin indicator={<LoadingOutlined spin />} />
37
- <ElapsedTime generationId={generation.id} isActive={isGenerating} />
38
- </Center>
39
- <ActionButtons onDelete={onDelete} />
40
- </Block>
41
- );
42
- });
26
+ return (
27
+ <Block
28
+ align={'center'}
29
+ className={styles.placeholderContainer}
30
+ justify={'center'}
31
+ style={{
32
+ aspectRatio,
33
+ maxWidth: getThumbnailMaxWidth(generation, generationBatch),
34
+ }}
35
+ variant={'filled'}
36
+ >
37
+ <Center gap={8}>
38
+ <Spin indicator={<LoadingOutlined spin />} />
39
+ <ElapsedTime generationId={generation.id} isActive={isGenerating} />
40
+ </Center>
41
+ <ActionButtons onDelete={onDelete} />
42
+ </Block>
43
+ );
44
+ },
45
+ );
43
46
 
44
47
  LoadingState.displayName = 'LoadingState';