@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
@@ -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.
|
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
|
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}
|
package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx
CHANGED
@@ -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
|
36
|
+
maxWidth: getThumbnailMaxWidth(generation, generationBatch),
|
36
37
|
}}
|
37
38
|
variant={'filled'}
|
38
39
|
>
|
package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx
CHANGED
@@ -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>(
|
18
|
-
|
18
|
+
export const LoadingState = memo<LoadingStateProps>(
|
19
|
+
({ generation, generationBatch, aspectRatio, onDelete }) => {
|
20
|
+
const { styles } = useStyles();
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
22
|
+
const isGenerating =
|
23
|
+
generation.task.status === AsyncTaskStatus.Processing ||
|
24
|
+
generation.task.status === AsyncTaskStatus.Pending;
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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';
|