mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-18 21:36:12 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa6d64df2 | |||
| 403aebd52e | |||
| 356cf0c392 | |||
| be98d56ef4 | |||
| 3fae1b2638 | |||
| ee4cc6c2e0 | |||
| b8c0e2d639 | |||
| 74d20bdbe8 | |||
| 7bab44e74c | |||
| fdaa72564c | |||
| 7d85151cb6 | |||
| 23ef2eea59 | |||
| 74ab822140 | |||
| d726ff108d | |||
| dd7b661140 | |||
| 49023419cf | |||
| 2eccbc79eb | |||
| 3527cb65f1 | |||
| 1731c841d8 | |||
| 404ac21229 | |||
| 2eaa2dbea0 | |||
| 50c0ed168d | |||
| 780e231afa | |||
| 07f3e6a4c4 | |||
| 3c35edced5 | |||
| 15770f188f | |||
| 9d7c6014fd | |||
| d1e4a54b01 | |||
| 1bc8815fb4 | |||
| 284826bed0 | |||
| b50f1212cb | |||
| 946517a52e |
@@ -26,6 +26,8 @@ Gather the modified code and context. Please strictly follow the process below:
|
||||
|
||||
### Code Style
|
||||
|
||||
read [typescript.mdc](mdc:.cursor/rules/typescript.mdc) to learn the project's code style.
|
||||
|
||||
- Ensure JSDoc comments accurately reflect the implementation; update them when needed.
|
||||
- Look for opportunities to simplify or modernize code with the latest JavaScript/TypeScript features.
|
||||
- Prefer `async`/`await` over callbacks or chained `.then` promises.
|
||||
|
||||
@@ -16,4 +16,6 @@ TypeScript Code Style Guide:
|
||||
- Always refactor repeated logic into a reusable function
|
||||
- Don't remove meaningful code comments, be sure to keep original comments when providing applied code
|
||||
- Update the code comments when needed after you modify the related code
|
||||
- Please respect my prettier preferences when you provide code
|
||||
- Please respect my prettier preferences when you provide code
|
||||
- Prefer object destructuring when accessing and using properties
|
||||
- Prefer async version api than sync version, eg: use readFile from 'fs/promises' instead of 'fs'
|
||||
|
||||
+5
-6
@@ -41,10 +41,6 @@ test-output
|
||||
# husky
|
||||
.husky/prepare-commit-msg
|
||||
|
||||
# misc
|
||||
# add other ignore file below
|
||||
CLAUDE.md
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
@@ -74,5 +70,8 @@ vertex-ai-key.json
|
||||
./packages/lobe-ui
|
||||
|
||||
|
||||
# for local prd docs
|
||||
docs/prd
|
||||
# local use ai coding files
|
||||
docs/.prd
|
||||
.claude
|
||||
.mcp.json
|
||||
CLAUDE.md
|
||||
+201
@@ -2,6 +2,207 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 1.105.1](https://github.com/lobehub/lobe-chat/compare/v1.105.0...v1.105.1)
|
||||
|
||||
<sup>Released on **2025-07-29**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Support more Text2Image from Qwen.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Support more Text2Image from Qwen, closes [#8574](https://github.com/lobehub/lobe-chat/issues/8574) ([b8c0e2d](https://github.com/lobehub/lobe-chat/commit/b8c0e2d))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.105.0](https://github.com/lobehub/lobe-chat/compare/v1.104.5...v1.105.0)
|
||||
|
||||
<sup>Released on **2025-07-28**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Implement API Key management functionality.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Implement API Key management functionality, closes [#8535](https://github.com/lobehub/lobe-chat/issues/8535) ([fdaa725](https://github.com/lobehub/lobe-chat/commit/fdaa725))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.5](https://github.com/lobehub/lobe-chat/compare/v1.104.4...v1.104.5)
|
||||
|
||||
<sup>Released on **2025-07-28**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Fix setting window layout when in desktop was disappear.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Fix setting window layout when in desktop was disappear, closes [#8585](https://github.com/lobehub/lobe-chat/issues/8585) ([74ab822](https://github.com/lobehub/lobe-chat/commit/74ab822))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.4](https://github.com/lobehub/lobe-chat/compare/v1.104.3...v1.104.4)
|
||||
|
||||
<sup>Released on **2025-07-28**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Fix setting window layout size, update i18n.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Fix setting window layout size, closes [#8483](https://github.com/lobehub/lobe-chat/issues/8483) ([4902341](https://github.com/lobehub/lobe-chat/commit/4902341))
|
||||
- **misc**: Update i18n, closes [#8579](https://github.com/lobehub/lobe-chat/issues/8579) ([2eccbc7](https://github.com/lobehub/lobe-chat/commit/2eccbc7))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.3](https://github.com/lobehub/lobe-chat/compare/v1.104.2...v1.104.3)
|
||||
|
||||
<sup>Released on **2025-07-26**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: Add Gemini 2.5 Flash-Lite GA model.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: Add Gemini 2.5 Flash-Lite GA model, closes [#8539](https://github.com/lobehub/lobe-chat/issues/8539) ([404ac21](https://github.com/lobehub/lobe-chat/commit/404ac21))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.2](https://github.com/lobehub/lobe-chat/compare/v1.104.1...v1.104.2)
|
||||
|
||||
<sup>Released on **2025-07-26**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Fix update hotkey invalid when input mod in desktop.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Fix update hotkey invalid when input mod in desktop, closes [#8572](https://github.com/lobehub/lobe-chat/issues/8572) ([07f3e6a](https://github.com/lobehub/lobe-chat/commit/07f3e6a))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.104.1](https://github.com/lobehub/lobe-chat/compare/v1.104.0...v1.104.1)
|
||||
|
||||
<sup>Released on **2025-07-25**</sup>
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **misc**: Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **misc**: Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider, closes [#8557](https://github.com/lobehub/lobe-chat/issues/8557) ([d1e4a54](https://github.com/lobehub/lobe-chat/commit/d1e4a54))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 1.104.0](https://github.com/lobehub/lobe-chat/compare/v1.103.2...v1.104.0)
|
||||
|
||||
<sup>Released on **2025-07-24**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **misc**: Support custom hotkey on desktop.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **misc**: Support custom hotkey on desktop, closes [#8559](https://github.com/lobehub/lobe-chat/issues/8559) ([b50f121](https://github.com/lobehub/lobe-chat/commit/b50f121))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
### [Version 1.103.2](https://github.com/lobehub/lobe-chat/compare/v1.103.1...v1.103.2)
|
||||
|
||||
<sup>Released on **2025-07-24**</sup>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
||||
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
||||
|
||||
export default class BrowserWindowsCtr extends ControllerModule {
|
||||
@shortcut('showMainWindow')
|
||||
@shortcut('showApp')
|
||||
async toggleMainWindow() {
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
mainWindow.toggleVisible();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ShortcutUpdateResult } from '@/core/ui/ShortcutManager';
|
||||
|
||||
import { ControllerModule, ipcClientEvent } from '.';
|
||||
|
||||
export default class ShortcutController extends ControllerModule {
|
||||
@@ -13,7 +15,13 @@ export default class ShortcutController extends ControllerModule {
|
||||
* 更新单个快捷键配置
|
||||
*/
|
||||
@ipcClientEvent('updateShortcutConfig')
|
||||
updateShortcutConfig(id: string, accelerator: string): boolean {
|
||||
updateShortcutConfig({
|
||||
id,
|
||||
accelerator,
|
||||
}: {
|
||||
accelerator: string;
|
||||
id: string;
|
||||
}): ShortcutUpdateResult {
|
||||
return this.app.shortcutManager.updateShortcutConfig(id, accelerator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,12 @@ import {
|
||||
|
||||
import { createLogger } from '@/utils/logger';
|
||||
|
||||
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
||||
import { ControllerModule, ipcClientEvent } from './index';
|
||||
|
||||
// 创建日志记录器
|
||||
const logger = createLogger('controllers:TrayMenuCtr');
|
||||
|
||||
export default class TrayMenuCtr extends ControllerModule {
|
||||
/**
|
||||
* 使用快捷键切换窗口可见性
|
||||
*/
|
||||
@shortcut('showMainWindow')
|
||||
async toggleMainWindow() {
|
||||
logger.debug('通过快捷键切换主窗口可见性');
|
||||
const mainWindow = this.app.browserManager.getMainWindow();
|
||||
|
||||
@@ -5,9 +5,9 @@ import type { App } from '@/core/App';
|
||||
import ShortcutController from '../ShortcutCtr';
|
||||
|
||||
// 模拟 App 及其依赖项
|
||||
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
|
||||
const mockGetShortcutsConfig = vi.fn().mockReturnValue({
|
||||
toggleMainWindow: 'CommandOrControl+Shift+L',
|
||||
openSettings: 'CommandOrControl+,'
|
||||
openSettings: 'CommandOrControl+,',
|
||||
});
|
||||
const mockUpdateShortcutConfig = vi.fn().mockImplementation((id, accelerator) => {
|
||||
// 简单模拟更新成功
|
||||
@@ -32,11 +32,11 @@ describe('ShortcutController', () => {
|
||||
describe('getShortcutsConfig', () => {
|
||||
it('should return shortcuts config from shortcutManager', () => {
|
||||
const result = shortcutController.getShortcutsConfig();
|
||||
|
||||
|
||||
expect(mockGetShortcutsConfig).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
toggleMainWindow: 'CommandOrControl+Shift+L',
|
||||
openSettings: 'CommandOrControl+,'
|
||||
openSettings: 'CommandOrControl+,',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -45,9 +45,9 @@ describe('ShortcutController', () => {
|
||||
it('should call shortcutManager.updateShortcutConfig with correct parameters', () => {
|
||||
const id = 'toggleMainWindow';
|
||||
const accelerator = 'CommandOrControl+Alt+L';
|
||||
|
||||
const result = shortcutController.updateShortcutConfig(id, accelerator);
|
||||
|
||||
|
||||
const result = shortcutController.updateShortcutConfig({ id, accelerator });
|
||||
|
||||
expect(mockUpdateShortcutConfig).toHaveBeenCalledWith(id, accelerator);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -55,10 +55,13 @@ describe('ShortcutController', () => {
|
||||
it('should return the result from shortcutManager.updateShortcutConfig', () => {
|
||||
// 模拟更新失败的情况
|
||||
mockUpdateShortcutConfig.mockReturnValueOnce(false);
|
||||
|
||||
const result = shortcutController.updateShortcutConfig('invalidKey', 'invalid+combo');
|
||||
|
||||
|
||||
const result = shortcutController.updateShortcutConfig({
|
||||
id: 'invalidKey',
|
||||
accelerator: 'invalid+combo',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,17 @@ import type { App } from '../App';
|
||||
// Create logger
|
||||
const logger = createLogger('core:ShortcutManager');
|
||||
|
||||
export interface ShortcutUpdateResult {
|
||||
errorType?:
|
||||
| 'INVALID_ID'
|
||||
| 'INVALID_FORMAT'
|
||||
| 'NO_MODIFIER'
|
||||
| 'CONFLICT'
|
||||
| 'SYSTEM_OCCUPIED'
|
||||
| 'UNKNOWN';
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class ShortcutManager {
|
||||
private app: App;
|
||||
private shortcuts: Map<string, () => void> = new Map();
|
||||
@@ -22,6 +33,28 @@ export class ShortcutManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert react-hotkey format to Electron accelerator format
|
||||
* @param accelerator The accelerator string from frontend
|
||||
* @returns Converted accelerator string for Electron
|
||||
*/
|
||||
private convertAcceleratorFormat(accelerator: string): string {
|
||||
return accelerator
|
||||
.split('+')
|
||||
.map((key) => {
|
||||
const trimmedKey = key.trim().toLowerCase();
|
||||
|
||||
// Convert react-hotkey 'mod' to Electron 'CommandOrControl'
|
||||
if (trimmedKey === 'mod') {
|
||||
return 'CommandOrControl';
|
||||
}
|
||||
|
||||
// Keep other keys as is, but preserve proper casing
|
||||
return key.trim().length === 1 ? key.trim().toUpperCase() : key.trim();
|
||||
})
|
||||
.join('+');
|
||||
}
|
||||
|
||||
initialize() {
|
||||
logger.info('Initializing global shortcuts');
|
||||
// Load shortcuts configuration from storage
|
||||
@@ -40,18 +73,79 @@ export class ShortcutManager {
|
||||
/**
|
||||
* Update a single shortcut configuration
|
||||
*/
|
||||
updateShortcutConfig(id: string, accelerator: string): boolean {
|
||||
updateShortcutConfig(id: string, accelerator: string): ShortcutUpdateResult {
|
||||
try {
|
||||
logger.debug(`Updating shortcut ${id} to ${accelerator}`);
|
||||
// Update configuration
|
||||
this.shortcutsConfig[id] = accelerator;
|
||||
|
||||
// 1. 检查 ID 是否有效
|
||||
if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
|
||||
logger.error(`Invalid shortcut ID: ${id}`);
|
||||
return { errorType: 'INVALID_ID', success: false };
|
||||
}
|
||||
|
||||
// 2. 基本格式校验
|
||||
if (!accelerator || typeof accelerator !== 'string' || accelerator.trim() === '') {
|
||||
logger.error(`Invalid accelerator format: ${accelerator}`);
|
||||
return { errorType: 'INVALID_FORMAT', success: false };
|
||||
}
|
||||
|
||||
// 转换前端格式到 Electron 格式
|
||||
const convertedAccelerator = this.convertAcceleratorFormat(accelerator.trim());
|
||||
const cleanAccelerator = convertedAccelerator.toLowerCase();
|
||||
|
||||
logger.debug(`Converted accelerator from ${accelerator} to ${convertedAccelerator}`);
|
||||
|
||||
// 3. 检查是否包含 + 号(修饰键格式)
|
||||
if (!cleanAccelerator.includes('+')) {
|
||||
logger.error(
|
||||
`Invalid accelerator format: ${cleanAccelerator}. Must contain modifier keys like 'CommandOrControl+E'`,
|
||||
);
|
||||
return { errorType: 'INVALID_FORMAT', success: false };
|
||||
}
|
||||
|
||||
// 4. 检查是否有基本的修饰键
|
||||
const hasModifier = ['CommandOrControl', 'Command', 'Ctrl', 'Alt', 'Shift'].some((modifier) =>
|
||||
cleanAccelerator.includes(modifier.toLowerCase()),
|
||||
);
|
||||
|
||||
if (!hasModifier) {
|
||||
logger.error(`Invalid accelerator format: ${cleanAccelerator}. Must contain modifier keys`);
|
||||
return { errorType: 'NO_MODIFIER', success: false };
|
||||
}
|
||||
|
||||
// 5. 检查冲突
|
||||
for (const [existingId, existingAccelerator] of Object.entries(this.shortcutsConfig)) {
|
||||
if (
|
||||
existingId !== id &&
|
||||
typeof existingAccelerator === 'string' &&
|
||||
existingAccelerator.toLowerCase() === cleanAccelerator
|
||||
) {
|
||||
logger.error(`Shortcut conflict: ${cleanAccelerator} already used by ${existingId}`);
|
||||
return { errorType: 'CONFLICT', success: false };
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 尝试注册测试(检查是否被系统占用)
|
||||
const testSuccess = globalShortcut.register(convertedAccelerator, () => {});
|
||||
if (!testSuccess) {
|
||||
logger.error(
|
||||
`Shortcut ${convertedAccelerator} is already registered by system or other app`,
|
||||
);
|
||||
return { errorType: 'SYSTEM_OCCUPIED', success: false };
|
||||
} else {
|
||||
// 测试成功,立即取消注册
|
||||
globalShortcut.unregister(convertedAccelerator);
|
||||
}
|
||||
|
||||
// 7. 更新配置
|
||||
this.shortcutsConfig[id] = convertedAccelerator;
|
||||
|
||||
this.saveShortcutsConfig();
|
||||
this.registerConfiguredShortcuts();
|
||||
return true;
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error(`Error updating shortcut ${id}:`, error);
|
||||
return false;
|
||||
return { errorType: 'UNKNOWN', success: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +224,34 @@ export class ShortcutManager {
|
||||
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
|
||||
this.saveShortcutsConfig();
|
||||
} else {
|
||||
this.shortcutsConfig = config;
|
||||
// Filter out invalid shortcuts that are not in DEFAULT_SHORTCUTS_CONFIG
|
||||
const filteredConfig: Record<string, string> = {};
|
||||
let hasInvalidKeys = false;
|
||||
|
||||
Object.entries(config).forEach(([id, accelerator]) => {
|
||||
if (DEFAULT_SHORTCUTS_CONFIG[id]) {
|
||||
filteredConfig[id] = accelerator;
|
||||
} else {
|
||||
hasInvalidKeys = true;
|
||||
logger.debug(`Filtering out invalid shortcut ID: ${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure all default shortcuts are present
|
||||
Object.entries(DEFAULT_SHORTCUTS_CONFIG).forEach(([id, defaultAccelerator]) => {
|
||||
if (!(id in filteredConfig)) {
|
||||
filteredConfig[id] = defaultAccelerator;
|
||||
logger.debug(`Adding missing default shortcut: ${id} = ${defaultAccelerator}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.shortcutsConfig = filteredConfig;
|
||||
|
||||
// Save the filtered configuration back to storage if we removed invalid keys
|
||||
if (hasInvalidKeys) {
|
||||
logger.debug('Saving filtered shortcuts config to remove invalid keys');
|
||||
this.saveShortcutsConfig();
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
import { globalShortcut } from 'electron';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
|
||||
|
||||
import type { App } from '../../App';
|
||||
import { ShortcutManager } from '../ShortcutManager';
|
||||
|
||||
// Mock electron
|
||||
vi.mock('electron', () => ({
|
||||
globalShortcut: {
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
unregisterAll: vi.fn(),
|
||||
isRegistered: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock Logger
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
createLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock DEFAULT_SHORTCUTS_CONFIG
|
||||
vi.mock('@/shortcuts', () => ({
|
||||
DEFAULT_SHORTCUTS_CONFIG: {
|
||||
showApp: 'Control+E',
|
||||
openSettings: 'CommandOrControl+,',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ShortcutManager', () => {
|
||||
let shortcutManager: ShortcutManager;
|
||||
let mockApp: App;
|
||||
let mockStoreManager: any;
|
||||
let mockShortcutMethodMap: Map<string, () => void>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset all mocks to their default behavior
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(true);
|
||||
vi.mocked(globalShortcut.unregister).mockReturnValue(undefined);
|
||||
vi.mocked(globalShortcut.unregisterAll).mockReturnValue(undefined);
|
||||
vi.mocked(globalShortcut.isRegistered).mockReturnValue(false);
|
||||
|
||||
// Mock store manager
|
||||
mockStoreManager = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
};
|
||||
|
||||
// Mock shortcut method map
|
||||
mockShortcutMethodMap = new Map();
|
||||
const showAppMethod = vi.fn();
|
||||
const openSettingsMethod = vi.fn();
|
||||
mockShortcutMethodMap.set('showApp', showAppMethod);
|
||||
mockShortcutMethodMap.set('openSettings', openSettingsMethod);
|
||||
|
||||
// Mock App
|
||||
mockApp = {
|
||||
storeManager: mockStoreManager,
|
||||
shortcutMethodMap: mockShortcutMethodMap,
|
||||
} as unknown as App;
|
||||
|
||||
shortcutManager = new ShortcutManager(mockApp);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize shortcut manager with app', () => {
|
||||
expect(shortcutManager).toBeDefined();
|
||||
expect(shortcutManager['app']).toBe(mockApp);
|
||||
});
|
||||
|
||||
it('should populate shortcuts map from app shortcut method map', () => {
|
||||
expect(shortcutManager['shortcuts'].size).toBe(2);
|
||||
expect(shortcutManager['shortcuts'].has('showApp')).toBe(true);
|
||||
expect(shortcutManager['shortcuts'].has('openSettings')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertAcceleratorFormat', () => {
|
||||
it('should convert mod to CommandOrControl', () => {
|
||||
const result = shortcutManager['convertAcceleratorFormat']('mod+e');
|
||||
expect(result).toBe('CommandOrControl+E');
|
||||
});
|
||||
|
||||
it('should preserve other keys as is except single characters', () => {
|
||||
const result = shortcutManager['convertAcceleratorFormat']('ctrl+alt+f12');
|
||||
expect(result).toBe('ctrl+alt+f12');
|
||||
});
|
||||
|
||||
it('should handle single character keys with uppercase', () => {
|
||||
const result = shortcutManager['convertAcceleratorFormat']('ctrl + a');
|
||||
expect(result).toBe('ctrl+A');
|
||||
});
|
||||
|
||||
it('should handle complex combinations', () => {
|
||||
const result = shortcutManager['convertAcceleratorFormat']('mod+shift+delete');
|
||||
expect(result).toBe('CommandOrControl+shift+delete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should load shortcuts config and register shortcuts', () => {
|
||||
// Mock store to return empty config (will use defaults)
|
||||
mockStoreManager.get.mockReturnValue({});
|
||||
|
||||
shortcutManager.initialize();
|
||||
|
||||
expect(mockStoreManager.get).toHaveBeenCalledWith('shortcuts');
|
||||
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Control+E', expect.any(Function));
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith(
|
||||
'CommandOrControl+,',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle stored config with filtering', () => {
|
||||
const storedConfig = {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+Shift+P',
|
||||
invalidKey: 'Ctrl+I', // Should be filtered out
|
||||
};
|
||||
mockStoreManager.get.mockReturnValue(storedConfig);
|
||||
|
||||
shortcutManager.initialize();
|
||||
|
||||
const config = shortcutManager.getShortcutsConfig();
|
||||
expect(config.showApp).toBe('Alt+E');
|
||||
expect(config.openSettings).toBe('Ctrl+Shift+P');
|
||||
expect(config.invalidKey).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShortcutsConfig', () => {
|
||||
it('should return current shortcuts configuration', () => {
|
||||
mockStoreManager.get.mockReturnValue({});
|
||||
shortcutManager.initialize();
|
||||
|
||||
const config = shortcutManager.getShortcutsConfig();
|
||||
expect(config).toEqual(DEFAULT_SHORTCUTS_CONFIG);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateShortcutConfig', () => {
|
||||
beforeEach(() => {
|
||||
mockStoreManager.get.mockReturnValue({});
|
||||
shortcutManager.initialize();
|
||||
});
|
||||
|
||||
it('should successfully update valid shortcut', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.errorType).toBeUndefined();
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith(
|
||||
'shortcuts',
|
||||
expect.objectContaining({
|
||||
showApp: 'Alt+E',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid shortcut ID', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('invalidId', 'Alt+E');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('INVALID_ID');
|
||||
});
|
||||
|
||||
it('should reject empty accelerator', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', '');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('INVALID_FORMAT');
|
||||
});
|
||||
|
||||
it('should reject accelerator without modifier keys', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'E');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('INVALID_FORMAT');
|
||||
});
|
||||
|
||||
it('should reject accelerator without proper modifiers', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'F1+E');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('NO_MODIFIER');
|
||||
});
|
||||
|
||||
it('should detect conflicts with existing shortcuts', () => {
|
||||
// First set a shortcut
|
||||
shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
|
||||
|
||||
// Try to set the same accelerator for another shortcut
|
||||
const result = shortcutManager.updateShortcutConfig('openSettings', 'Alt+E');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('CONFLICT');
|
||||
});
|
||||
|
||||
it('should detect system occupied shortcuts', () => {
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(false);
|
||||
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('SYSTEM_OCCUPIED');
|
||||
});
|
||||
|
||||
it('should handle registration test cleanup', () => {
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(true);
|
||||
|
||||
shortcutManager.updateShortcutConfig('showApp', 'Ctrl+Alt+T');
|
||||
|
||||
// Should unregister the test registration
|
||||
expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+Alt+T');
|
||||
});
|
||||
|
||||
it('should handle conversion from react-hotkey format', () => {
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'mod+shift+e');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const config = shortcutManager.getShortcutsConfig();
|
||||
expect(config.showApp).toBe('CommandOrControl+shift+E');
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
// Mock globalShortcut.register to throw an error during testing
|
||||
vi.mocked(globalShortcut.register).mockImplementation(() => {
|
||||
throw new Error('Register error');
|
||||
});
|
||||
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'Alt+E');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorType).toBe('UNKNOWN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerShortcut', () => {
|
||||
it('should register new shortcut successfully', () => {
|
||||
const callback = vi.fn();
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(true);
|
||||
|
||||
const result = shortcutManager.registerShortcut('Ctrl+T', callback);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback);
|
||||
expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(true);
|
||||
});
|
||||
|
||||
it('should unregister existing shortcut before registering new one', () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
|
||||
// First registration
|
||||
shortcutManager['shortcuts'].set('Ctrl+T', callback1);
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(true);
|
||||
|
||||
shortcutManager.registerShortcut('Ctrl+T', callback2);
|
||||
|
||||
expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+T', callback2);
|
||||
});
|
||||
|
||||
it('should handle registration failure', () => {
|
||||
const callback = vi.fn();
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(false);
|
||||
|
||||
const result = shortcutManager.registerShortcut('Ctrl+T', callback);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle registration errors', () => {
|
||||
const callback = vi.fn();
|
||||
vi.mocked(globalShortcut.register).mockImplementation(() => {
|
||||
throw new Error('Registration error');
|
||||
});
|
||||
|
||||
const result = shortcutManager.registerShortcut('Ctrl+T', callback);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregisterShortcut', () => {
|
||||
it('should unregister shortcut successfully', () => {
|
||||
const callback = vi.fn();
|
||||
shortcutManager['shortcuts'].set('Ctrl+T', callback);
|
||||
|
||||
shortcutManager.unregisterShortcut('Ctrl+T');
|
||||
|
||||
expect(globalShortcut.unregister).toHaveBeenCalledWith('Ctrl+T');
|
||||
expect(shortcutManager['shortcuts'].has('Ctrl+T')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle unregistration errors', () => {
|
||||
vi.mocked(globalShortcut.unregister).mockImplementation(() => {
|
||||
throw new Error('Unregister error');
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
expect(() => shortcutManager.unregisterShortcut('Ctrl+T')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRegistered', () => {
|
||||
it('should check if shortcut is registered', () => {
|
||||
vi.mocked(globalShortcut.isRegistered).mockReturnValue(true);
|
||||
|
||||
const result = shortcutManager.isRegistered('Ctrl+T');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(globalShortcut.isRegistered).toHaveBeenCalledWith('Ctrl+T');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unregisterAll', () => {
|
||||
it('should unregister all shortcuts', () => {
|
||||
shortcutManager.unregisterAll();
|
||||
|
||||
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadShortcutsConfig', () => {
|
||||
it('should use defaults when no config exists', () => {
|
||||
mockStoreManager.get.mockReturnValue(null);
|
||||
|
||||
shortcutManager['loadShortcutsConfig']();
|
||||
|
||||
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
|
||||
});
|
||||
|
||||
it('should use defaults when config is empty', () => {
|
||||
mockStoreManager.get.mockReturnValue({});
|
||||
|
||||
shortcutManager['loadShortcutsConfig']();
|
||||
|
||||
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
|
||||
});
|
||||
|
||||
it('should filter invalid keys from stored config', () => {
|
||||
const storedConfig = {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+P',
|
||||
invalidKey1: 'Ctrl+I',
|
||||
invalidKey2: 'Ctrl+J',
|
||||
};
|
||||
mockStoreManager.get.mockReturnValue(storedConfig);
|
||||
|
||||
shortcutManager['loadShortcutsConfig']();
|
||||
|
||||
const config = shortcutManager['shortcutsConfig'];
|
||||
expect(config.showApp).toBe('Alt+E');
|
||||
expect(config.openSettings).toBe('Ctrl+P');
|
||||
expect(config.invalidKey1).toBeUndefined();
|
||||
expect(config.invalidKey2).toBeUndefined();
|
||||
|
||||
// Should save filtered config
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
|
||||
});
|
||||
|
||||
it('should add missing default shortcuts', () => {
|
||||
const incompleteConfig = {
|
||||
showApp: 'Alt+E',
|
||||
// Missing openSettings
|
||||
};
|
||||
mockStoreManager.get.mockReturnValue(incompleteConfig);
|
||||
|
||||
shortcutManager['loadShortcutsConfig']();
|
||||
|
||||
const config = shortcutManager['shortcutsConfig'];
|
||||
expect(config.showApp).toBe('Alt+E');
|
||||
expect(config.openSettings).toBe('CommandOrControl+,'); // Default value
|
||||
});
|
||||
|
||||
it('should not save config if no invalid keys were found', () => {
|
||||
const validConfig = {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+P',
|
||||
};
|
||||
mockStoreManager.get.mockReturnValue(validConfig);
|
||||
|
||||
shortcutManager['loadShortcutsConfig']();
|
||||
|
||||
// Should not call set since no changes were made
|
||||
expect(mockStoreManager.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle store errors gracefully', () => {
|
||||
mockStoreManager.get.mockImplementation(() => {
|
||||
throw new Error('Store error');
|
||||
});
|
||||
|
||||
shortcutManager['loadShortcutsConfig']();
|
||||
|
||||
expect(shortcutManager['shortcutsConfig']).toEqual(DEFAULT_SHORTCUTS_CONFIG);
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', DEFAULT_SHORTCUTS_CONFIG);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveShortcutsConfig', () => {
|
||||
it('should save shortcuts config to store', () => {
|
||||
shortcutManager['shortcutsConfig'] = { showApp: 'Alt+E', openSettings: 'Ctrl+P' };
|
||||
|
||||
shortcutManager['saveShortcutsConfig']();
|
||||
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+P',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle save errors gracefully', () => {
|
||||
mockStoreManager.set.mockImplementation(() => {
|
||||
throw new Error('Save error');
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
expect(() => shortcutManager['saveShortcutsConfig']()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerConfiguredShortcuts', () => {
|
||||
beforeEach(() => {
|
||||
shortcutManager['shortcutsConfig'] = {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+P',
|
||||
};
|
||||
});
|
||||
|
||||
it('should register all configured shortcuts', () => {
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(true);
|
||||
|
||||
shortcutManager['registerConfiguredShortcuts']();
|
||||
|
||||
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should skip shortcuts not in DEFAULT_SHORTCUTS_CONFIG', () => {
|
||||
shortcutManager['shortcutsConfig'] = {
|
||||
showApp: 'Alt+E',
|
||||
invalidKey: 'Ctrl+I',
|
||||
};
|
||||
|
||||
shortcutManager['registerConfiguredShortcuts']();
|
||||
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
|
||||
expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+I', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should skip shortcuts with empty accelerator', () => {
|
||||
shortcutManager['shortcutsConfig'] = {
|
||||
showApp: '',
|
||||
openSettings: 'Ctrl+P',
|
||||
};
|
||||
|
||||
shortcutManager['registerConfiguredShortcuts']();
|
||||
|
||||
expect(globalShortcut.register).not.toHaveBeenCalledWith('', expect.any(Function));
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should skip shortcuts without corresponding methods', () => {
|
||||
// Remove method from map
|
||||
mockShortcutMethodMap.delete('openSettings');
|
||||
shortcutManager = new ShortcutManager(mockApp);
|
||||
shortcutManager['shortcutsConfig'] = {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+P',
|
||||
};
|
||||
|
||||
shortcutManager['registerConfiguredShortcuts']();
|
||||
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith('Alt+E', expect.any(Function));
|
||||
expect(globalShortcut.register).not.toHaveBeenCalledWith('Ctrl+P', expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should complete full initialization flow', () => {
|
||||
const storedConfig = {
|
||||
showApp: 'Alt+E',
|
||||
openSettings: 'Ctrl+Shift+P',
|
||||
invalidKey: 'Ctrl+I',
|
||||
};
|
||||
mockStoreManager.get.mockReturnValue(storedConfig);
|
||||
vi.mocked(globalShortcut.register).mockReturnValue(true);
|
||||
|
||||
shortcutManager.initialize();
|
||||
|
||||
// Should filter config and register valid shortcuts
|
||||
const config = shortcutManager.getShortcutsConfig();
|
||||
expect(config.showApp).toBe('Alt+E');
|
||||
expect(config.openSettings).toBe('Ctrl+Shift+P');
|
||||
expect(config.invalidKey).toBeUndefined();
|
||||
|
||||
expect(globalShortcut.register).toHaveBeenCalledTimes(2);
|
||||
expect(mockStoreManager.set).toHaveBeenCalledWith('shortcuts', config);
|
||||
});
|
||||
|
||||
it('should handle complete update workflow', () => {
|
||||
mockStoreManager.get.mockReturnValue({});
|
||||
shortcutManager.initialize();
|
||||
|
||||
// Update a shortcut
|
||||
const result = shortcutManager.updateShortcutConfig('showApp', 'mod+alt+e');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Should convert format and register
|
||||
const config = shortcutManager.getShortcutsConfig();
|
||||
expect(config.showApp).toBe('CommandOrControl+alt+E');
|
||||
|
||||
// Should have saved and re-registered shortcuts
|
||||
expect(mockStoreManager.set).toHaveBeenCalled();
|
||||
expect(globalShortcut.unregisterAll).toHaveBeenCalled();
|
||||
expect(globalShortcut.register).toHaveBeenCalledWith(
|
||||
'CommandOrControl+alt+E',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,11 @@
|
||||
* 快捷键操作类型枚举
|
||||
*/
|
||||
export const ShortcutActionEnum = {
|
||||
openSettings: 'openSettings',
|
||||
/**
|
||||
* 显示/隐藏主窗口
|
||||
*/
|
||||
showMainWindow: 'showMainWindow',
|
||||
showApp: 'showApp',
|
||||
} as const;
|
||||
|
||||
export type ShortcutActionType = (typeof ShortcutActionEnum)[keyof typeof ShortcutActionEnum];
|
||||
@@ -14,5 +15,6 @@ export type ShortcutActionType = (typeof ShortcutActionEnum)[keyof typeof Shortc
|
||||
* 默认快捷键配置
|
||||
*/
|
||||
export const DEFAULT_SHORTCUTS_CONFIG: Record<ShortcutActionType, string> = {
|
||||
[ShortcutActionEnum.showMainWindow]: 'Control+E',
|
||||
[ShortcutActionEnum.showApp]: 'Control+E',
|
||||
[ShortcutActionEnum.openSettings]: 'CommandOrControl+,',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,72 @@
|
||||
[
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Support more Text2Image from Qwen."]
|
||||
},
|
||||
"date": "2025-07-29",
|
||||
"version": "1.105.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Implement API Key management functionality."]
|
||||
},
|
||||
"date": "2025-07-28",
|
||||
"version": "1.105.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Fix setting window layout when in desktop was disappear."]
|
||||
},
|
||||
"date": "2025-07-28",
|
||||
"version": "1.104.5"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Fix setting window layout size, update i18n."]
|
||||
},
|
||||
"date": "2025-07-28",
|
||||
"version": "1.104.4"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Add Gemini 2.5 Flash-Lite GA model."]
|
||||
},
|
||||
"date": "2025-07-26",
|
||||
"version": "1.104.3"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix update hotkey invalid when input mod in desktop."]
|
||||
},
|
||||
"date": "2025-07-26",
|
||||
"version": "1.104.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": [
|
||||
"Update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider."
|
||||
]
|
||||
},
|
||||
"date": "2025-07-25",
|
||||
"version": "1.104.1"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": ["Support custom hotkey on desktop."]
|
||||
},
|
||||
"date": "2025-07-24",
|
||||
"version": "1.104.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["Fix chat stream in desktop and update shortcut."],
|
||||
"improvements": [
|
||||
"Add cached token count to usage of GoogleAI and VertexAI, fix desktop titlebar style in window, fix sub topic width in md responsive."
|
||||
]
|
||||
},
|
||||
"date": "2025-07-24",
|
||||
"version": "1.103.2"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Update i18n."]
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "تم الإنشاء تلقائيًا",
|
||||
"copy": "نسخ",
|
||||
"copyError": "فشل النسخ",
|
||||
"copySuccess": "تم نسخ مفتاح API إلى الحافظة",
|
||||
"enterPlaceholder": "الرجاء الإدخال",
|
||||
"hide": "إخفاء",
|
||||
"neverExpires": "لا تنتهي صلاحيتها أبدًا",
|
||||
"neverUsed": "لم يُستخدم أبدًا",
|
||||
"show": "عرض"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "تاريخ الانتهاء",
|
||||
"placeholder": "لا تنتهي صلاحيتها أبدًا"
|
||||
},
|
||||
"name": {
|
||||
"label": "الاسم",
|
||||
"placeholder": "الرجاء إدخال اسم مفتاح API"
|
||||
}
|
||||
},
|
||||
"submit": "إنشاء",
|
||||
"title": "إنشاء مفتاح API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "إنشاء مفتاح API",
|
||||
"delete": "حذف",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "إلغاء",
|
||||
"ok": "تأكيد"
|
||||
},
|
||||
"content": "هل أنت متأكد من حذف هذا المفتاح؟",
|
||||
"title": "تأكيد العملية"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "الإجراءات",
|
||||
"expiresAt": "تاريخ الانتهاء",
|
||||
"key": "المفتاح",
|
||||
"lastUsedAt": "آخر استخدام",
|
||||
"name": "الاسم",
|
||||
"status": "حالة التفعيل"
|
||||
},
|
||||
"title": "قائمة مفاتيح API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "لا يمكن أن يكون المحتوى فارغًا"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "الشهر الماضي",
|
||||
"recent30Days": "آخر 30 يومًا"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "كلمات"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "إدارة مفاتيح API",
|
||||
"profile": "الملف الشخصي",
|
||||
"security": "الأمان",
|
||||
"stats": "الإحصائيات"
|
||||
|
||||
+10
-4
@@ -7,6 +7,16 @@
|
||||
"desc": "مسح الرسائل والملفات المرفوعة في المحادثة الحالية",
|
||||
"title": "مسح رسائل المحادثة"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "افتح صفحة إعدادات التطبيق",
|
||||
"title": "إعدادات التطبيق"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "مفتاح اختصار عام لإظهار أو إخفاء النافذة الرئيسية",
|
||||
"title": "إظهار/إخفاء النافذة الرئيسية"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "الدخول إلى وضع التحرير عن طريق الضغط على مفتاح Alt والنقر المزدوج على الرسالة",
|
||||
"title": "تحرير الرسالة"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "عرض جميع تعليمات استخدام الاختصارات",
|
||||
"title": "فتح مساعدة الاختصارات"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "فتح صفحة إعدادات التطبيق",
|
||||
"title": "إعدادات التطبيق"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "إعادة توليد آخر رسالة",
|
||||
"title": "إعادة توليد الرسالة"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash هو نموذج Google الأكثر فعالية من حيث التكلفة، ويوفر وظائف شاملة."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite هو أصغر وأفضل نموذج من حيث التكلفة من Google، مصمم للاستخدام على نطاق واسع."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview هو أصغر وأكفأ نموذج من Google، مصمم للاستخدام واسع النطاق."
|
||||
},
|
||||
|
||||
+12
-1
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "يتعارض مع اختصارات لوحة المفاتيح الحالية",
|
||||
"errors": {
|
||||
"CONFLICT": "تعارض في اختصار لوحة المفاتيح: هذا الاختصار مستخدم بالفعل من قبل وظيفة أخرى",
|
||||
"INVALID_FORMAT": "تنسيق اختصار لوحة المفاتيح غير صالح: يرجى استخدام التنسيق الصحيح (مثل CommandOrControl+E)",
|
||||
"INVALID_ID": "معرف اختصار لوحة المفاتيح غير صالح",
|
||||
"NO_MODIFIER": "يجب أن يحتوي اختصار لوحة المفاتيح على مفتاح تعديل (Ctrl، Alt، Shift، إلخ)",
|
||||
"SYSTEM_OCCUPIED": "اختصار لوحة المفاتيح مستخدم من قبل النظام أو تطبيقات أخرى",
|
||||
"UNKNOWN": "فشل التحديث: خطأ غير معروف"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "المحادثة",
|
||||
"desktop": "سطح المكتب",
|
||||
"essential": "أساسي"
|
||||
},
|
||||
"invalidCombination": "يجب أن تحتوي اختصارات لوحة المفاتيح على مفتاح تعديل واحد على الأقل (Ctrl، Alt، Shift) ومفتاح عادي واحد",
|
||||
"record": "اضغط على المفتاح لتسجيل اختصار لوحة المفاتيح",
|
||||
"reset": "إعادة تعيين إلى اختصارات لوحة المفاتيح الافتراضية",
|
||||
"title": "اختصارات لوحة المفاتيح"
|
||||
"title": "اختصارات لوحة المفاتيح",
|
||||
"updateError": "فشل تحديث اختصار لوحة المفاتيح: خطأ في الشبكة أو النظام",
|
||||
"updateSuccess": "تم تحديث اختصار لوحة المفاتيح بنجاح"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "سيتم استخدام خوارزمية التشفير <1>AES-GCM</1> لتشفير مفتاحك وعنوان الوكيل",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Автоматично генериран",
|
||||
"copy": "Копирай",
|
||||
"copyError": "Грешка при копиране",
|
||||
"copySuccess": "API ключът е копиран в клипборда",
|
||||
"enterPlaceholder": "Моля, въведете",
|
||||
"hide": "Скрий",
|
||||
"neverExpires": "Никога не изтича",
|
||||
"neverUsed": "Никога не е използван",
|
||||
"show": "Покажи"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Дата на изтичане",
|
||||
"placeholder": "Никога не изтича"
|
||||
},
|
||||
"name": {
|
||||
"label": "Име",
|
||||
"placeholder": "Моля, въведете име на API ключ"
|
||||
}
|
||||
},
|
||||
"submit": "Създай",
|
||||
"title": "Създаване на API ключ"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Създай API ключ",
|
||||
"delete": "Изтрий",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Отказ",
|
||||
"ok": "Потвърди"
|
||||
},
|
||||
"content": "Сигурни ли сте, че искате да изтриете този API ключ?",
|
||||
"title": "Потвърждение на действие"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Действия",
|
||||
"expiresAt": "Дата на изтичане",
|
||||
"key": "Ключ",
|
||||
"lastUsedAt": "Последна употреба",
|
||||
"name": "Име",
|
||||
"status": "Статус на активиране"
|
||||
},
|
||||
"title": "Списък с API ключове"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Полето не може да бъде празно"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Миналия месец",
|
||||
"recent30Days": "Последните 30 дни"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Думи"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Управление на API ключове",
|
||||
"profile": "Профил",
|
||||
"security": "Сигурност",
|
||||
"stats": "Статистика"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Изтриване на текущите съобщения и качените файлове в сесията",
|
||||
"title": "Изтриване на съобщенията в сесията"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Отворете страницата с настройки на приложението",
|
||||
"title": "Настройки на приложението"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Глобална клавишна комбинация за показване или скриване на главния прозорец",
|
||||
"title": "Показване/скриване на главния прозорец"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Влезте в режим на редактиране, като задържите Alt и два пъти кликнете върху съобщението",
|
||||
"title": "Редактиране на съобщение"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Прегледайте инструкциите за използване на всички клавишни комбинации",
|
||||
"title": "Отворете помощта за клавишни комбинации"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Отворете страницата с настройки на приложението",
|
||||
"title": "Настройки на приложението"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Прегенерирайте последното съобщение",
|
||||
"title": "Прегенериране на съобщение"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash е най-ефективният модел на Google, предлагащ пълна функционалност."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite е най-малкият и най-ефективен модел на Google, създаден специално за масово използване."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview е най-малкият и най-ефективен модел на Google, проектиран за мащабна употреба."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Конфликт с текущите клавишни комбинации",
|
||||
"errors": {
|
||||
"CONFLICT": "Конфликт на клавишната комбинация: тази комбинация вече е заета от друга функция",
|
||||
"INVALID_FORMAT": "Невалиден формат на клавишната комбинация: моля, използвайте правилния формат (например CommandOrControl+E)",
|
||||
"INVALID_ID": "Невалиден идентификатор на клавишната комбинация",
|
||||
"NO_MODIFIER": "Клавишната комбинация трябва да съдържа модификатор (Ctrl, Alt, Shift и др.)",
|
||||
"SYSTEM_OCCUPIED": "Клавишната комбинация е заета от системата или друго приложение",
|
||||
"UNKNOWN": "Актуализацията не бе успешна: неизвестна грешка"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Разговор",
|
||||
"desktop": "Настолен",
|
||||
"essential": "Основен"
|
||||
},
|
||||
"invalidCombination": "Клавишната комбинация трябва да съдържа поне един модификатор (Ctrl, Alt, Shift) и един обикновен клавиш",
|
||||
"record": "Натиснете клавиш, за да запишете клавишна комбинация",
|
||||
"reset": "Нулиране до подразбиращите се клавишни комбинации",
|
||||
"title": "Бързи клавиши"
|
||||
"title": "Бързи клавиши",
|
||||
"updateError": "Актуализацията на клавишната комбинация не бе успешна: мрежова или системна грешка",
|
||||
"updateSuccess": "Актуализацията на клавишната комбинация бе успешна"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Вашият ключ и адрес на агента ще бъдат криптирани с алгоритъма за криптиране <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Automatisch generiert",
|
||||
"copy": "Kopieren",
|
||||
"copyError": "Kopieren fehlgeschlagen",
|
||||
"copySuccess": "API-Schlüssel wurde in die Zwischenablage kopiert",
|
||||
"enterPlaceholder": "Bitte eingeben",
|
||||
"hide": "Verbergen",
|
||||
"neverExpires": "Läuft nie ab",
|
||||
"neverUsed": "Nie verwendet",
|
||||
"show": "Anzeigen"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Ablaufdatum",
|
||||
"placeholder": "Läuft nie ab"
|
||||
},
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Bitte API-Schlüsselname eingeben"
|
||||
}
|
||||
},
|
||||
"submit": "Erstellen",
|
||||
"title": "API-Schlüssel erstellen"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API-Schlüssel erstellen",
|
||||
"delete": "Löschen",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "Bestätigen"
|
||||
},
|
||||
"content": "Möchten Sie diesen API-Schlüssel wirklich löschen?",
|
||||
"title": "Bestätigung"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Aktionen",
|
||||
"expiresAt": "Ablaufdatum",
|
||||
"key": "Schlüssel",
|
||||
"lastUsedAt": "Letzte Verwendung",
|
||||
"name": "Name",
|
||||
"status": "Aktivierungsstatus"
|
||||
},
|
||||
"title": "API-Schlüssel Liste"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Inhalt darf nicht leer sein"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Letzter Monat",
|
||||
"recent30Days": "Letzte 30 Tage"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Wörter"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API-Schlüssel Verwaltung",
|
||||
"profile": "Profil",
|
||||
"security": "Sicherheit",
|
||||
"stats": "Statistiken"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Aktuelle Nachrichten und hochgeladene Dateien im Gespräch löschen",
|
||||
"title": "Gesprächsnachrichten löschen"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Öffnet die Anwendungseinstellungsseite",
|
||||
"title": "Anwendungseinstellungen"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Globale Tastenkombination zum Anzeigen oder Verbergen des Hauptfensters",
|
||||
"title": "Hauptfenster anzeigen/verbergen"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Treten Sie in den Bearbeitungsmodus, indem Sie die Alt-Taste gedrückt halten und auf die Nachricht doppelklicken",
|
||||
"title": "Nachricht bearbeiten"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Anleitung zur Verwendung aller Tastenkombinationen anzeigen",
|
||||
"title": "Tastenkombinationshilfe öffnen"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Öffnen Sie die Anwendungseinstellungen",
|
||||
"title": "Anwendungseinstellungen"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Die letzte Nachricht neu generieren",
|
||||
"title": "Nachricht neu generieren"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash ist Googles kosteneffizientestes Modell und bietet umfassende Funktionen."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite ist Googles kleinstes und kosteneffizientestes Modell, das speziell für den großflächigen Einsatz entwickelt wurde."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview ist Googles kleinstes und kosteneffizientestes Modell, speziell für den großflächigen Einsatz konzipiert."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Konflikte mit bestehenden Tastenkombinationen",
|
||||
"errors": {
|
||||
"CONFLICT": "Tastenkonflikt: Diese Tastenkombination wird bereits von einer anderen Funktion verwendet",
|
||||
"INVALID_FORMAT": "Ungültiges Tastenkürzel-Format: Bitte verwenden Sie das korrekte Format (z. B. CommandOrControl+E)",
|
||||
"INVALID_ID": "Ungültige Tastenkürzel-ID",
|
||||
"NO_MODIFIER": "Das Tastenkürzel muss einen Modifikatortaste enthalten (Strg, Alt, Shift usw.)",
|
||||
"SYSTEM_OCCUPIED": "Das Tastenkürzel wird vom System oder einer anderen Anwendung verwendet",
|
||||
"UNKNOWN": "Aktualisierung fehlgeschlagen: Unbekannter Fehler"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Gespräch",
|
||||
"desktop": "Desktop",
|
||||
"essential": "Grundlegend"
|
||||
},
|
||||
"invalidCombination": "Die Tastenkombination muss mindestens einen Modifikatortaste (Strg, Alt, Umschalt) und eine normale Taste enthalten",
|
||||
"record": "Drücken Sie eine Taste, um die Tastenkombination aufzuzeichnen",
|
||||
"reset": "Auf die Standard-Tastenkombination zurücksetzen",
|
||||
"title": "Tastenkombinationen"
|
||||
"title": "Tastenkombinationen",
|
||||
"updateError": "Tastenkürzel-Aktualisierung fehlgeschlagen: Netzwerk- oder Systemfehler",
|
||||
"updateSuccess": "Tastenkürzel erfolgreich aktualisiert"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Ihr Schlüssel und Ihre Proxy-Adresse werden mit dem <1>AES-GCM</1> Verschlüsselungsalgorithmus verschlüsselt.",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Auto-generated",
|
||||
"copy": "Copy",
|
||||
"copyError": "Copy failed",
|
||||
"copySuccess": "API Key copied to clipboard",
|
||||
"enterPlaceholder": "Please enter",
|
||||
"hide": "Hide",
|
||||
"neverExpires": "Never expires",
|
||||
"neverUsed": "Never used",
|
||||
"show": "Show"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Expiration Date",
|
||||
"placeholder": "Never expires"
|
||||
},
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Please enter API Key name"
|
||||
}
|
||||
},
|
||||
"submit": "Create",
|
||||
"title": "Create API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Create API Key",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancel",
|
||||
"ok": "Confirm"
|
||||
},
|
||||
"content": "Are you sure you want to delete this API Key?",
|
||||
"title": "Confirm Action"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Actions",
|
||||
"expiresAt": "Expiration Date",
|
||||
"key": "Key",
|
||||
"lastUsedAt": "Last Used",
|
||||
"name": "Name",
|
||||
"status": "Enabled Status"
|
||||
},
|
||||
"title": "API Key List"
|
||||
},
|
||||
"validation": {
|
||||
"required": "This field cannot be empty"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Last Month",
|
||||
"recent30Days": "Last 30 Days"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Total Words"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key Management",
|
||||
"profile": "Profile",
|
||||
"security": "Security",
|
||||
"stats": "Statistics"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Clear the messages and uploaded files from the current conversation",
|
||||
"title": "Clear Conversation Messages"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Open the application settings page",
|
||||
"title": "Application Settings"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Toggle the main window visibility with a global shortcut",
|
||||
"title": "Show/Hide Main Window"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Enter edit mode by holding Alt and double-clicking the message",
|
||||
"title": "Edit Message"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "View instructions for all keyboard shortcuts",
|
||||
"title": "Open Hotkey Help"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Open the application settings page",
|
||||
"title": "Application Settings"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Regenerate the last message",
|
||||
"title": "Regenerate Message"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash is Google's most cost-effective model, offering comprehensive capabilities."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite is Google's smallest and most cost-effective model, designed for large-scale use."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview is Google's smallest and most cost-efficient model, designed for large-scale usage."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Conflicts with existing hotkeys",
|
||||
"errors": {
|
||||
"CONFLICT": "Hotkey conflict: This hotkey is already assigned to another function",
|
||||
"INVALID_FORMAT": "Invalid hotkey format: Please use the correct format (e.g., CommandOrControl+E)",
|
||||
"INVALID_ID": "Invalid hotkey ID",
|
||||
"NO_MODIFIER": "Hotkey must include a modifier key (Ctrl, Alt, Shift, etc.)",
|
||||
"SYSTEM_OCCUPIED": "Hotkey is occupied by the system or another application",
|
||||
"UNKNOWN": "Update failed: Unknown error"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Conversation",
|
||||
"desktop": "Desktop",
|
||||
"essential": "Essential"
|
||||
},
|
||||
"invalidCombination": "The hotkey must include at least one modifier key (Ctrl, Alt, Shift) and one regular key",
|
||||
"record": "Press a key to record the hotkey",
|
||||
"reset": "Reset to default hotkeys",
|
||||
"title": "Hotkeys"
|
||||
"title": "Hotkeys",
|
||||
"updateError": "Failed to update hotkey: Network or system error",
|
||||
"updateSuccess": "Hotkey updated successfully"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Your keys and proxy address will be encrypted using the <1>AES-GCM</1> encryption algorithm",
|
||||
@@ -524,6 +535,7 @@
|
||||
"experiment": "Experiment",
|
||||
"hotkey": "Hotkeys",
|
||||
"llm": "Language Model",
|
||||
"plugin": "Plugin Management",
|
||||
"provider": "AI Service Provider",
|
||||
"proxy": "Network Proxy",
|
||||
"storage": "Data Storage",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Generado automáticamente",
|
||||
"copy": "Copiar",
|
||||
"copyError": "Error al copiar",
|
||||
"copySuccess": "Clave API copiada al portapapeles",
|
||||
"enterPlaceholder": "Por favor ingrese",
|
||||
"hide": "Ocultar",
|
||||
"neverExpires": "Nunca expira",
|
||||
"neverUsed": "Nunca usado",
|
||||
"show": "Mostrar"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Fecha de expiración",
|
||||
"placeholder": "Nunca expira"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nombre",
|
||||
"placeholder": "Por favor ingrese el nombre de la clave API"
|
||||
}
|
||||
},
|
||||
"submit": "Crear",
|
||||
"title": "Crear Clave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Crear Clave API",
|
||||
"delete": "Eliminar",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Confirmar"
|
||||
},
|
||||
"content": "¿Está seguro de eliminar esta clave API?",
|
||||
"title": "Confirmar acción"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Acciones",
|
||||
"expiresAt": "Fecha de expiración",
|
||||
"key": "Clave",
|
||||
"lastUsedAt": "Último uso",
|
||||
"name": "Nombre",
|
||||
"status": "Estado"
|
||||
},
|
||||
"title": "Lista de Claves API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "El contenido no puede estar vacío"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Último mes",
|
||||
"recent30Days": "Últimos 30 días"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Palabras"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestión de Claves API",
|
||||
"profile": "Perfil",
|
||||
"security": "Seguridad",
|
||||
"stats": "Estadísticas"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Eliminar los mensajes y archivos subidos de la conversación actual",
|
||||
"title": "Eliminar mensajes de la conversación"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Abrir la página de configuración de la aplicación",
|
||||
"title": "Configuración de la aplicación"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Mostrar u ocultar la ventana principal mediante un atajo global",
|
||||
"title": "Mostrar/Ocultar ventana principal"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Entrar en modo de edición manteniendo presionada la tecla Alt y haciendo doble clic en el mensaje",
|
||||
"title": "Editar mensaje"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Ver las instrucciones de uso de todos los atajos de teclado",
|
||||
"title": "Abrir ayuda de atajos de teclado"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Abrir la página de configuración de la aplicación",
|
||||
"title": "Configuración de la aplicación"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Regenerar el último mensaje",
|
||||
"title": "Regenerar mensaje"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash es el modelo de mejor relación calidad-precio de Google, que ofrece funcionalidades completas."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite es el modelo más pequeño y rentable de Google, diseñado para un uso a gran escala."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview es el modelo más pequeño y con mejor relación calidad-precio de Google, diseñado para un uso a gran escala."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Conflicto con las teclas de acceso rápido existentes",
|
||||
"errors": {
|
||||
"CONFLICT": "Conflicto de atajo: este atajo ya está asignado a otra función",
|
||||
"INVALID_FORMAT": "Formato de atajo inválido: por favor use el formato correcto (por ejemplo, CommandOrControl+E)",
|
||||
"INVALID_ID": "ID de atajo inválido",
|
||||
"NO_MODIFIER": "El atajo debe incluir una tecla modificadora (Ctrl, Alt, Shift, etc.)",
|
||||
"SYSTEM_OCCUPIED": "El atajo está ocupado por el sistema u otra aplicación",
|
||||
"UNKNOWN": "Error al actualizar: error desconocido"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Conversación",
|
||||
"desktop": "Escritorio",
|
||||
"essential": "Esencial"
|
||||
},
|
||||
"invalidCombination": "La combinación de teclas de acceso rápido debe incluir al menos una tecla modificadora (Ctrl, Alt, Shift) y una tecla normal",
|
||||
"record": "Presiona una tecla para grabar la tecla de acceso rápido",
|
||||
"reset": "Restablecer a las teclas de acceso rápido predeterminadas",
|
||||
"title": "Atajos de teclado"
|
||||
"title": "Atajos de teclado",
|
||||
"updateError": "Error al actualizar el atajo: problema de red o del sistema",
|
||||
"updateSuccess": "Atajo actualizado con éxito"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Su clave y dirección del agente se cifrarán utilizando el algoritmo de cifrado <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "تولید خودکار",
|
||||
"copy": "کپی",
|
||||
"copyError": "کپی ناموفق بود",
|
||||
"copySuccess": "کلید API به کلیپبورد کپی شد",
|
||||
"enterPlaceholder": "لطفاً وارد کنید",
|
||||
"hide": "مخفی کردن",
|
||||
"neverExpires": "هرگز منقضی نمیشود",
|
||||
"neverUsed": "هرگز استفاده نشده",
|
||||
"show": "نمایش"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "تاریخ انقضا",
|
||||
"placeholder": "هرگز منقضی نمیشود"
|
||||
},
|
||||
"name": {
|
||||
"label": "نام",
|
||||
"placeholder": "لطفاً نام کلید API را وارد کنید"
|
||||
}
|
||||
},
|
||||
"submit": "ایجاد",
|
||||
"title": "ایجاد کلید API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "ایجاد کلید API",
|
||||
"delete": "حذف",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "لغو",
|
||||
"ok": "تأیید"
|
||||
},
|
||||
"content": "آیا از حذف این کلید API مطمئن هستید؟",
|
||||
"title": "تأیید عملیات"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "عملیات",
|
||||
"expiresAt": "تاریخ انقضا",
|
||||
"key": "کلید",
|
||||
"lastUsedAt": "آخرین زمان استفاده",
|
||||
"name": "نام",
|
||||
"status": "وضعیت فعال"
|
||||
},
|
||||
"title": "فهرست کلیدهای API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "محتوا نباید خالی باشد"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "ماه گذشته",
|
||||
"recent30Days": "۳۰ روز گذشته"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "کلمات"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "مدیریت کلید API",
|
||||
"profile": "پروفایل",
|
||||
"security": "امنیت",
|
||||
"stats": "آمار"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "حذف پیامها و فایلهای بارگذاری شده در جلسه جاری",
|
||||
"title": "حذف پیامهای جلسه"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "باز کردن صفحه تنظیمات برنامه",
|
||||
"title": "تنظیمات برنامه"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "نمایش یا پنهان کردن پنجره اصلی با کلید میانبر جهانی",
|
||||
"title": "نمایش/پنهان کردن پنجره اصلی"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "با نگه داشتن کلید Alt و دوبار کلیک بر روی پیام وارد حالت ویرایش شوید",
|
||||
"title": "ویرایش پیام"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "مشاهده تمام توضیحات استفاده از کلیدهای میانبر",
|
||||
"title": "باز کردن راهنمای کلیدهای میانبر"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "صفحه تنظیمات برنامه را باز کنید",
|
||||
"title": "تنظیمات برنامه"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "آخرین پیام را دوباره تولید کنید",
|
||||
"title": "تولید مجدد پیام"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash مدل با بهترین نسبت قیمت به کارایی گوگل است که امکانات جامع را ارائه میدهد."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite کوچکترین و مقرونبهصرفهترین مدل گوگل است که برای استفاده در مقیاس وسیع طراحی شده است."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview کوچکترین و مقرونبهصرفهترین مدل گوگل است که برای استفاده در مقیاس بزرگ طراحی شده است."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "تداخل با کلیدهای میانبر موجود",
|
||||
"errors": {
|
||||
"CONFLICT": "تداخل کلید میانبر: این کلید میانبر قبلاً توسط عملکرد دیگری استفاده شده است",
|
||||
"INVALID_FORMAT": "فرمت کلید میانبر نامعتبر است: لطفاً از فرمت صحیح استفاده کنید (مانند CommandOrControl+E)",
|
||||
"INVALID_ID": "شناسه کلید میانبر نامعتبر است",
|
||||
"NO_MODIFIER": "کلید میانبر باید شامل کلیدهای تغییر دهنده (Ctrl، Alt، Shift و غیره) باشد",
|
||||
"SYSTEM_OCCUPIED": "کلید میانبر توسط سیستم یا برنامههای دیگر اشغال شده است",
|
||||
"UNKNOWN": "بهروزرسانی ناموفق بود: خطای ناشناخته"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "گفتگو",
|
||||
"desktop": "نسخه دسکتاپ",
|
||||
"essential": "اساسی"
|
||||
},
|
||||
"invalidCombination": "کلیدهای میانبر باید حداقل شامل یک کلید اصلاحی (Ctrl, Alt, Shift) و یک کلید معمولی باشند",
|
||||
"record": "برای ضبط کلید میانبر، کلید را فشار دهید",
|
||||
"reset": "بازنشانی به کلیدهای میانبر پیشفرض",
|
||||
"title": "کلیدهای میانبر"
|
||||
"title": "کلیدهای میانبر",
|
||||
"updateError": "بهروزرسانی کلید میانبر ناموفق بود: خطای شبکه یا سیستم",
|
||||
"updateSuccess": "کلید میانبر با موفقیت بهروزرسانی شد"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "کلید و آدرس پروکسی شما با استفاده از الگوریتم رمزنگاری <1>AES-GCM</1> رمزگذاری خواهد شد",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Généré automatiquement",
|
||||
"copy": "Copier",
|
||||
"copyError": "Échec de la copie",
|
||||
"copySuccess": "Clé API copiée dans le presse-papiers",
|
||||
"enterPlaceholder": "Veuillez saisir",
|
||||
"hide": "Cacher",
|
||||
"neverExpires": "N'expire jamais",
|
||||
"neverUsed": "Jamais utilisé",
|
||||
"show": "Afficher"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Date d'expiration",
|
||||
"placeholder": "N'expire jamais"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nom",
|
||||
"placeholder": "Veuillez saisir le nom de la clé API"
|
||||
}
|
||||
},
|
||||
"submit": "Créer",
|
||||
"title": "Créer une clé API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Créer une clé API",
|
||||
"delete": "Supprimer",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Annuler",
|
||||
"ok": "Confirmer"
|
||||
},
|
||||
"content": "Confirmez-vous la suppression de cette clé API ?",
|
||||
"title": "Confirmation"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Actions",
|
||||
"expiresAt": "Date d'expiration",
|
||||
"key": "Clé",
|
||||
"lastUsedAt": "Dernière utilisation",
|
||||
"name": "Nom",
|
||||
"status": "Statut d'activation"
|
||||
},
|
||||
"title": "Liste des clés API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Ce champ est obligatoire"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Le mois dernier",
|
||||
"recent30Days": "Les 30 derniers jours"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Mots"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestion des clés API",
|
||||
"profile": "Profil",
|
||||
"security": "Sécurité",
|
||||
"stats": "Statistiques"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Effacer les messages de la session actuelle et les fichiers téléchargés",
|
||||
"title": "Effacer les messages de la session"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Ouvrir la page des paramètres de l'application",
|
||||
"title": "Paramètres de l'application"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Afficher ou masquer la fenêtre principale via un raccourci global",
|
||||
"title": "Afficher/Masquer la fenêtre principale"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Entrez en mode édition en maintenant la touche Alt enfoncée et en double-cliquant sur le message",
|
||||
"title": "Éditer le message"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Voir les instructions d'utilisation de tous les raccourcis",
|
||||
"title": "Ouvrir l'aide des raccourcis"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Ouvrir la page des paramètres de l'application",
|
||||
"title": "Paramètres de l'application"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Régénérer le dernier message",
|
||||
"title": "Régénérer le message"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash est le modèle le plus rentable de Google, offrant des fonctionnalités complètes."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite est le modèle le plus petit et le plus rentable de Google, conçu pour une utilisation à grande échelle."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview est le modèle le plus compact et rentable de Google, conçu pour une utilisation à grande échelle."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Conflit avec les raccourcis existants",
|
||||
"errors": {
|
||||
"CONFLICT": "Conflit de raccourci : ce raccourci est déjà utilisé par une autre fonction",
|
||||
"INVALID_FORMAT": "Format de raccourci invalide : veuillez utiliser le format correct (par exemple CommandOrControl+E)",
|
||||
"INVALID_ID": "ID de raccourci invalide",
|
||||
"NO_MODIFIER": "Le raccourci doit inclure une touche modificateur (Ctrl, Alt, Shift, etc.)",
|
||||
"SYSTEM_OCCUPIED": "Le raccourci est déjà utilisé par le système ou une autre application",
|
||||
"UNKNOWN": "Échec de la mise à jour : erreur inconnue"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Conversation",
|
||||
"desktop": "Bureau",
|
||||
"essential": "Essentiel"
|
||||
},
|
||||
"invalidCombination": "Le raccourci doit contenir au moins une touche de modification (Ctrl, Alt, Shift) et une touche normale",
|
||||
"record": "Appuyez sur une touche pour enregistrer le raccourci",
|
||||
"reset": "Réinitialiser aux raccourcis par défaut",
|
||||
"title": "Raccourcis clavier"
|
||||
"title": "Raccourcis clavier",
|
||||
"updateError": "Échec de la mise à jour du raccourci : erreur réseau ou système",
|
||||
"updateSuccess": "Mise à jour du raccourci réussie"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Votre clé, votre adresse de proxy, etc. seront cryptées à l'aide de l'algorithme de chiffrement <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Generato automaticamente",
|
||||
"copy": "Copia",
|
||||
"copyError": "Copia non riuscita",
|
||||
"copySuccess": "Chiave API copiata negli appunti",
|
||||
"enterPlaceholder": "Inserisci",
|
||||
"hide": "Nascondi",
|
||||
"neverExpires": "Non scade mai",
|
||||
"neverUsed": "Mai usato",
|
||||
"show": "Mostra"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Data di scadenza",
|
||||
"placeholder": "Non scade mai"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nome",
|
||||
"placeholder": "Inserisci il nome della Chiave API"
|
||||
}
|
||||
},
|
||||
"submit": "Crea",
|
||||
"title": "Crea Chiave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Crea Chiave API",
|
||||
"delete": "Elimina",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Annulla",
|
||||
"ok": "Conferma"
|
||||
},
|
||||
"content": "Sei sicuro di voler eliminare questa Chiave API?",
|
||||
"title": "Conferma azione"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Azioni",
|
||||
"expiresAt": "Data di scadenza",
|
||||
"key": "Chiave",
|
||||
"lastUsedAt": "Ultimo utilizzo",
|
||||
"name": "Nome",
|
||||
"status": "Stato attivo"
|
||||
},
|
||||
"title": "Elenco Chiavi API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Il contenuto non può essere vuoto"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Mese Scorso",
|
||||
"recent30Days": "Ultimi 30 Giorni"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Parole"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gestione Chiavi API",
|
||||
"profile": "Profilo",
|
||||
"security": "Sicurezza",
|
||||
"stats": "Statistiche"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Cancella i messaggi e i file caricati della conversazione attuale",
|
||||
"title": "Cancella messaggi della conversazione"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Apri la pagina delle impostazioni dell'app",
|
||||
"title": "Impostazioni dell'app"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Mostra o nascondi la finestra principale con una scorciatoia globale",
|
||||
"title": "Mostra/Nascondi finestra principale"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Entra in modalità di modifica tenendo premuto Alt e facendo doppio clic sul messaggio",
|
||||
"title": "Modifica messaggio"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Visualizza le istruzioni per l'uso di tutte le scorciatoie da tastiera",
|
||||
"title": "Apri aiuto scorciatoie"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Apri la pagina delle impostazioni dell'app",
|
||||
"title": "Impostazioni dell'app"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Rigenera l'ultimo messaggio",
|
||||
"title": "Rigenera messaggio"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash è il modello Google con il miglior rapporto qualità-prezzo, offrendo funzionalità complete."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite è il modello più piccolo e conveniente di Google, progettato per un utilizzo su larga scala."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview è il modello Google più piccolo e con il miglior rapporto qualità-prezzo, progettato per un utilizzo su larga scala."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "In conflitto con i tasti di scelta rapida esistenti",
|
||||
"errors": {
|
||||
"CONFLICT": "Conflitto di tasti rapidi: questo tasto è già assegnato ad un'altra funzione",
|
||||
"INVALID_FORMAT": "Formato del tasto rapido non valido: utilizzare un formato corretto (es. CommandOrControl+E)",
|
||||
"INVALID_ID": "ID del tasto rapido non valido",
|
||||
"NO_MODIFIER": "Il tasto rapido deve includere un modificatore (Ctrl, Alt, Shift, ecc.)",
|
||||
"SYSTEM_OCCUPIED": "Il tasto rapido è già occupato dal sistema o da un'altra applicazione",
|
||||
"UNKNOWN": "Aggiornamento fallito: errore sconosciuto"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Conversazione",
|
||||
"desktop": "Desktop",
|
||||
"essential": "Essenziale"
|
||||
},
|
||||
"invalidCombination": "La combinazione di tasti deve contenere almeno un tasto modificatore (Ctrl, Alt, Shift) e un tasto normale",
|
||||
"record": "Premi un tasto per registrare la scorciatoia",
|
||||
"reset": "Ripristina le scorciatoie predefinite",
|
||||
"title": "Scorciatoie"
|
||||
"title": "Scorciatoie",
|
||||
"updateError": "Aggiornamento del tasto rapido fallito: errore di rete o di sistema",
|
||||
"updateSuccess": "Aggiornamento del tasto rapido riuscito"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "La tua chiave e l'indirizzo dell'agente saranno crittografati utilizzando l'algoritmo di crittografia <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自動生成",
|
||||
"copy": "コピー",
|
||||
"copyError": "コピーに失敗しました",
|
||||
"copySuccess": "APIキーがクリップボードにコピーされました",
|
||||
"enterPlaceholder": "入力してください",
|
||||
"hide": "非表示",
|
||||
"neverExpires": "期限なし",
|
||||
"neverUsed": "未使用",
|
||||
"show": "表示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "有効期限",
|
||||
"placeholder": "期限なし"
|
||||
},
|
||||
"name": {
|
||||
"label": "名前",
|
||||
"placeholder": "APIキーの名前を入力してください"
|
||||
}
|
||||
},
|
||||
"submit": "作成",
|
||||
"title": "APIキーを作成"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "APIキーを作成",
|
||||
"delete": "削除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "キャンセル",
|
||||
"ok": "確認"
|
||||
},
|
||||
"content": "このAPIキーを削除してもよろしいですか?",
|
||||
"title": "操作の確認"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "有効期限",
|
||||
"key": "キー",
|
||||
"lastUsedAt": "最終使用日時",
|
||||
"name": "名前",
|
||||
"status": "有効状態"
|
||||
},
|
||||
"title": "APIキー一覧"
|
||||
},
|
||||
"validation": {
|
||||
"required": "内容を入力してください"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "先月",
|
||||
"recent30Days": "過去30日間"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "単語"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "APIキー管理",
|
||||
"profile": "プロフィール",
|
||||
"security": "セキュリティ",
|
||||
"stats": "統計"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "現在のセッションのメッセージとアップロードされたファイルをクリアする",
|
||||
"title": "セッションメッセージをクリア"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "アプリ設定ページを開く",
|
||||
"title": "アプリ設定"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "グローバルショートカットでメインウィンドウを表示または非表示にする",
|
||||
"title": "メインウィンドウの表示/非表示"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Altキーを押しながらメッセージをダブルクリックして編集モードに入ります",
|
||||
"title": "メッセージを編集"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "すべてのショートカットキーの使用説明を表示する",
|
||||
"title": "ショートカットヘルプを開く"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "アプリの設定ページを開く",
|
||||
"title": "アプリ設定"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "最後のメッセージを再生成します",
|
||||
"title": "メッセージを再生成"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 FlashはGoogleのコストパフォーマンスに優れたモデルで、包括的な機能を提供します。"
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite は、Google の中で最も小さく、コストパフォーマンスに優れたモデルであり、大規模な利用を目的に設計されています。"
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite PreviewはGoogleの最小かつコストパフォーマンスに優れたモデルで、大規模利用を目的に設計されています。"
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "既存のショートカットキーと衝突しています",
|
||||
"errors": {
|
||||
"CONFLICT": "ホットキーの競合:このホットキーは他の機能で既に使用されています",
|
||||
"INVALID_FORMAT": "ホットキーの形式が無効です:正しい形式を使用してください(例:CommandOrControl+E)",
|
||||
"INVALID_ID": "無効なホットキーIDです",
|
||||
"NO_MODIFIER": "ホットキーには修飾キー(Ctrl、Alt、Shiftなど)が含まれている必要があります",
|
||||
"SYSTEM_OCCUPIED": "ホットキーはシステムまたは他のアプリケーションで使用されています",
|
||||
"UNKNOWN": "更新に失敗しました:不明なエラー"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "会話",
|
||||
"desktop": "デスクトップ",
|
||||
"essential": "基本"
|
||||
},
|
||||
"invalidCombination": "ショートカットキーには少なくとも1つの修飾キー(Ctrl、Alt、Shift)と1つの通常のキーが必要です",
|
||||
"record": "ショートカットキーを録音するにはキーを押してください",
|
||||
"reset": "デフォルトのショートカットキーにリセット",
|
||||
"title": "ショートカットキー"
|
||||
"title": "ショートカットキー",
|
||||
"updateError": "ホットキーの更新に失敗しました:ネットワークまたはシステムエラー",
|
||||
"updateSuccess": "ホットキーが正常に更新されました"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "キーとプロキシアドレスなどは <1>AES-GCM</1> 暗号化アルゴリズムを使用して暗号化されます",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "자동 생성",
|
||||
"copy": "복사",
|
||||
"copyError": "복사 실패",
|
||||
"copySuccess": "API 키가 클립보드에 복사되었습니다",
|
||||
"enterPlaceholder": "입력하세요",
|
||||
"hide": "숨기기",
|
||||
"neverExpires": "만료되지 않음",
|
||||
"neverUsed": "한 번도 사용되지 않음",
|
||||
"show": "표시"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "만료 시간",
|
||||
"placeholder": "만료되지 않음"
|
||||
},
|
||||
"name": {
|
||||
"label": "이름",
|
||||
"placeholder": "API 키 이름을 입력하세요"
|
||||
}
|
||||
},
|
||||
"submit": "생성",
|
||||
"title": "API 키 생성"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API 키 생성",
|
||||
"delete": "삭제",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "취소",
|
||||
"ok": "확인"
|
||||
},
|
||||
"content": "이 API 키를 삭제하시겠습니까?",
|
||||
"title": "작업 확인"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "작업",
|
||||
"expiresAt": "만료 시간",
|
||||
"key": "키",
|
||||
"lastUsedAt": "마지막 사용 시간",
|
||||
"name": "이름",
|
||||
"status": "활성 상태"
|
||||
},
|
||||
"title": "API 키 목록"
|
||||
},
|
||||
"validation": {
|
||||
"required": "내용을 비워둘 수 없습니다"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "지난 달",
|
||||
"recent30Days": "최근 30일"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "단어"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API 키 관리",
|
||||
"profile": "프로필",
|
||||
"security": "보안",
|
||||
"stats": "통계"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "현재 대화의 메시지와 업로드된 파일을 지웁니다",
|
||||
"title": "대화 메시지 지우기"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "애플리케이션 설정 페이지 열기",
|
||||
"title": "애플리케이션 설정"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "글로벌 단축키로 메인 창 표시 또는 숨기기",
|
||||
"title": "메인 창 표시/숨기기"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Alt 키를 누른 채로 메시지를 더블 클릭하여 편집 모드로 들어갑니다",
|
||||
"title": "메시지 편집"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "모든 단축키 사용 설명을 확인합니다.",
|
||||
"title": "단축키 도움말 열기"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "앱 설정 페이지 열기",
|
||||
"title": "앱 설정"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "마지막 메시지를 다시 생성합니다",
|
||||
"title": "메시지 다시 생성"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash는 구글에서 가장 가성비가 뛰어난 모델로, 포괄적인 기능을 제공합니다."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite는 Google의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview는 구글의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "기존 단축키와 충돌",
|
||||
"errors": {
|
||||
"CONFLICT": "단축키 충돌: 해당 단축키는 이미 다른 기능에서 사용 중입니다",
|
||||
"INVALID_FORMAT": "단축키 형식이 올바르지 않습니다: 올바른 형식(예: CommandOrControl+E)을 사용하세요",
|
||||
"INVALID_ID": "유효하지 않은 단축키 ID입니다",
|
||||
"NO_MODIFIER": "단축키에는 반드시 수정 키(Ctrl, Alt, Shift 등)가 포함되어야 합니다",
|
||||
"SYSTEM_OCCUPIED": "단축키가 시스템 또는 다른 애플리케이션에서 사용 중입니다",
|
||||
"UNKNOWN": "업데이트 실패: 알 수 없는 오류"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "대화",
|
||||
"desktop": "데스크톱",
|
||||
"essential": "기본"
|
||||
},
|
||||
"invalidCombination": "단축키는 최소한 하나의 수정 키(Ctrl, Alt, Shift)와 하나의 일반 키를 포함해야 합니다",
|
||||
"record": "단축키를 녹음하려면 키를 누르세요",
|
||||
"reset": "기본 단축키로 재설정",
|
||||
"title": "단축키"
|
||||
"title": "단축키",
|
||||
"updateError": "단축키 업데이트 실패: 네트워크 또는 시스템 오류",
|
||||
"updateSuccess": "단축키가 성공적으로 업데이트되었습니다"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "귀하의 키 및 프록시 주소는 <1>AES-GCM</1> 암호화 알고리즘을 사용하여 암호화됩니다",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Automatisch gegenereerd",
|
||||
"copy": "Kopiëren",
|
||||
"copyError": "Kopiëren mislukt",
|
||||
"copySuccess": "API-sleutel is gekopieerd naar het klembord",
|
||||
"enterPlaceholder": "Voer in",
|
||||
"hide": "Verbergen",
|
||||
"neverExpires": "Verloopt nooit",
|
||||
"neverUsed": "Nooit gebruikt",
|
||||
"show": "Weergeven"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Vervaldatum",
|
||||
"placeholder": "Verloopt nooit"
|
||||
},
|
||||
"name": {
|
||||
"label": "Naam",
|
||||
"placeholder": "Voer de naam van de API-sleutel in"
|
||||
}
|
||||
},
|
||||
"submit": "Aanmaken",
|
||||
"title": "API-sleutel aanmaken"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API-sleutel aanmaken",
|
||||
"delete": "Verwijderen",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Annuleren",
|
||||
"ok": "Bevestigen"
|
||||
},
|
||||
"content": "Weet u zeker dat u deze API-sleutel wilt verwijderen?",
|
||||
"title": "Bevestig actie"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Acties",
|
||||
"expiresAt": "Vervaldatum",
|
||||
"key": "Sleutel",
|
||||
"lastUsedAt": "Laatst gebruikt",
|
||||
"name": "Naam",
|
||||
"status": "Status"
|
||||
},
|
||||
"title": "API-sleutellijst"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Inhoud mag niet leeg zijn"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Vorige maand",
|
||||
"recent30Days": "Laatste 30 dagen"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Woorden"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API-sleutelbeheer",
|
||||
"profile": "Profiel",
|
||||
"security": "Beveiliging",
|
||||
"stats": "Statistieken"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Verwijder de berichten en geüploade bestanden van de huidige sessie",
|
||||
"title": "Verwijder sessieberichten"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Open de applicatie-instellingenpagina",
|
||||
"title": "Applicatie-instellingen"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Toon of verberg het hoofdvenster met een globale sneltoets",
|
||||
"title": "Toon/verberg hoofdvenster"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Ga naar de bewerkingsmodus door Alt ingedrukt te houden en op het bericht te dubbelklikken",
|
||||
"title": "Bewerk bericht"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Bekijk de gebruiksaanwijzing voor alle sneltoetsen",
|
||||
"title": "Open sneltoets hulp"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Open de applicatie-instellingenpagina",
|
||||
"title": "Applicatie-instellingen"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Genereer het laatste bericht opnieuw",
|
||||
"title": "Genereer bericht opnieuw"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash is het meest kosteneffectieve model van Google en biedt uitgebreide functionaliteiten."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite is het kleinste en meest kosteneffectieve model van Google, speciaal ontworpen voor grootschalig gebruik."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview is het kleinste en meest kosteneffectieve model van Google, speciaal ontworpen voor grootschalig gebruik."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Conflict met bestaande sneltoetsen",
|
||||
"errors": {
|
||||
"CONFLICT": "Sneltoetsconflict: deze sneltoets wordt al door een andere functie gebruikt",
|
||||
"INVALID_FORMAT": "Ongeldig sneltoetsformaat: gebruik het juiste formaat (bijv. CommandOrControl+E)",
|
||||
"INVALID_ID": "Ongeldige sneltoets-ID",
|
||||
"NO_MODIFIER": "Sneltoets moet een modificatortoets bevatten (Ctrl, Alt, Shift, enz.)",
|
||||
"SYSTEM_OCCUPIED": "Sneltoets wordt al door het systeem of een andere applicatie gebruikt",
|
||||
"UNKNOWN": "Bijwerken mislukt: onbekende fout"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Gesprek",
|
||||
"desktop": "Desktop",
|
||||
"essential": "Essentieel"
|
||||
},
|
||||
"invalidCombination": "Sneltoets moet ten minste één modifier-toets (Ctrl, Alt, Shift) en één reguliere toets bevatten",
|
||||
"record": "Druk op een toets om de sneltoets op te nemen",
|
||||
"reset": "Reset naar standaard sneltoetsen",
|
||||
"title": "Sneltoetsen"
|
||||
"title": "Sneltoetsen",
|
||||
"updateError": "Sneltoets bijwerken mislukt: netwerk- of systeemfout",
|
||||
"updateSuccess": "Sneltoets succesvol bijgewerkt"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Uw sleutel en proxy-adres zullen worden versleuteld met het <1>AES-GCM</1> encryptie-algoritme",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Automatycznie wygenerowany",
|
||||
"copy": "Kopiuj",
|
||||
"copyError": "Kopiowanie nie powiodło się",
|
||||
"copySuccess": "Klucz API został skopiowany do schowka",
|
||||
"enterPlaceholder": "Wpisz",
|
||||
"hide": "Ukryj",
|
||||
"neverExpires": "Nigdy nie wygasa",
|
||||
"neverUsed": "Nigdy nie używany",
|
||||
"show": "Pokaż"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Data wygaśnięcia",
|
||||
"placeholder": "Nigdy nie wygasa"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nazwa",
|
||||
"placeholder": "Wpisz nazwę klucza API"
|
||||
}
|
||||
},
|
||||
"submit": "Utwórz",
|
||||
"title": "Utwórz klucz API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Utwórz klucz API",
|
||||
"delete": "Usuń",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Anuluj",
|
||||
"ok": "Potwierdź"
|
||||
},
|
||||
"content": "Czy na pewno chcesz usunąć ten klucz API?",
|
||||
"title": "Potwierdź operację"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Akcje",
|
||||
"expiresAt": "Data wygaśnięcia",
|
||||
"key": "Klucz",
|
||||
"lastUsedAt": "Ostatnie użycie",
|
||||
"name": "Nazwa",
|
||||
"status": "Status aktywacji"
|
||||
},
|
||||
"title": "Lista kluczy API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Pole nie może być puste"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Poprzedni miesiąc",
|
||||
"recent30Days": "Ostatnie 30 dni"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Słowa"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Zarządzanie kluczami API",
|
||||
"profile": "Profil",
|
||||
"security": "Bezpieczeństwo",
|
||||
"stats": "Statystyki"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Wyczyść wiadomości i przesłane pliki w bieżącej rozmowie",
|
||||
"title": "Wyczyść wiadomości rozmowy"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Otwórz stronę ustawień aplikacji",
|
||||
"title": "Ustawienia aplikacji"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Globalny skrót klawiszowy do wyświetlania lub ukrywania głównego okna",
|
||||
"title": "Pokaż/Ukryj główne okno"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Wejdź w tryb edycji, przytrzymując klawisz Alt i podwójnie klikając wiadomość",
|
||||
"title": "Edytuj wiadomość"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Zobacz wszystkie instrukcje dotyczące skrótów klawiszowych",
|
||||
"title": "Otwórz pomoc dotyczącą skrótów klawiszowych"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Otwórz stronę ustawień aplikacji",
|
||||
"title": "Ustawienia aplikacji"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Ponownie wygeneruj ostatnią wiadomość",
|
||||
"title": "Ponownie wygeneruj wiadomość"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash to najbardziej opłacalny model Google, oferujący wszechstronne funkcje."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite to najmniejszy i najbardziej opłacalny model Google, zaprojektowany z myślą o szerokim zastosowaniu."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview to najmniejszy i najbardziej opłacalny model Google, zaprojektowany z myślą o masowym zastosowaniu."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Kolizja z istniejącymi skrótami klawiszowymi",
|
||||
"errors": {
|
||||
"CONFLICT": "Konflikt skrótu klawiszowego: ten skrót jest już używany przez inną funkcję",
|
||||
"INVALID_FORMAT": "Nieprawidłowy format skrótu klawiszowego: użyj poprawnego formatu (np. CommandOrControl+E)",
|
||||
"INVALID_ID": "Nieprawidłowy identyfikator skrótu klawiszowego",
|
||||
"NO_MODIFIER": "Skrót klawiszowy musi zawierać klawisz modyfikujący (Ctrl, Alt, Shift itp.)",
|
||||
"SYSTEM_OCCUPIED": "Skrót klawiszowy jest zajęty przez system lub inną aplikację",
|
||||
"UNKNOWN": "Aktualizacja nie powiodła się: nieznany błąd"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Rozmowa",
|
||||
"desktop": "Pulpit",
|
||||
"essential": "Podstawowy"
|
||||
},
|
||||
"invalidCombination": "Skrót klawiszowy musi zawierać przynajmniej jeden klawisz modyfikujący (Ctrl, Alt, Shift) oraz jeden klawisz zwykły",
|
||||
"record": "Naciśnij klawisz, aby nagrać skrót klawiszowy",
|
||||
"reset": "Przywróć domyślne skróty klawiszowe",
|
||||
"title": "Skróty klawiszowe"
|
||||
"title": "Skróty klawiszowe",
|
||||
"updateError": "Aktualizacja skrótu klawiszowego nie powiodła się: błąd sieci lub systemu",
|
||||
"updateSuccess": "Skrót klawiszowy został pomyślnie zaktualizowany"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Twój klucz, adres proxy i inne będą szyfrowane za pomocą algorytmu szyfrowania <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Gerado automaticamente",
|
||||
"copy": "Copiar",
|
||||
"copyError": "Falha ao copiar",
|
||||
"copySuccess": "Chave API copiada para a área de transferência",
|
||||
"enterPlaceholder": "Por favor, insira",
|
||||
"hide": "Ocultar",
|
||||
"neverExpires": "Nunca expira",
|
||||
"neverUsed": "Nunca usado",
|
||||
"show": "Mostrar"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Data de expiração",
|
||||
"placeholder": "Nunca expira"
|
||||
},
|
||||
"name": {
|
||||
"label": "Nome",
|
||||
"placeholder": "Por favor, insira o nome da Chave API"
|
||||
}
|
||||
},
|
||||
"submit": "Criar",
|
||||
"title": "Criar Chave API"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Criar Chave API",
|
||||
"delete": "Excluir",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Confirmar"
|
||||
},
|
||||
"content": "Tem certeza de que deseja excluir esta Chave API?",
|
||||
"title": "Confirmar ação"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Ações",
|
||||
"expiresAt": "Data de expiração",
|
||||
"key": "Chave",
|
||||
"lastUsedAt": "Último uso",
|
||||
"name": "Nome",
|
||||
"status": "Status de ativação"
|
||||
},
|
||||
"title": "Lista de Chaves API"
|
||||
},
|
||||
"validation": {
|
||||
"required": "O conteúdo não pode estar vazio"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Último Mês",
|
||||
"recent30Days": "Últimos 30 Dias"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Palavras"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Gerenciamento de Chave API",
|
||||
"profile": "Perfil",
|
||||
"security": "Segurança",
|
||||
"stats": "Estatísticas"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Limpar as mensagens da conversa atual e os arquivos enviados",
|
||||
"title": "Limpar mensagens da conversa"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Abrir a página de configurações do aplicativo",
|
||||
"title": "Configurações do Aplicativo"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Atalho global para mostrar ou ocultar a janela principal",
|
||||
"title": "Mostrar/Ocultar Janela Principal"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Entre no modo de edição pressionando Alt e clicando duas vezes na mensagem",
|
||||
"title": "Editar mensagem"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Ver as instruções de uso de todos os atalhos",
|
||||
"title": "Abrir ajuda de atalhos"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Abra a página de configurações do aplicativo",
|
||||
"title": "Configurações do Aplicativo"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Regenerar a última mensagem",
|
||||
"title": "Regenerar mensagem"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash é o modelo com melhor custo-benefício do Google, oferecendo funcionalidades abrangentes."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite é o modelo mais compacto e com melhor custo-benefício do Google, projetado para uso em larga escala."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview é o modelo mais compacto e com melhor custo-benefício do Google, projetado para uso em larga escala."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Conflito com teclas de atalho existentes",
|
||||
"errors": {
|
||||
"CONFLICT": "Conflito de atalho: este atalho já está em uso por outra função",
|
||||
"INVALID_FORMAT": "Formato de atalho inválido: por favor, use o formato correto (ex: CommandOrControl+E)",
|
||||
"INVALID_ID": "ID de atalho inválido",
|
||||
"NO_MODIFIER": "O atalho deve incluir uma tecla modificadora (Ctrl, Alt, Shift, etc.)",
|
||||
"SYSTEM_OCCUPIED": "Atalho já está ocupado pelo sistema ou por outro aplicativo",
|
||||
"UNKNOWN": "Falha na atualização: erro desconhecido"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Conversa",
|
||||
"desktop": "Área de trabalho",
|
||||
"essential": "Essencial"
|
||||
},
|
||||
"invalidCombination": "A combinação de teclas de atalho deve incluir pelo menos uma tecla modificadora (Ctrl, Alt, Shift) e uma tecla comum",
|
||||
"record": "Pressione a tecla para gravar o atalho",
|
||||
"reset": "Redefinir para os atalhos padrão",
|
||||
"title": "Atalhos"
|
||||
"title": "Atalhos",
|
||||
"updateError": "Falha na atualização do atalho: erro de rede ou sistema",
|
||||
"updateSuccess": "Atalho atualizado com sucesso"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Suas chaves, endereço do agente, etc., serão criptografados usando o algoritmo de criptografia <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Автоматически сгенерировано",
|
||||
"copy": "Копировать",
|
||||
"copyError": "Ошибка копирования",
|
||||
"copySuccess": "API ключ скопирован в буфер обмена",
|
||||
"enterPlaceholder": "Пожалуйста, введите",
|
||||
"hide": "Скрыть",
|
||||
"neverExpires": "Никогда не истекает",
|
||||
"neverUsed": "Никогда не использовался",
|
||||
"show": "Показать"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Срок действия",
|
||||
"placeholder": "Никогда не истекает"
|
||||
},
|
||||
"name": {
|
||||
"label": "Название",
|
||||
"placeholder": "Пожалуйста, введите название API ключа"
|
||||
}
|
||||
},
|
||||
"submit": "Создать",
|
||||
"title": "Создать API ключ"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Создать API ключ",
|
||||
"delete": "Удалить",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Отмена",
|
||||
"ok": "Подтвердить"
|
||||
},
|
||||
"content": "Вы уверены, что хотите удалить этот API ключ?",
|
||||
"title": "Подтверждение действия"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Действия",
|
||||
"expiresAt": "Срок действия",
|
||||
"key": "Ключ",
|
||||
"lastUsedAt": "Последнее использование",
|
||||
"name": "Название",
|
||||
"status": "Статус активации"
|
||||
},
|
||||
"title": "Список API ключей"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Поле не может быть пустым"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Прошлый месяц",
|
||||
"recent30Days": "Последние 30 дней"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Слова"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Управление API ключами",
|
||||
"profile": "Профиль",
|
||||
"security": "Безопасность",
|
||||
"stats": "Статистика"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Очистить сообщения текущего сеанса и загруженные файлы",
|
||||
"title": "Очистить сообщения сеанса"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Открыть страницу настроек приложения",
|
||||
"title": "Настройки приложения"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Глобальная горячая клавиша для отображения или скрытия главного окна",
|
||||
"title": "Показать/Скрыть главное окно"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Войти в режим редактирования, удерживая Alt и дважды щелкнув по сообщению",
|
||||
"title": "Редактировать сообщение"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Просмотреть инструкции по использованию всех горячих клавиш",
|
||||
"title": "Открыть справку по горячим клавишам"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Открыть страницу настроек приложения",
|
||||
"title": "Настройки приложения"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Сгенерировать последнее сообщение заново",
|
||||
"title": "Перегенерировать сообщение"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash — самая экономичная модель Google, предоставляющая полный набор функций."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite — это самая компактная и экономичная модель от Google, разработанная для масштабного использования."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview — самая компактная и экономичная модель Google, разработанная для масштабного использования."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Конфликт с существующими горячими клавишами",
|
||||
"errors": {
|
||||
"CONFLICT": "Конфликт горячих клавиш: эта комбинация уже используется другой функцией",
|
||||
"INVALID_FORMAT": "Неверный формат горячей клавиши: используйте правильный формат (например, CommandOrControl+E)",
|
||||
"INVALID_ID": "Недопустимый идентификатор горячей клавиши",
|
||||
"NO_MODIFIER": "Горячая клавиша должна содержать модификатор (Ctrl, Alt, Shift и т.д.)",
|
||||
"SYSTEM_OCCUPIED": "Горячая клавиша занята системой или другим приложением",
|
||||
"UNKNOWN": "Ошибка обновления: неизвестная ошибка"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Беседа",
|
||||
"desktop": "Настольная версия",
|
||||
"essential": "Основной"
|
||||
},
|
||||
"invalidCombination": "Горячая клавиша должна содержать как минимум одну модификаторную клавишу (Ctrl, Alt, Shift) и одну обычную клавишу",
|
||||
"record": "Нажмите клавишу для записи горячей клавиши",
|
||||
"reset": "Сбросить на стандартные горячие клавиши",
|
||||
"title": "Горячие клавиши"
|
||||
"title": "Горячие клавиши",
|
||||
"updateError": "Ошибка обновления горячих клавиш: сетевая или системная ошибка",
|
||||
"updateSuccess": "Горячие клавиши успешно обновлены"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Ваши ключи и адреса агентов будут зашифрованы с использованием алгоритма шифрования <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Otomatik Oluşturuldu",
|
||||
"copy": "Kopyala",
|
||||
"copyError": "Kopyalama Başarısız",
|
||||
"copySuccess": "API Anahtarı panoya kopyalandı",
|
||||
"enterPlaceholder": "Lütfen giriniz",
|
||||
"hide": "Gizle",
|
||||
"neverExpires": "Asla Süresi Dolmaz",
|
||||
"neverUsed": "Hiç Kullanılmadı",
|
||||
"show": "Göster"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Son Kullanma Tarihi",
|
||||
"placeholder": "Asla Süresi Dolmaz"
|
||||
},
|
||||
"name": {
|
||||
"label": "Ad",
|
||||
"placeholder": "Lütfen API Anahtarı adını giriniz"
|
||||
}
|
||||
},
|
||||
"submit": "Oluştur",
|
||||
"title": "API Anahtarı Oluştur"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "API Anahtarı Oluştur",
|
||||
"delete": "Sil",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "İptal",
|
||||
"ok": "Onayla"
|
||||
},
|
||||
"content": "Bu API Anahtarını silmek istediğinize emin misiniz?",
|
||||
"title": "İşlemi Onayla"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "İşlemler",
|
||||
"expiresAt": "Son Kullanma Tarihi",
|
||||
"key": "Anahtar",
|
||||
"lastUsedAt": "Son Kullanım Tarihi",
|
||||
"name": "Ad",
|
||||
"status": "Etkinlik Durumu"
|
||||
},
|
||||
"title": "API Anahtarı Listesi"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Bu alan boş bırakılamaz"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Geçen Ay",
|
||||
"recent30Days": "Son 30 Gün"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Toplam kelime sayısı"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Anahtarı Yönetimi",
|
||||
"profile": "Profil",
|
||||
"security": "Güvenlik",
|
||||
"stats": "İstatistikler"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Geçerli oturumun mesajlarını ve yüklenen dosyaları temizle",
|
||||
"title": "Oturum mesajlarını temizle"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Uygulama ayarları sayfasını aç",
|
||||
"title": "Uygulama Ayarları"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Küresel kısayol tuşu ile ana pencereyi göster veya gizle",
|
||||
"title": "Ana Pencereyi Göster/Gizle"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Mesaja çift tıklayıp Alt tuşuna basarak düzenleme moduna geçin",
|
||||
"title": "Mesajı düzenle"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Tüm kısayol tuşlarının kullanım talimatlarını görüntüle",
|
||||
"title": "Kısayol Yardımını Aç"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Uygulama ayarları sayfasını aç",
|
||||
"title": "Uygulama Ayarları"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Son mesajı yeniden oluştur",
|
||||
"title": "Mesajı yeniden oluştur"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash, Google'ın en yüksek maliyet-performans modelidir ve kapsamlı özellikler sunar."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite, Google'ın en küçük ve en uygun maliyetli modeli olup, geniş çaplı kullanım için tasarlanmıştır."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Önizlemesi, Google'ın en küçük ve en yüksek maliyet-performans modelidir ve büyük ölçekli kullanım için tasarlanmıştır."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Mevcut kısayol tuşlarıyla çakışıyor",
|
||||
"errors": {
|
||||
"CONFLICT": "Kısayol çakışması: Bu kısayol başka bir işlev tarafından kullanılıyor",
|
||||
"INVALID_FORMAT": "Geçersiz kısayol formatı: Lütfen doğru formatı kullanın (örneğin CommandOrControl+E)",
|
||||
"INVALID_ID": "Geçersiz kısayol kimliği",
|
||||
"NO_MODIFIER": "Kısayol en az bir değiştirici tuş içermelidir (Ctrl, Alt, Shift vb.)",
|
||||
"SYSTEM_OCCUPIED": "Kısayol sistem veya başka bir uygulama tarafından kullanılıyor",
|
||||
"UNKNOWN": "Güncelleme başarısız: Bilinmeyen hata"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Sohbet",
|
||||
"desktop": "Masaüstü",
|
||||
"essential": "Temel"
|
||||
},
|
||||
"invalidCombination": "Kısayol tuşu en az bir modifiye tuşu (Ctrl, Alt, Shift) ve bir normal tuş içermelidir",
|
||||
"record": "Kısayol tuşunu kaydetmek için tuşa basın",
|
||||
"reset": "Varsayılan kısayol tuşlarına sıfırla",
|
||||
"title": "Kısayollar"
|
||||
"title": "Kısayollar",
|
||||
"updateError": "Kısayol güncelleme başarısız: Ağ veya sistem hatası",
|
||||
"updateSuccess": "Kısayol başarıyla güncellendi"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Anahtarınız ve vekil adresiniz <1>AES-GCM</1> şifreleme algoritması kullanılarak şifrelenecektir",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "Tự động tạo",
|
||||
"copy": "Sao chép",
|
||||
"copyError": "Sao chép thất bại",
|
||||
"copySuccess": "API Key đã được sao chép vào bộ nhớ tạm",
|
||||
"enterPlaceholder": "Vui lòng nhập",
|
||||
"hide": "Ẩn",
|
||||
"neverExpires": "Không bao giờ hết hạn",
|
||||
"neverUsed": "Chưa từng sử dụng",
|
||||
"show": "Hiển thị"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "Thời gian hết hạn",
|
||||
"placeholder": "Không bao giờ hết hạn"
|
||||
},
|
||||
"name": {
|
||||
"label": "Tên",
|
||||
"placeholder": "Vui lòng nhập tên API Key"
|
||||
}
|
||||
},
|
||||
"submit": "Tạo",
|
||||
"title": "Tạo API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "Tạo API Key",
|
||||
"delete": "Xóa",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "Hủy",
|
||||
"ok": "Xác nhận"
|
||||
},
|
||||
"content": "Bạn có chắc chắn muốn xóa API Key này không?",
|
||||
"title": "Xác nhận thao tác"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "Thao tác",
|
||||
"expiresAt": "Thời gian hết hạn",
|
||||
"key": "Khóa",
|
||||
"lastUsedAt": "Lần sử dụng cuối",
|
||||
"name": "Tên",
|
||||
"status": "Trạng thái kích hoạt"
|
||||
},
|
||||
"title": "Danh sách API Key"
|
||||
},
|
||||
"validation": {
|
||||
"required": "Nội dung không được để trống"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "Tháng trước",
|
||||
"recent30Days": "30 ngày qua"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "Từ"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "Quản lý API Key",
|
||||
"profile": "Hồ sơ",
|
||||
"security": "Bảo mật",
|
||||
"stats": "Thống kê"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "Xóa tất cả tin nhắn và tệp đã tải lên trong cuộc trò chuyện hiện tại",
|
||||
"title": "Xóa tin nhắn cuộc trò chuyện"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "Mở trang cài đặt ứng dụng",
|
||||
"title": "Cài đặt ứng dụng"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "Phím tắt toàn cục để hiển thị hoặc ẩn cửa sổ chính",
|
||||
"title": "Hiển thị/Ẩn cửa sổ chính"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "Vào chế độ chỉnh sửa bằng cách giữ phím Alt và nhấp đúp vào tin nhắn",
|
||||
"title": "Chỉnh sửa tin nhắn"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "Xem hướng dẫn sử dụng tất cả các phím tắt",
|
||||
"title": "Mở trợ giúp phím tắt"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "Mở trang cài đặt ứng dụng",
|
||||
"title": "Cài đặt ứng dụng"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "Tạo lại tin nhắn cuối cùng",
|
||||
"title": "Tạo lại tin nhắn"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash là mô hình có hiệu suất chi phí tốt nhất của Google, cung cấp đầy đủ các chức năng."
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite là mô hình nhỏ nhất và có hiệu suất chi phí tốt nhất của Google, được thiết kế dành cho việc sử dụng quy mô lớn."
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview là mô hình nhỏ nhất và có hiệu suất chi phí tốt nhất của Google, được thiết kế dành cho sử dụng quy mô lớn."
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "Xung đột với phím tắt hiện có",
|
||||
"errors": {
|
||||
"CONFLICT": "Phím tắt bị trùng: Phím tắt này đã được chức năng khác sử dụng",
|
||||
"INVALID_FORMAT": "Định dạng phím tắt không hợp lệ: Vui lòng sử dụng định dạng đúng (ví dụ CommandOrControl+E)",
|
||||
"INVALID_ID": "ID phím tắt không hợp lệ",
|
||||
"NO_MODIFIER": "Phím tắt phải bao gồm phím điều khiển (Ctrl, Alt, Shift, v.v.)",
|
||||
"SYSTEM_OCCUPIED": "Phím tắt đã bị hệ thống hoặc ứng dụng khác chiếm dụng",
|
||||
"UNKNOWN": "Cập nhật thất bại: Lỗi không xác định"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "Cuộc trò chuyện",
|
||||
"desktop": "Phiên bản máy tính để bàn",
|
||||
"essential": "Cơ bản"
|
||||
},
|
||||
"invalidCombination": "Phím tắt cần ít nhất một phím sửa đổi (Ctrl, Alt, Shift) và một phím thông thường",
|
||||
"record": "Nhấn phím để ghi lại phím tắt",
|
||||
"reset": "Đặt lại thành phím tắt mặc định",
|
||||
"title": "Phím tắt"
|
||||
"title": "Phím tắt",
|
||||
"updateError": "Cập nhật phím tắt thất bại: Lỗi mạng hoặc hệ thống",
|
||||
"updateSuccess": "Cập nhật phím tắt thành công"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "Khóa và địa chỉ proxy của bạn sẽ được mã hóa bằng thuật toán mã hóa <1>AES-GCM</1>",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自动生成",
|
||||
"copy": "复制",
|
||||
"copyError": "复制失败",
|
||||
"copySuccess": "API Key 已复制到剪贴板",
|
||||
"enterPlaceholder": "请输入",
|
||||
"hide": "隐藏",
|
||||
"neverExpires": "永不过期",
|
||||
"neverUsed": "从未使用",
|
||||
"show": "显示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "过期时间",
|
||||
"placeholder": "永不过期"
|
||||
},
|
||||
"name": {
|
||||
"label": "名称",
|
||||
"placeholder": "请输入 API Key 名称"
|
||||
}
|
||||
},
|
||||
"submit": "创建",
|
||||
"title": "创建 API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "创建 API Key",
|
||||
"delete": "删除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"ok": "确认"
|
||||
},
|
||||
"content": "确认删除该 API Key 吗?",
|
||||
"title": "确认操作"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "过期时间",
|
||||
"key": "密钥",
|
||||
"lastUsedAt": "最后使用时间",
|
||||
"name": "名称",
|
||||
"status": "启用状态"
|
||||
},
|
||||
"title": "API Key 列表"
|
||||
},
|
||||
"validation": {
|
||||
"required": "内容不得为空"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "上个月",
|
||||
"recent30Days": "最近30天"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "累计字数"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key 管理",
|
||||
"profile": "个人资料",
|
||||
"security": "安全",
|
||||
"stats": "数据统计"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "清空当前会话的消息和上传的文件",
|
||||
"title": "清空会话消息"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "打开应用设置页面",
|
||||
"title": "应用设置"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "全局快捷键显示或隐藏主窗口",
|
||||
"title": "显示/隐藏主窗口"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "通过按住 Alt 并双击消息进入编辑模式",
|
||||
"title": "编辑消息"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "查看所有快捷键的使用说明",
|
||||
"title": "打开快捷键帮助"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "打开应用设置页面",
|
||||
"title": "应用设置"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "重新生成最后一条消息",
|
||||
"title": "重新生成消息"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash 是 Google 性价比最高的模型,提供全面的功能。"
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite 是 Google 最小、性价比最高的模型,专为大规模使用而设计。"
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview 是 Google 最小、性价比最高的模型,专为大规模使用而设计。"
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "与现有快捷键冲突",
|
||||
"errors": {
|
||||
"CONFLICT": "快捷键冲突:该快捷键已被其他功能占用",
|
||||
"INVALID_FORMAT": "快捷键格式无效:请使用正确的格式(如 CommandOrControl+E)",
|
||||
"INVALID_ID": "无效的快捷键ID",
|
||||
"NO_MODIFIER": "快捷键必须包含修饰键(Ctrl、Alt、Shift等)",
|
||||
"SYSTEM_OCCUPIED": "快捷键已被系统或其他应用程序占用",
|
||||
"UNKNOWN": "更新失败:未知错误"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "会话",
|
||||
"desktop": "桌面端",
|
||||
"essential": "基础"
|
||||
},
|
||||
"invalidCombination": "快捷键需要至少包含一个修饰键 (Ctrl, Alt, Shift) 和一个常规键",
|
||||
"record": "按下按键以录制快捷键",
|
||||
"reset": "重置为默认快捷键",
|
||||
"title": "快捷键"
|
||||
"title": "快捷键",
|
||||
"updateError": "快捷键更新失败:网络或系统错误",
|
||||
"updateSuccess": "快捷键更新成功"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "您的秘钥与代理地址等将使用 <1>AES-GCM</1> 加密算法进行加密",
|
||||
@@ -524,6 +535,7 @@
|
||||
"experiment": "实验",
|
||||
"hotkey": "快捷键",
|
||||
"llm": "语言模型",
|
||||
"plugin": "插件管理",
|
||||
"provider": "AI 服务商",
|
||||
"proxy": "网络代理",
|
||||
"storage": "数据存储",
|
||||
|
||||
@@ -1,4 +1,57 @@
|
||||
{
|
||||
"apikey": {
|
||||
"display": {
|
||||
"autoGenerated": "自動生成",
|
||||
"copy": "複製",
|
||||
"copyError": "複製失敗",
|
||||
"copySuccess": "API Key 已複製到剪貼簿",
|
||||
"enterPlaceholder": "請輸入",
|
||||
"hide": "隱藏",
|
||||
"neverExpires": "永不過期",
|
||||
"neverUsed": "從未使用",
|
||||
"show": "顯示"
|
||||
},
|
||||
"form": {
|
||||
"fields": {
|
||||
"expiresAt": {
|
||||
"label": "過期時間",
|
||||
"placeholder": "永不過期"
|
||||
},
|
||||
"name": {
|
||||
"label": "名稱",
|
||||
"placeholder": "請輸入 API Key 名稱"
|
||||
}
|
||||
},
|
||||
"submit": "建立",
|
||||
"title": "建立 API Key"
|
||||
},
|
||||
"list": {
|
||||
"actions": {
|
||||
"create": "建立 API Key",
|
||||
"delete": "刪除",
|
||||
"deleteConfirm": {
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"ok": "確認"
|
||||
},
|
||||
"content": "確認刪除該 API Key 嗎?",
|
||||
"title": "確認操作"
|
||||
}
|
||||
},
|
||||
"columns": {
|
||||
"actions": "操作",
|
||||
"expiresAt": "過期時間",
|
||||
"key": "密鑰",
|
||||
"lastUsedAt": "最後使用時間",
|
||||
"name": "名稱",
|
||||
"status": "啟用狀態"
|
||||
},
|
||||
"title": "API Key 列表"
|
||||
},
|
||||
"validation": {
|
||||
"required": "內容不得為空"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"prevMonth": "上個月",
|
||||
"recent30Days": "最近30天"
|
||||
@@ -89,6 +142,7 @@
|
||||
"words": "總字數"
|
||||
},
|
||||
"tab": {
|
||||
"apikey": "API Key 管理",
|
||||
"profile": "個人資料",
|
||||
"security": "安全",
|
||||
"stats": "數據統計"
|
||||
|
||||
@@ -7,6 +7,16 @@
|
||||
"desc": "清空當前會話的消息和上傳的檔案",
|
||||
"title": "清空會話消息"
|
||||
},
|
||||
"desktop": {
|
||||
"openSettings": {
|
||||
"desc": "打開應用設定頁面",
|
||||
"title": "應用設定"
|
||||
},
|
||||
"showApp": {
|
||||
"desc": "全域快速鍵顯示或隱藏主視窗",
|
||||
"title": "顯示/隱藏主視窗"
|
||||
}
|
||||
},
|
||||
"editMessage": {
|
||||
"desc": "通過按住 Alt 並雙擊消息進入編輯模式",
|
||||
"title": "編輯消息"
|
||||
@@ -19,10 +29,6 @@
|
||||
"desc": "查看所有快捷鍵的使用說明",
|
||||
"title": "打開快捷鍵幫助"
|
||||
},
|
||||
"openSettings": {
|
||||
"desc": "打開應用設定頁面",
|
||||
"title": "應用設定"
|
||||
},
|
||||
"regenerateMessage": {
|
||||
"desc": "重新生成最後一條消息",
|
||||
"title": "重新生成消息"
|
||||
|
||||
@@ -1100,6 +1100,9 @@
|
||||
"gemini-2.5-flash": {
|
||||
"description": "Gemini 2.5 Flash 是 Google 性價比最高的模型,提供全面的功能。"
|
||||
},
|
||||
"gemini-2.5-flash-lite": {
|
||||
"description": "Gemini 2.5 Flash-Lite 是 Google 最小、性價比最高的模型,專為大規模使用而設計。"
|
||||
},
|
||||
"gemini-2.5-flash-lite-preview-06-17": {
|
||||
"description": "Gemini 2.5 Flash-Lite Preview 是 Google 最小、性價比最高的模型,專為大規模使用而設計。"
|
||||
},
|
||||
|
||||
@@ -45,14 +45,25 @@
|
||||
},
|
||||
"hotkey": {
|
||||
"conflicts": "與現有快捷鍵衝突",
|
||||
"errors": {
|
||||
"CONFLICT": "快速鍵衝突:該快速鍵已被其他功能佔用",
|
||||
"INVALID_FORMAT": "快速鍵格式無效:請使用正確的格式(如 CommandOrControl+E)",
|
||||
"INVALID_ID": "無效的快速鍵ID",
|
||||
"NO_MODIFIER": "快速鍵必須包含修飾鍵(Ctrl、Alt、Shift等)",
|
||||
"SYSTEM_OCCUPIED": "快速鍵已被系統或其他應用程式佔用",
|
||||
"UNKNOWN": "更新失敗:未知錯誤"
|
||||
},
|
||||
"group": {
|
||||
"conversation": "對話",
|
||||
"desktop": "桌面端",
|
||||
"essential": "基本"
|
||||
},
|
||||
"invalidCombination": "快捷鍵需要至少包含一個修飾鍵 (Ctrl, Alt, Shift) 和一個常規鍵",
|
||||
"record": "按下按鍵以錄製快捷鍵",
|
||||
"reset": "重置為預設快捷鍵",
|
||||
"title": "快速鍵"
|
||||
"title": "快速鍵",
|
||||
"updateError": "快速鍵更新失敗:網路或系統錯誤",
|
||||
"updateSuccess": "快速鍵更新成功"
|
||||
},
|
||||
"llm": {
|
||||
"aesGcm": "您的金鑰與代理地址等將使用 <1>AES-GCM</1> 加密演算法進行加密",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/chat",
|
||||
"version": "1.103.2",
|
||||
"version": "1.105.1",
|
||||
"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.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
@@ -153,7 +153,7 @@
|
||||
"@lobehub/icons": "^2.17.0",
|
||||
"@lobehub/market-sdk": "^0.22.7",
|
||||
"@lobehub/tts": "^2.0.1",
|
||||
"@lobehub/ui": "^2.7.4",
|
||||
"@lobehub/ui": "^2.7.5",
|
||||
"@modelcontextprotocol/sdk": "^1.16.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@next/third-parties": "^15.4.3",
|
||||
@@ -196,7 +196,7 @@
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"immer": "^10.1.1",
|
||||
"jose": "^5.10.0",
|
||||
"jose": "^6.0.12",
|
||||
"js-sha256": "^0.11.1",
|
||||
"jsonl-parse-stringify": "^1.0.3",
|
||||
"keyv": "^4.5.4",
|
||||
@@ -241,7 +241,7 @@
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hotkeys-hook": "^5.1.0",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-layout-kit": "^1.9.2",
|
||||
"react-layout-kit": "^2.0.0",
|
||||
"react-lazy-load": "^4.0.1",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-rnd": "^10.5.2",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ShortcutUpdateResult } from '../types';
|
||||
|
||||
export interface ShortcutDispatchEvents {
|
||||
getShortcutsConfig: () => Record<string, string>;
|
||||
updateShortcutConfig: (id: string, accelerator: string) => boolean;
|
||||
updateShortcutConfig: (params: { accelerator: string; id: string }) => ShortcutUpdateResult;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,14 @@ export interface ShortcutConfig {
|
||||
id: string;
|
||||
}
|
||||
export type ShortcutActionType = Record<string, any>;
|
||||
|
||||
export interface ShortcutUpdateResult {
|
||||
errorType?:
|
||||
| 'INVALID_ID'
|
||||
| 'INVALID_FORMAT'
|
||||
| 'NO_MODIFIER'
|
||||
| 'CONFLICT'
|
||||
| 'SYSTEM_OCCUPIED'
|
||||
| 'UNKNOWN';
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AgentRuntimeError } from '@/libs/model-runtime';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { RequestHandler, checkAuth } from './index';
|
||||
import { checkAuthMethod } from './utils';
|
||||
@@ -20,8 +20,8 @@ vi.mock('./utils', () => ({
|
||||
checkAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn(),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('checkAuth', () => {
|
||||
@@ -50,7 +50,7 @@ describe('checkAuth', () => {
|
||||
it('should return error response on getJWTPayload error', async () => {
|
||||
const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
mockRequest.headers.set('Authorization', 'invalid');
|
||||
vi.mocked(getJWTPayload).mockRejectedValueOnce(mockError);
|
||||
vi.mocked(getXorPayload).mockRejectedValueOnce(mockError);
|
||||
|
||||
await checkAuth(mockHandler)(mockRequest, mockOptions);
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('checkAuth', () => {
|
||||
it('should return error response on checkAuthMethod error', async () => {
|
||||
const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
mockRequest.headers.set('Authorization', 'valid');
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({});
|
||||
vi.mocked(getXorPayload).mockResolvedValueOnce({});
|
||||
vi.mocked(checkAuthMethod).mockImplementationOnce(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AuthObject } from '@clerk/backend';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
JWTPayload,
|
||||
ClientSecretPayload,
|
||||
LOBE_CHAT_AUTH_HEADER,
|
||||
LOBE_CHAT_OIDC_AUTH_HEADER,
|
||||
OAUTH_AUTHORIZED,
|
||||
@@ -13,18 +13,18 @@ import { AgentRuntime, AgentRuntimeError, ChatCompletionErrorPayload } from '@/l
|
||||
import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { checkAuthMethod } from './utils';
|
||||
|
||||
type CreateRuntime = (jwtPayload: JWTPayload) => AgentRuntime;
|
||||
type CreateRuntime = (jwtPayload: ClientSecretPayload) => AgentRuntime;
|
||||
type RequestOptions = { createRuntime?: CreateRuntime; params: Promise<{ provider: string }> };
|
||||
|
||||
export type RequestHandler = (
|
||||
req: Request,
|
||||
options: RequestOptions & {
|
||||
createRuntime?: CreateRuntime;
|
||||
jwtPayload: JWTPayload;
|
||||
jwtPayload: ClientSecretPayload;
|
||||
},
|
||||
) => Promise<Response>;
|
||||
|
||||
@@ -36,7 +36,7 @@ export const checkAuth =
|
||||
return handler(req, { ...options, jwtPayload: { userId: 'DEV_USER' } });
|
||||
}
|
||||
|
||||
let jwtPayload: JWTPayload;
|
||||
let jwtPayload: ClientSecretPayload;
|
||||
|
||||
try {
|
||||
// get Authorization from header
|
||||
@@ -55,7 +55,7 @@ export const checkAuth =
|
||||
clerkAuth = data.clerkAuth;
|
||||
}
|
||||
|
||||
jwtPayload = await getJWTPayload(authorization);
|
||||
jwtPayload = getXorPayload(authorization);
|
||||
|
||||
const oidcAuthorization = req.headers.get(LOBE_CHAT_OIDC_AUTH_HEADER);
|
||||
let isUseOidcAuth = false;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { checkAuthMethod } from '@/app/(backend)/middleware/auth/utils';
|
||||
import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
|
||||
import { AgentRuntime, LobeRuntimeAI } from '@/libs/model-runtime';
|
||||
import { ChatErrorType } from '@/types/fetch';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
|
||||
import { POST } from './route';
|
||||
|
||||
@@ -18,8 +18,8 @@ vi.mock('@/app/(backend)/middleware/auth/utils', () => ({
|
||||
checkAuthMethod: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/server/jwt', () => ({
|
||||
getJWTPayload: vi.fn(),
|
||||
vi.mock('@/utils/server/xor', () => ({
|
||||
getXorPayload: vi.fn(),
|
||||
}));
|
||||
|
||||
// 定义一个变量来存储 enableAuth 的值
|
||||
@@ -61,7 +61,7 @@ describe('POST handler', () => {
|
||||
const mockParams = Promise.resolve({ provider: 'test-provider' });
|
||||
|
||||
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -78,7 +78,7 @@ describe('POST handler', () => {
|
||||
await POST(request as unknown as Request, { params: mockParams });
|
||||
|
||||
// 验证是否正确调用了模拟函数
|
||||
expect(getJWTPayload).toHaveBeenCalledWith('Bearer some-valid-token');
|
||||
expect(getXorPayload).toHaveBeenCalledWith('Bearer some-valid-token');
|
||||
expect(spy).toHaveBeenCalledWith('test-provider', expect.anything());
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('POST handler', () => {
|
||||
it('should have pass clerk Auth when enable clerk', async () => {
|
||||
enableClerk = true;
|
||||
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -142,7 +142,9 @@ describe('POST handler', () => {
|
||||
|
||||
it('should return InternalServerError error when throw a unknown error', async () => {
|
||||
const mockParams = Promise.resolve({ provider: 'test-provider' });
|
||||
vi.mocked(getJWTPayload).mockRejectedValueOnce(new Error('unknown error'));
|
||||
vi.mocked(getXorPayload).mockImplementationOnce(() => {
|
||||
throw new Error('unknown error');
|
||||
});
|
||||
|
||||
const response = await POST(request, { params: mockParams });
|
||||
|
||||
@@ -159,7 +161,7 @@ describe('POST handler', () => {
|
||||
|
||||
describe('chat', () => {
|
||||
it('should correctly handle chat completion with valid payload', async () => {
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
@@ -189,7 +191,7 @@ describe('POST handler', () => {
|
||||
|
||||
it('should return an error response when chat completion fails', async () => {
|
||||
// 设置 getJWTPayload 和 initAgentRuntimeWithUserPayload 的模拟返回值
|
||||
vi.mocked(getJWTPayload).mockResolvedValueOnce({
|
||||
vi.mocked(getXorPayload).mockReturnValueOnce({
|
||||
accessCode: 'test-access-code',
|
||||
apiKey: 'test-api-key',
|
||||
azureApiVersion: 'v1',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AgentRuntimeError } from '@/libs/model-runtime';
|
||||
import { TraceClient } from '@/libs/traces';
|
||||
import { ChatErrorType, ErrorType } from '@/types/fetch';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { getJWTPayload } from '@/utils/server/jwt';
|
||||
import { getXorPayload } from '@/utils/server/xor';
|
||||
import { getTracePayload } from '@/utils/trace';
|
||||
|
||||
import { parserPluginSettings } from './settings';
|
||||
@@ -44,7 +44,7 @@ export const POST = async (req: Request) => {
|
||||
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
|
||||
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
||||
const payload = await getJWTPayload(authorization);
|
||||
const payload = getXorPayload(authorization);
|
||||
|
||||
const result = checkAuth(payload.accessCode!, oauthAuthorized);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { AsyncTaskErrorType } from '@/types/asyncTask';
|
||||
import { GenerationBatch } from '@/types/generation';
|
||||
|
||||
import { GenerationItem } from './GenerationItem';
|
||||
import { DEFAULT_MAX_ITEM_WIDTH } from './GenerationItem/utils';
|
||||
import { ReferenceImages } from './ReferenceImages';
|
||||
|
||||
const useStyles = createStyles(({ cx, css, token }) => ({
|
||||
@@ -182,7 +183,11 @@ export const GenerationBatchItem = memo<GenerationBatchItemProps>(({ batch }) =>
|
||||
{promptAndMetadata}
|
||||
</>
|
||||
)}
|
||||
<Grid maxItemWidth={200} ref={imageGridRef} rows={batch.generations.length || 4}>
|
||||
<Grid
|
||||
maxItemWidth={DEFAULT_MAX_ITEM_WIDTH}
|
||||
ref={imageGridRef}
|
||||
rows={batch.generations.length}
|
||||
>
|
||||
{batch.generations.map((generation) => (
|
||||
<GenerationItem
|
||||
generation={generation}
|
||||
|
||||
+3
-2
@@ -9,10 +9,11 @@ import { Center } from 'react-layout-kit';
|
||||
import { ActionButtons } from './ActionButtons';
|
||||
import { useStyles } from './styles';
|
||||
import { ErrorStateProps } from './types';
|
||||
import { getThumbnailMaxWidth } from './utils';
|
||||
|
||||
// 错误状态组件
|
||||
export const ErrorState = memo<ErrorStateProps>(
|
||||
({ generation, aspectRatio, onDelete, onCopyError }) => {
|
||||
({ generation, generationBatch, aspectRatio, onDelete, onCopyError }) => {
|
||||
const { styles, theme } = useStyles();
|
||||
const { t } = useTranslation('image');
|
||||
|
||||
@@ -32,7 +33,7 @@ export const ErrorState = memo<ErrorStateProps>(
|
||||
style={{
|
||||
aspectRatio,
|
||||
cursor: 'pointer',
|
||||
maxWidth: generation.asset?.width ? generation.asset.width / 2 : 'unset',
|
||||
maxWidth: getThumbnailMaxWidth(generation, generationBatch),
|
||||
}}
|
||||
variant={'filled'}
|
||||
>
|
||||
|
||||
+27
-24
@@ -12,33 +12,36 @@ import { ActionButtons } from './ActionButtons';
|
||||
import { ElapsedTime } from './ElapsedTime';
|
||||
import { useStyles } from './styles';
|
||||
import { LoadingStateProps } from './types';
|
||||
import { getThumbnailMaxWidth } from './utils';
|
||||
|
||||
// 加载状态组件
|
||||
export const LoadingState = memo<LoadingStateProps>(({ generation, aspectRatio, onDelete }) => {
|
||||
const { styles } = useStyles();
|
||||
export const LoadingState = memo<LoadingStateProps>(
|
||||
({ generation, generationBatch, aspectRatio, onDelete }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
const isGenerating =
|
||||
generation.task.status === AsyncTaskStatus.Processing ||
|
||||
generation.task.status === AsyncTaskStatus.Pending;
|
||||
const isGenerating =
|
||||
generation.task.status === AsyncTaskStatus.Processing ||
|
||||
generation.task.status === AsyncTaskStatus.Pending;
|
||||
|
||||
return (
|
||||
<Block
|
||||
align={'center'}
|
||||
className={styles.placeholderContainer}
|
||||
justify={'center'}
|
||||
style={{
|
||||
aspectRatio,
|
||||
maxWidth: generation.asset?.width ? generation.asset.width / 2 : 'unset',
|
||||
}}
|
||||
variant={'filled'}
|
||||
>
|
||||
<Center gap={8}>
|
||||
<Spin indicator={<LoadingOutlined spin />} />
|
||||
<ElapsedTime generationId={generation.id} isActive={isGenerating} />
|
||||
</Center>
|
||||
<ActionButtons onDelete={onDelete} />
|
||||
</Block>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Block
|
||||
align={'center'}
|
||||
className={styles.placeholderContainer}
|
||||
justify={'center'}
|
||||
style={{
|
||||
aspectRatio,
|
||||
maxWidth: getThumbnailMaxWidth(generation, generationBatch),
|
||||
}}
|
||||
variant={'filled'}
|
||||
>
|
||||
<Center gap={8}>
|
||||
<Spin indicator={<LoadingOutlined spin />} />
|
||||
<ElapsedTime generationId={generation.id} isActive={isGenerating} />
|
||||
</Center>
|
||||
<ActionButtons onDelete={onDelete} />
|
||||
</Block>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
LoadingState.displayName = 'LoadingState';
|
||||
|
||||
+14
-3
@@ -8,10 +8,20 @@ import ImageItem from '@/components/ImageItem';
|
||||
import { ActionButtons } from './ActionButtons';
|
||||
import { useStyles } from './styles';
|
||||
import { SuccessStateProps } from './types';
|
||||
import { getThumbnailMaxWidth } from './utils';
|
||||
|
||||
// 成功状态组件
|
||||
export const SuccessState = memo<SuccessStateProps>(
|
||||
({ generation, prompt, aspectRatio, onDelete, onDownload, onCopySeed, seedTooltip }) => {
|
||||
({
|
||||
generation,
|
||||
generationBatch,
|
||||
prompt,
|
||||
aspectRatio,
|
||||
onDelete,
|
||||
onDownload,
|
||||
onCopySeed,
|
||||
seedTooltip,
|
||||
}) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
@@ -21,7 +31,7 @@ export const SuccessState = memo<SuccessStateProps>(
|
||||
justify={'center'}
|
||||
style={{
|
||||
aspectRatio,
|
||||
maxWidth: generation.asset?.width ? generation.asset.width / 2 : 'unset',
|
||||
maxWidth: getThumbnailMaxWidth(generation, generationBatch),
|
||||
}}
|
||||
variant={'filled'}
|
||||
>
|
||||
@@ -31,7 +41,8 @@ export const SuccessState = memo<SuccessStateProps>(
|
||||
src: generation.asset!.url,
|
||||
}}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
url={generation.asset!.thumbnailUrl}
|
||||
// Thumbnail quality is too bad
|
||||
url={generation.asset!.url}
|
||||
/>
|
||||
<ActionButtons
|
||||
onCopySeed={onCopySeed}
|
||||
|
||||
@@ -37,13 +37,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
||||
const shouldPoll = !isFinalized;
|
||||
useCheckGenerationStatus(generation.id, generation.task.id, activeTopicId!, shouldPoll);
|
||||
|
||||
const aspectRatio = getAspectRatio(
|
||||
generation.asset ?? {
|
||||
height: generationBatch.config?.height,
|
||||
type: 'image',
|
||||
width: generationBatch.config?.width,
|
||||
},
|
||||
);
|
||||
const aspectRatio = getAspectRatio(generation, generationBatch);
|
||||
|
||||
// 事件处理函数
|
||||
const handleDeleteGeneration = async () => {
|
||||
@@ -120,6 +114,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
||||
<SuccessState
|
||||
aspectRatio={aspectRatio}
|
||||
generation={generation}
|
||||
generationBatch={generationBatch}
|
||||
onCopySeed={handleCopySeed}
|
||||
onDelete={handleDeleteGeneration}
|
||||
onDownload={handleDownloadImage}
|
||||
@@ -134,6 +129,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
||||
<ErrorState
|
||||
aspectRatio={aspectRatio}
|
||||
generation={generation}
|
||||
generationBatch={generationBatch}
|
||||
onCopyError={handleCopyError}
|
||||
onDelete={handleDeleteGeneration}
|
||||
/>
|
||||
@@ -145,6 +141,7 @@ export const GenerationItem = memo<GenerationItemProps>(
|
||||
<LoadingState
|
||||
aspectRatio={aspectRatio}
|
||||
generation={generation}
|
||||
generationBatch={generationBatch}
|
||||
onDelete={handleDeleteGeneration}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ActionButtonsProps {
|
||||
export interface SuccessStateProps {
|
||||
aspectRatio: string;
|
||||
generation: Generation;
|
||||
generationBatch: GenerationBatch;
|
||||
onCopySeed?: () => void;
|
||||
onDelete: () => void;
|
||||
onDownload: () => void;
|
||||
@@ -28,6 +29,7 @@ export interface SuccessStateProps {
|
||||
export interface ErrorStateProps {
|
||||
aspectRatio: string;
|
||||
generation: Generation;
|
||||
generationBatch: GenerationBatch;
|
||||
onCopyError: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
@@ -35,5 +37,6 @@ export interface ErrorStateProps {
|
||||
export interface LoadingStateProps {
|
||||
aspectRatio: string;
|
||||
generation: Generation;
|
||||
generationBatch: GenerationBatch;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Generation, GenerationBatch } from '@/types/generation';
|
||||
|
||||
// Import functions for testing
|
||||
import {
|
||||
DEFAULT_MAX_ITEM_WIDTH,
|
||||
getAspectRatio,
|
||||
getImageDimensions,
|
||||
getThumbnailMaxWidth,
|
||||
} from './utils';
|
||||
|
||||
describe('getImageDimensions', () => {
|
||||
// Mock base generation object
|
||||
const baseGeneration: Generation = {
|
||||
id: 'test-gen-id',
|
||||
seed: 12345,
|
||||
createdAt: new Date(),
|
||||
asyncTaskId: null,
|
||||
task: {
|
||||
id: 'task-id',
|
||||
status: 'success' as any,
|
||||
},
|
||||
};
|
||||
|
||||
describe('with asset dimensions', () => {
|
||||
it('should return width, height and aspect ratio from asset', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation);
|
||||
expect(result).toEqual({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
aspectRatio: '1920 / 1080',
|
||||
});
|
||||
});
|
||||
|
||||
it('should prioritize asset even when other sources exist', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
config: {
|
||||
prompt: 'test',
|
||||
width: 512,
|
||||
height: 512,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: 800,
|
||||
height: 600,
|
||||
aspectRatio: '800 / 600',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with config dimensions', () => {
|
||||
it('should return dimensions from config when asset is not available', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
width: 1024,
|
||||
height: 768,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: 1024,
|
||||
height: 768,
|
||||
aspectRatio: '1024 / 768',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with batch top-level dimensions', () => {
|
||||
it('should return dimensions from batch when config is not available', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
width: 1280,
|
||||
height: 720,
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: 1280,
|
||||
height: 720,
|
||||
aspectRatio: '1280 / 720',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with size parameter', () => {
|
||||
it('should parse dimensions from size parameter', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
size: '1920x1080',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
aspectRatio: '1920 / 1080',
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore size when it is "auto"', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
size: 'auto',
|
||||
aspectRatio: '16:9',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: null,
|
||||
height: null,
|
||||
aspectRatio: '16 / 9',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with aspectRatio parameter only', () => {
|
||||
it('should return aspect ratio without dimensions', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
aspectRatio: '16:9',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: null,
|
||||
height: null,
|
||||
aspectRatio: '16 / 9',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle various aspect ratio formats', () => {
|
||||
const testCases = [
|
||||
{ aspectRatio: '1:1', expected: '1 / 1' },
|
||||
{ aspectRatio: '4:3', expected: '4 / 3' },
|
||||
{ aspectRatio: '16:9', expected: '16 / 9' },
|
||||
{ aspectRatio: '21:9', expected: '21 / 9' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ aspectRatio, expected }) => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
aspectRatio,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result.aspectRatio).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return all null when no dimensions are available', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation);
|
||||
expect(result).toEqual({
|
||||
width: null,
|
||||
height: null,
|
||||
aspectRatio: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle partial asset dimensions', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 1920,
|
||||
// height is missing
|
||||
},
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
width: 1024,
|
||||
height: 768,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: 1024,
|
||||
height: 768,
|
||||
aspectRatio: '1024 / 768',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid size format', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
size: 'invalid-format',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: null,
|
||||
height: null,
|
||||
aspectRatio: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid aspectRatio format', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: null,
|
||||
};
|
||||
|
||||
const generationBatch: GenerationBatch = {
|
||||
id: 'batch-id',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
config: {
|
||||
prompt: 'test',
|
||||
aspectRatio: 'invalid-format',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation, generationBatch);
|
||||
expect(result).toEqual({
|
||||
width: null,
|
||||
height: null,
|
||||
aspectRatio: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle zero dimensions', () => {
|
||||
const generation: Generation = {
|
||||
...baseGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getImageDimensions(generation);
|
||||
expect(result).toEqual({
|
||||
width: null,
|
||||
height: null,
|
||||
aspectRatio: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAspectRatio (isolated unit testing)', () => {
|
||||
const mockGeneration: Generation = {
|
||||
id: 'test-gen-id',
|
||||
seed: 12345,
|
||||
createdAt: new Date(),
|
||||
asyncTaskId: null,
|
||||
task: {
|
||||
id: 'task-id',
|
||||
status: 'success' as any,
|
||||
},
|
||||
};
|
||||
const mockGenerationBatch: GenerationBatch = {
|
||||
id: 'test-batch-id',
|
||||
provider: 'test-provider',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return aspectRatio from getImageDimensions when dimensions have aspectRatio', () => {
|
||||
// Test the actual implementation directly with mock data
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
};
|
||||
|
||||
const result = getAspectRatio(mockGen);
|
||||
expect(result).toBe('1920 / 1080');
|
||||
});
|
||||
|
||||
it('should return default "1 / 1" when no dimensions are available', () => {
|
||||
const result = getAspectRatio(mockGeneration, mockGenerationBatch);
|
||||
expect(result).toBe('1 / 1');
|
||||
});
|
||||
|
||||
it('should work with different aspectRatio sources', () => {
|
||||
const mockBatch: GenerationBatch = {
|
||||
id: 'test-batch',
|
||||
provider: 'test-provider',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
config: {
|
||||
prompt: 'test prompt',
|
||||
aspectRatio: '16:9',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getAspectRatio(mockGeneration, mockBatch);
|
||||
expect(result).toBe('16 / 9');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThumbnailMaxWidth (isolated unit testing)', () => {
|
||||
const mockGeneration: Generation = {
|
||||
id: 'test-gen-id',
|
||||
seed: 12345,
|
||||
createdAt: new Date(),
|
||||
asyncTaskId: null,
|
||||
task: {
|
||||
id: 'task-id',
|
||||
status: 'success' as any,
|
||||
},
|
||||
};
|
||||
const mockGenerationBatch: GenerationBatch = {
|
||||
id: 'test-batch-id',
|
||||
provider: 'test-provider',
|
||||
model: 'test-model',
|
||||
prompt: 'test prompt',
|
||||
createdAt: new Date(),
|
||||
generations: [],
|
||||
};
|
||||
|
||||
// Mock window.innerHeight for tests
|
||||
const originalWindow = global.window;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
Object.defineProperty(global, 'window', {
|
||||
writable: true,
|
||||
value: {
|
||||
innerHeight: 800,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.window = originalWindow;
|
||||
});
|
||||
|
||||
it('should return DEFAULT_MAX_ITEM_WIDTH when no dimensions available', () => {
|
||||
const result = getThumbnailMaxWidth(mockGeneration, mockGenerationBatch);
|
||||
expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
|
||||
});
|
||||
|
||||
it('should return DEFAULT_MAX_ITEM_WIDTH when width is missing', () => {
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
// No asset with width/height, should fall back to default
|
||||
};
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
|
||||
});
|
||||
|
||||
it('should return DEFAULT_MAX_ITEM_WIDTH when height is missing', () => {
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
// No asset with valid dimensions
|
||||
};
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(DEFAULT_MAX_ITEM_WIDTH);
|
||||
});
|
||||
|
||||
it('should calculate width based on screen height constraint', () => {
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 300,
|
||||
height: 200,
|
||||
},
|
||||
};
|
||||
|
||||
// aspectRatio = 300/200 = 1.5
|
||||
// maxScreenHeight = 800/2 = 400
|
||||
// maxWidthFromHeight = 400 * 1.5 = 600
|
||||
// maxReasonableWidth = 200 * 2 = 400
|
||||
// min(600, 400) = 400
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should apply maxReasonableWidth limit', () => {
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 600,
|
||||
height: 200,
|
||||
},
|
||||
};
|
||||
|
||||
// aspectRatio = 600/200 = 3
|
||||
// maxScreenHeight = 800/2 = 400
|
||||
// maxWidthFromHeight = 400 * 3 = 1200
|
||||
// maxReasonableWidth = 200 * 2 = 400
|
||||
// min(1200, 400) = 400
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should use screen height constraint when smaller', () => {
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 200,
|
||||
height: 400,
|
||||
},
|
||||
};
|
||||
|
||||
// aspectRatio = 200/400 = 0.5
|
||||
// maxScreenHeight = 800/2 = 400
|
||||
// maxWidthFromHeight = 400 * 0.5 = 200
|
||||
// maxReasonableWidth = 200 * 2 = 400
|
||||
// min(200, 400) = 200
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(200);
|
||||
});
|
||||
|
||||
it('should handle different window.innerHeight values', () => {
|
||||
Object.defineProperty(global, 'window', {
|
||||
writable: true,
|
||||
value: {
|
||||
innerHeight: 600,
|
||||
},
|
||||
});
|
||||
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 400,
|
||||
height: 200,
|
||||
},
|
||||
};
|
||||
|
||||
// aspectRatio = 400/200 = 2
|
||||
// maxScreenHeight = 600/2 = 300
|
||||
// maxWidthFromHeight = 300 * 2 = 600
|
||||
// maxReasonableWidth = 200 * 2 = 400
|
||||
// min(600, 400) = 400
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(400);
|
||||
});
|
||||
|
||||
it('should round calculated width correctly', () => {
|
||||
const mockGen: Generation = {
|
||||
...mockGeneration,
|
||||
asset: {
|
||||
type: 'image',
|
||||
width: 512,
|
||||
height: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
// aspectRatio = 512/1000 = 0.512
|
||||
// maxScreenHeight = 800/2 = 400
|
||||
// maxWidthFromHeight = Math.round(400 * 0.512) = Math.round(204.8) = 205
|
||||
// maxReasonableWidth = 200 * 2 = 400
|
||||
// min(205, 400) = 205
|
||||
const result = getThumbnailMaxWidth(mockGen);
|
||||
expect(result).toBe(205);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,130 @@
|
||||
import { Generation } from '@/types/generation';
|
||||
import { Generation, GenerationBatch } from '@/types/generation';
|
||||
|
||||
// 计算图片的宽高比,用于设置容器的 aspect-ratio
|
||||
export const getAspectRatio = (asset: Generation['asset']) => {
|
||||
if (!asset?.width || !asset?.height) {
|
||||
// 如果没有尺寸信息,使用 1:1 比例
|
||||
return '1 / 1';
|
||||
// Default maximum width for image items
|
||||
export const DEFAULT_MAX_ITEM_WIDTH = 200;
|
||||
|
||||
/**
|
||||
* Get image dimensions from various sources
|
||||
* Returns width, height and aspect ratio when available
|
||||
*/
|
||||
export const getImageDimensions = (
|
||||
generation: Generation,
|
||||
generationBatch?: GenerationBatch,
|
||||
): { aspectRatio: string | null; height: number | null; width: number | null } => {
|
||||
// 1. Priority: actual dimensions from asset
|
||||
if (
|
||||
generation.asset?.width &&
|
||||
generation.asset?.height &&
|
||||
generation.asset.width > 0 &&
|
||||
generation.asset.height > 0
|
||||
) {
|
||||
const { width, height } = generation.asset;
|
||||
return {
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
return `${asset.width} / ${asset.height}`;
|
||||
// 2. Try to get dimensions from generationBatch config
|
||||
const config = generationBatch?.config;
|
||||
if (config?.width && config?.height && config.width > 0 && config.height > 0) {
|
||||
const { width, height } = config;
|
||||
return {
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Try to get dimensions from generationBatch top-level
|
||||
if (
|
||||
generationBatch?.width &&
|
||||
generationBatch?.height &&
|
||||
generationBatch.width > 0 &&
|
||||
generationBatch.height > 0
|
||||
) {
|
||||
const { width, height } = generationBatch;
|
||||
return {
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. Try to parse from size parameter (format: "1024x768")
|
||||
if (config?.size && config.size !== 'auto') {
|
||||
const sizeMatch = config.size.match(/^(\d+)x(\d+)$/);
|
||||
if (sizeMatch) {
|
||||
const [, widthStr, heightStr] = sizeMatch;
|
||||
const width = parseInt(widthStr, 10);
|
||||
const height = parseInt(heightStr, 10);
|
||||
if (width > 0 && height > 0) {
|
||||
return {
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Try to get aspect ratio only (format: "16:9")
|
||||
if (config?.aspectRatio) {
|
||||
const ratioMatch = config.aspectRatio.match(/^(\d+):(\d+)$/);
|
||||
if (ratioMatch) {
|
||||
const [, x, y] = ratioMatch;
|
||||
return {
|
||||
aspectRatio: `${x} / ${y}`,
|
||||
height: null,
|
||||
width: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 6. No dimensions available
|
||||
return {
|
||||
aspectRatio: null,
|
||||
height: null,
|
||||
width: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAspectRatio = (
|
||||
generation: Generation,
|
||||
generationBatch?: GenerationBatch,
|
||||
): string => {
|
||||
const dimensions = getImageDimensions(generation, generationBatch);
|
||||
return dimensions.aspectRatio || '1 / 1';
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate display max width for generation items
|
||||
* Ensures height doesn't exceed half screen height based on original aspect ratio
|
||||
*
|
||||
* @note This function is only used in client-side rendering environments.
|
||||
* It directly accesses window.innerHeight and is not designed for SSR compatibility.
|
||||
*/
|
||||
export const getThumbnailMaxWidth = (
|
||||
generation: Generation,
|
||||
generationBatch?: GenerationBatch,
|
||||
): number => {
|
||||
const dimensions = getImageDimensions(generation, generationBatch);
|
||||
|
||||
// Return default width if dimensions are not available
|
||||
if (!dimensions.width || !dimensions.height) {
|
||||
return DEFAULT_MAX_ITEM_WIDTH;
|
||||
}
|
||||
|
||||
const { width: originalWidth, height: originalHeight } = dimensions;
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
|
||||
// Apply screen height constraint (half of screen height)
|
||||
// Note: window.innerHeight is safe to use here as this function is client-side only
|
||||
const maxScreenHeight = window.innerHeight / 2;
|
||||
const maxWidthFromHeight = Math.round(maxScreenHeight * aspectRatio);
|
||||
|
||||
// Use the smaller of: calculated width from height constraint or a reasonable maximum
|
||||
const maxReasonableWidth = DEFAULT_MAX_ITEM_WIDTH * 2;
|
||||
return Math.min(maxWidthFromHeight, maxReasonableWidth);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Popconfirm, Switch } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Trash } from 'lucide-react';
|
||||
import { FC, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { ApiKeyItem, CreateApiKeyParams, UpdateApiKeyParams } from '@/types/apiKey';
|
||||
|
||||
import { ApiKeyDisplay, ApiKeyModal, EditableCell } from './features';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
container: css`
|
||||
.ant-pro-card-body {
|
||||
padding-inline: 0;
|
||||
|
||||
.ant-pro-table-list-toolbar-container {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-end: ${token.margin}px;
|
||||
`,
|
||||
table: css`
|
||||
border-radius: ${token.borderRadius}px;
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Client: FC = () => {
|
||||
const { styles } = useStyles();
|
||||
const { t } = useTranslation('auth');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const actionRef = useRef<ActionType>(null);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (params: CreateApiKeyParams) => lambdaClient.apiKey.createApiKey.mutate(params),
|
||||
onSuccess: () => {
|
||||
actionRef.current?.reload();
|
||||
setModalOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, params }: { id: number; params: UpdateApiKeyParams }) =>
|
||||
lambdaClient.apiKey.updateApiKey.mutate({ id, value: params }),
|
||||
onSuccess: () => {
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => lambdaClient.apiKey.deleteApiKey.mutate({ id }),
|
||||
onSuccess: () => {
|
||||
actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleModalOk = (values: CreateApiKeyParams) => {
|
||||
createMutation.mutate(values);
|
||||
};
|
||||
|
||||
const columns: ProColumns<ApiKeyItem>[] = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (_, apiKey) => (
|
||||
<EditableCell
|
||||
onSubmit={(name) => {
|
||||
if (!name || name === apiKey.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutate({ id: apiKey.id!, params: { name: name as string } });
|
||||
}}
|
||||
placeholder={t('apikey.display.enterPlaceholder')}
|
||||
type="text"
|
||||
value={apiKey.name}
|
||||
/>
|
||||
),
|
||||
title: t('apikey.list.columns.name'),
|
||||
},
|
||||
{
|
||||
dataIndex: 'key',
|
||||
ellipsis: true,
|
||||
key: 'key',
|
||||
render: (_, apiKey) => <ApiKeyDisplay apiKey={apiKey.key} />,
|
||||
title: t('apikey.list.columns.key'),
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (_, apiKey: ApiKeyItem) => (
|
||||
<Switch
|
||||
checked={!!apiKey.enabled}
|
||||
onChange={(checked) => {
|
||||
updateMutation.mutate({ id: apiKey.id!, params: { enabled: checked } });
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: t('apikey.list.columns.status'),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
dataIndex: 'expiresAt',
|
||||
key: 'expiresAt',
|
||||
render: (_, apiKey) => (
|
||||
<EditableCell
|
||||
onSubmit={(expiresAt) => {
|
||||
if (expiresAt === apiKey.expiresAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
id: apiKey.id!,
|
||||
params: { expiresAt: expiresAt ? new Date(expiresAt as string) : null },
|
||||
});
|
||||
}}
|
||||
placeholder={t('apikey.display.neverExpires')}
|
||||
type="date"
|
||||
value={apiKey.expiresAt?.toLocaleString() || t('apikey.display.neverExpires')}
|
||||
/>
|
||||
),
|
||||
title: t('apikey.list.columns.expiresAt'),
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
dataIndex: 'lastUsedAt',
|
||||
key: 'lastUsedAt',
|
||||
renderText: (_, apiKey: ApiKeyItem) =>
|
||||
apiKey.lastUsedAt?.toLocaleString() || t('apikey.display.neverUsed'),
|
||||
title: t('apikey.list.columns.lastUsedAt'),
|
||||
},
|
||||
{
|
||||
key: 'action',
|
||||
render: (_: any, apiKey: ApiKeyItem) => (
|
||||
<Popconfirm
|
||||
cancelText={t('apikey.list.actions.deleteConfirm.actions.cancel')}
|
||||
description={t('apikey.list.actions.deleteConfirm.content')}
|
||||
okText={t('apikey.list.actions.deleteConfirm.actions.ok')}
|
||||
onConfirm={() => deleteMutation.mutate(apiKey.id!)}
|
||||
title={t('apikey.list.actions.deleteConfirm.title')}
|
||||
>
|
||||
<Button
|
||||
icon={Trash}
|
||||
size="small"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
title={t('apikey.list.actions.delete')}
|
||||
type="text"
|
||||
/>
|
||||
</Popconfirm>
|
||||
),
|
||||
title: t('apikey.list.columns.actions'),
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
className={styles.table}
|
||||
columns={columns}
|
||||
headerTitle={t('apikey.list.title')}
|
||||
options={false}
|
||||
pagination={false}
|
||||
request={async () => {
|
||||
const apiKeys = await lambdaClient.apiKey.getApiKeys.query();
|
||||
|
||||
return {
|
||||
data: apiKeys,
|
||||
success: true,
|
||||
};
|
||||
}}
|
||||
rowKey="id"
|
||||
search={false}
|
||||
toolbar={{
|
||||
actions: [
|
||||
<Button key="create" onClick={handleCreate} type="primary">
|
||||
{t('apikey.list.actions.create')}
|
||||
</Button>,
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ApiKeyModal
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={handleModalOk}
|
||||
open={modalOpen}
|
||||
submitLoading={createMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Client;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user