Compare commits

..

32 Commits

Author SHA1 Message Date
arvinxx 4fa6d64df2 add plugin settings 2025-07-29 14:48:46 +08:00
Zhijie He 403aebd52e 💄 style: add more OpenAI SDK Text2Image providers (#8573) 2025-07-29 14:45:19 +08:00
LobeHub Bot 356cf0c392 💄 style: update i18n (#8593)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-29 14:37:29 +08:00
Arvin Xu be98d56ef4 ♻️ refactor: refactor jose-JWT to xor obfuscation (#8595)
* refactor jose-jwt to xor obfuscation

* rename JWTPayload type to ClientSecretPayload

* fix tests

* revert next version
2025-07-29 14:37:01 +08:00
lobehubbot 3fae1b2638 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-29 04:07:54 +00:00
semantic-release-bot ee4cc6c2e0 🔖 chore(release): v1.105.1 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-29 04:07:02 +00:00
Zhijie He b8c0e2d639 💄 style: support more Text2Image from Qwen (#8574) 2025-07-29 11:51:40 +08:00
lobehubbot 74d20bdbe8 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 09:12:32 +00:00
semantic-release-bot 7bab44e74c 🔖 chore(release): v1.105.0 [skip ci]
## [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 09:11:38 +00:00
Zephyr fdaa72564c feat: Implement API Key management functionality (#8535)
*  feat: Implement API Key management functionality

- Added new components for API Key management including creation, deletion, and display.
- Introduced a new database schema for storing API Keys.
- Implemented server and client services for API Key operations.
- Integrated API Key management into the profile section with appropriate routing and feature flags.
- Enhanced localization support for API Key related UI elements.

This commit lays the groundwork for managing API Keys within the application, allowing users to create, view, and manage their keys effectively.

* fix: server config unit test

*  feat(database): Create api_keys table with conditional existence check

- Added a conditional check to create the "api_keys" table only if it does not already exist.
- Ensured the foreign key constraint for "user_id" references the "users" table remains intact.

This change enhances the migration process by preventing errors during table creation if the table already exists.

* feat: Implement API Key management interface

- Introduced a new Client component for managing API keys, including creation, updating, and deletion functionalities.
- Replaced the previous page component with the new Client component in the API key management page.
- Removed obsolete client and server service files related to API key management, streamlining the service layer.

This update enhances the user experience by providing a dedicated interface for API key operations.
2025-07-28 16:55:57 +08:00
lobehubbot 7d85151cb6 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 08:53:48 +00:00
semantic-release-bot 23ef2eea59 🔖 chore(release): v1.104.5 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 08:52:59 +00:00
Shinji-Li 74ab822140 💄 style: fix setting window layout when in desktop was disappear (#8585) 2025-07-28 16:37:45 +08:00
lobehubbot d726ff108d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-28 02:37:48 +00:00
semantic-release-bot dd7b661140 🔖 chore(release): v1.104.4 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-28 02:36:53 +00:00
Shinji-Li 49023419cf 💄 style: fix setting window layout size (#8483) 2025-07-28 10:21:30 +08:00
LobeHub Bot 2eccbc79eb 💄 style: update i18n (#8579)
Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2025-07-28 10:19:46 +08:00
lobehubbot 3527cb65f1 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-26 09:48:50 +00:00
semantic-release-bot 1731c841d8 🔖 chore(release): v1.104.3 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-26 09:47:57 +00:00
afon 404ac21229 💄 style: Add Gemini 2.5 Flash-Lite GA model (#8539) 2025-07-26 17:32:34 +08:00
Arvin Xu 2eaa2dbea0 🔨 chore: add react scan debugger and bump deps (#8576)
* add REACT_SCAN debug

* upgrade lobehub/ui

* clean

* head

* update
2025-07-26 16:32:55 +08:00
lobehubbot 50c0ed168d 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-26 07:32:43 +00:00
semantic-release-bot 780e231afa 🔖 chore(release): v1.104.2 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-26 07:31:53 +00:00
Arvin Xu 07f3e6a4c4 🐛 fix: fix update hotkey invalid when input mod in desktop (#8572)
* fix hotkey with mod

* fix invalid hotkeys

* add tests
2025-07-26 15:16:45 +08:00
lobehubbot 3c35edced5 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-25 09:59:34 +00:00
semantic-release-bot 15770f188f 🔖 chore(release): v1.104.1 [skip ci]
### [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-25 09:58:34 +00:00
YuTengjing 9d7c6014fd chore: improve image display quality (#8571) 2025-07-25 17:42:23 +08:00
sxjeru d1e4a54b01 🐛 fix: update convertUsage to handle XAI provider and adjust OpenAIStream to pass provider (#8557) 2025-07-25 16:16:54 +08:00
lobehubbot 1bc8815fb4 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-24 17:20:43 +00:00
semantic-release-bot 284826bed0 🔖 chore(release): v1.104.0 [skip ci]
## [Version&nbsp;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">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-07-24 17:19:53 +00:00
Arvin Xu b50f1212cb feat: support custom hotkey on desktop (#8559)
* support custom hotkey

* update tests

* clean

* fix tests
2025-07-25 01:04:18 +08:00
lobehubbot 946517a52e 📝 docs(bot): Auto sync agents & plugin to readme 2025-07-24 04:20:29 +00:00
189 changed files with 13510 additions and 602 deletions
+2
View File
@@ -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.
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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),
);
});
});
});
+4 -2
View File
@@ -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+,',
};
+68
View File
@@ -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."]
+54
View File
@@ -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
View File
@@ -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": "إعادة توليد الرسالة"
+3
View File
@@ -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
View File
@@ -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> لتشفير مفتاحك وعنوان الوكيل",
+54
View File
@@ -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": "Статистика"
+10 -4
View File
@@ -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": "Прегенериране на съобщение"
+3
View File
@@ -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
View File
@@ -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>",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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.",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+13 -1
View File
@@ -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",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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>",
+54
View File
@@ -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": "آمار"
+10 -4
View File
@@ -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": "تولید مجدد پیام"
+3
View File
@@ -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 کوچک‌ترین و مقرون‌به‌صرفه‌ترین مدل گوگل است که برای استفاده در مقیاس بزرگ طراحی شده است."
},
+12 -1
View File
@@ -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> رمزگذاری خواهد شد",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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>",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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>",
+54
View File
@@ -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": "統計"
+10 -4
View File
@@ -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": "メッセージを再生成"
+3
View File
@@ -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
View File
@@ -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> 暗号化アルゴリズムを使用して暗号化されます",
+54
View File
@@ -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": "통계"
+10 -4
View File
@@ -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": "메시지 다시 생성"
+3
View File
@@ -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는 구글의 가장 작고 가성비가 뛰어난 모델로, 대규모 사용을 위해 설계되었습니다."
},
+12 -1
View File
@@ -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> 암호화 알고리즘을 사용하여 암호화됩니다",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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",
+54
View File
@@ -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"
+10 -4
View File
@@ -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ść"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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>",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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>",
+54
View File
@@ -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": "Статистика"
+10 -4
View File
@@ -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": "Перегенерировать сообщение"
+3
View File
@@ -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
View File
@@ -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>",
+54
View File
@@ -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"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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",
+54
View File
@@ -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ê"
+10 -4
View File
@@ -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"
+3
View File
@@ -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."
},
+12 -1
View File
@@ -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>",
+54
View File
@@ -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": "数据统计"
+10 -4
View File
@@ -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": "重新生成消息"
+3
View File
@@ -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 最小、性价比最高的模型,专为大规模使用而设计。"
},
+13 -1
View File
@@ -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": "数据存储",
+54
View File
@@ -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": "數據統計"
+10 -4
View File
@@ -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": "重新生成消息"
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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;
});
+6 -6
View File
@@ -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}
@@ -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'}
>
@@ -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';
@@ -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