Merge branch 'kangfenmao:main' into develop
This commit is contained in:
commit
e35d928bcd
@ -2,4 +2,4 @@ node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
|
||||
scripts/cloudflare-worker.js
|
||||
|
||||
73
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
Normal file
73
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
name: 🐛 错误报告
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间填写此错误报告!
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 错误描述
|
||||
description: 清晰简洁地描述错误是什么
|
||||
placeholder: 告诉我们发生了什么...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 重现行为的步骤
|
||||
placeholder: |
|
||||
1. 转到 '...'
|
||||
2. 点击 '....'
|
||||
3. 向下滚动到 '....'
|
||||
4. 看到错误
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 预期行为
|
||||
description: 清晰简洁地描述您期望发生的事情
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 相关日志输出
|
||||
description: 请复制并粘贴任何相关的日志输出
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 在此添加有关问题的任何其他上下文
|
||||
38
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: 💡 功能建议
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出新的功能建议!
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 您的功能建议是否与某个问题相关?
|
||||
description: 请简明扼要地描述您遇到的问题
|
||||
placeholder: 我总是感到沮丧,因为...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 请描述您希望实现的解决方案
|
||||
description: 请简明扼要地描述您希望发生的情况
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 请描述您考虑过的其他方案
|
||||
description: 请简明扼要地描述您考虑过的任何其他解决方案或功能
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 其他补充信息
|
||||
description: 在此添加任何其他与功能建议相关的上下文或截图
|
||||
44
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: ❓ 提问
|
||||
description: 提出一个问题或寻求帮助
|
||||
title: '[问题]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 您的问题
|
||||
description: 请详细描述您的问题
|
||||
placeholder: 请尽可能清楚地说明您的问题...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 相关背景
|
||||
description: 请提供一些背景信息,帮助我们更好地理解您的问题
|
||||
placeholder: 例如:使用场景、已尝试的解决方案等
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 任何其他相关的信息、截图或代码示例
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级
|
||||
description: 这个问题对您来说有多紧急?
|
||||
options:
|
||||
- 低 (有空再看)
|
||||
- 中 (希望尽快得到答复)
|
||||
- 高 (阻碍工作进行)
|
||||
validations:
|
||||
required: true
|
||||
73
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
Normal file
73
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: What platform are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Cherry Studio are you running?
|
||||
placeholder: e.g. v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
placeholder: Tell us what happened...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant Log Output
|
||||
description: Please copy and paste any relevant log output
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here
|
||||
38
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to suggest a new feature!
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: A clear and concise description of what the problem is
|
||||
placeholder: I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here
|
||||
44
.github/ISSUE_TEMPLATE/2_question.yml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/2_question.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: ❓ Question
|
||||
description: Ask a question or seek help
|
||||
title: '[Question]: '
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Your Question
|
||||
description: Please describe your question in detail
|
||||
placeholder: Please explain your question as clearly as possible...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Context
|
||||
description: Please provide some background information to help us better understand your question
|
||||
placeholder: "For example: use case, solutions you've tried, etc."
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other relevant information, screenshots, or code examples
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: Priority
|
||||
description: How urgent is this question for you?
|
||||
options:
|
||||
- Low (Can wait)
|
||||
- Medium (Would like a response soon)
|
||||
- High (Blocking progress)
|
||||
validations:
|
||||
required: true
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -1,6 +1,12 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version (e.g. v1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
|
||||
26
.yarn/patches/openai-npm-4.71.1-b5940d6401.patch
Normal file
26
.yarn/patches/openai-npm-4.71.1-b5940d6401.patch
Normal file
@ -0,0 +1,26 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d82573ea5 100644
|
||||
--- a/core.js
|
||||
+++ b/core.js
|
||||
@@ -156,7 +156,7 @@ class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/core.mjs b/core.mjs
|
||||
index 8bc7a0ee10d61560d7113cf3f703355bb19f7ddd..5e4c8586ea6b13fe887a22af2de05eaa4700b5ec 100644
|
||||
--- a/core.mjs
|
||||
+++ b/core.mjs
|
||||
@@ -149,7 +149,7 @@ export class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
BIN
build/tray_icon.png
Normal file
BIN
build/tray_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
build/tray_icon_dark.png
Normal file
BIN
build/tray_icon_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
build/tray_icon_light.png
Normal file
BIN
build/tray_icon_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@ -133,7 +133,7 @@ Cherry Studioへの貢献を歓迎します!以下の方法で貢献できま
|
||||
|
||||
# 📃 ライセンス
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ⭐️ スター履歴
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ $ yarn build:linux
|
||||
|
||||
# 📃 许可证
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ⭐️ Star 记录
|
||||
|
||||
|
||||
@ -63,6 +63,6 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
支持聊天气泡样式和简洁样式切换
|
||||
支持导出对话为 Word 文档
|
||||
错误修复
|
||||
修复快捷键设置错误导致的无法启动问题
|
||||
修复翻译按钮无法正常输出内容问题
|
||||
修复检测更新按钮逻辑错误
|
||||
|
||||
@ -7,8 +7,9 @@ export default defineConfig({
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@main': resolve('src/main'),
|
||||
'@types': resolve('src/renderer/src/types'),
|
||||
'@main': resolve('src/main')
|
||||
'@shared': resolve('packages/shared')
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -16,11 +17,15 @@ export default defineConfig({
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
},
|
||||
renderer: {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
'@renderer': resolve('src/renderer/src'),
|
||||
'@shared': resolve('packages/shared')
|
||||
}
|
||||
},
|
||||
plugins: [react()]
|
||||
optimizeDeps: {
|
||||
exclude: []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.23",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@ -38,7 +38,7 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
@ -48,7 +48,6 @@
|
||||
"html2canvas": "^1.4.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"unzipper": "^0.12.3",
|
||||
"webdav": "4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -60,6 +59,7 @@
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
@ -67,7 +67,6 @@
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/unzipper": "^0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.18.3",
|
||||
"axios": "^1.7.3",
|
||||
@ -90,20 +89,19 @@
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"gpt-tokens": "^1.3.10",
|
||||
"i18next": "^23.11.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.4",
|
||||
"openai": "^4.52.1",
|
||||
"openai": "patch:openai@npm%3A4.71.1#~/.yarn/patches/openai-npm-4.71.1-b5940d6401.patch",
|
||||
"prettier": "^3.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@ -112,6 +110,7 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sass": "^1.77.2",
|
||||
"shiki": "^1.22.2",
|
||||
"styled-components": "^6.1.11",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^5.6.2",
|
||||
|
||||
112
packages/shared/config/constant.ts
Normal file
112
packages/shared/config/constant.ts
Normal file
@ -0,0 +1,112 @@
|
||||
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
'.md', // Markdown 文件
|
||||
'.mdx', // Markdown 文件
|
||||
'.html', // HTML 文件
|
||||
'.htm', // HTML 文件的另一种扩展名
|
||||
'.xml', // XML 文件
|
||||
'.json', // JSON 文件
|
||||
'.yaml', // YAML 文件
|
||||
'.yml', // YAML 文件的另一种扩展名
|
||||
'.csv', // 逗号分隔值文件
|
||||
'.tsv', // 制表符分隔值文件
|
||||
'.ini', // 配置文件
|
||||
'.log', // 日志文件
|
||||
'.rtf', // 富文本格式文件
|
||||
'.tex', // LaTeX 文件
|
||||
'.srt', // 字幕文件
|
||||
'.xhtml', // XHTML 文件
|
||||
'.nfo', // 信息文件(主要用于场景发布)
|
||||
'.conf', // 配置文件
|
||||
'.config', // 配置文件
|
||||
'.env', // 环境变量文件
|
||||
'.rst', // reStructuredText 文件
|
||||
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||
'.ts', // TypeScript 文件
|
||||
'.jsp', // JavaServer Pages 文件
|
||||
'.aspx', // ASP.NET 文件
|
||||
'.bat', // Windows 批处理文件
|
||||
'.sh', // Unix/Linux Shell 脚本文件
|
||||
'.py', // Python 脚本文件
|
||||
'.rb', // Ruby 脚本文件
|
||||
'.pl', // Perl 脚本文件
|
||||
'.sql', // SQL 脚本文件
|
||||
'.css', // Cascading Style Sheets 文件
|
||||
'.less', // Less CSS 预处理器文件
|
||||
'.scss', // Sass CSS 预处理器文件
|
||||
'.sass', // Sass 文件
|
||||
'.styl', // Stylus CSS 预处理器文件
|
||||
'.coffee', // CoffeeScript 文件
|
||||
'.ino', // Arduino 代码文件
|
||||
'.asm', // Assembly 语言文件
|
||||
'.go', // Go 语言文件
|
||||
'.scala', // Scala 语言文件
|
||||
'.swift', // Swift 语言文件
|
||||
'.kt', // Kotlin 语言文件
|
||||
'.rs', // Rust 语言文件
|
||||
'.lua', // Lua 语言文件
|
||||
'.groovy', // Groovy 语言文件
|
||||
'.dart', // Dart 语言文件
|
||||
'.hs', // Haskell 语言文件
|
||||
'.clj', // Clojure 语言文件
|
||||
'.cljs', // ClojureScript 语言文件
|
||||
'.elm', // Elm 语言文件
|
||||
'.erl', // Erlang 语言文件
|
||||
'.ex', // Elixir 语言文件
|
||||
'.exs', // Elixir 脚本文件
|
||||
'.pug', // Pug (formerly Jade) 模板文件
|
||||
'.haml', // Haml 模板文件
|
||||
'.slim', // Slim 模板文件
|
||||
'.tpl', // 模板文件(通用)
|
||||
'.ejs', // Embedded JavaScript 模板文件
|
||||
'.hbs', // Handlebars 模板文件
|
||||
'.mustache', // Mustache 模板文件
|
||||
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||
'.twig', // Twig 模板文件
|
||||
'.blade', // Blade 模板文件 (Laravel)
|
||||
'.vue', // Vue.js 单文件组件
|
||||
'.jsx', // React JSX 文件
|
||||
'.tsx', // React TSX 文件
|
||||
'.graphql', // GraphQL 查询语言文件
|
||||
'.gql', // GraphQL 查询语言文件
|
||||
'.proto', // Protocol Buffers 文件
|
||||
'.thrift', // Thrift 文件
|
||||
'.toml', // TOML 配置文件
|
||||
'.edn', // Clojure 数据表示文件
|
||||
'.cake', // CakePHP 配置文件
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java' // Java 代码文件
|
||||
]
|
||||
|
||||
export const ZOOM_SHORTCUTS = [
|
||||
{
|
||||
key: 'zoom_in',
|
||||
shortcut: ['CommandOrControl', '='],
|
||||
editable: false,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_out',
|
||||
shortcut: ['CommandOrControl', '-'],
|
||||
editable: false,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
key: 'zoom_reset',
|
||||
shortcut: ['CommandOrControl', '0'],
|
||||
editable: false,
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
595
scripts/cloudflare-worker.js
Normal file
595
scripts/cloudflare-worker.js
Normal file
@ -0,0 +1,595 @@
|
||||
// 配置信息
|
||||
const config = {
|
||||
R2_CUSTOM_DOMAIN: 'cherrystudio.ocool.online',
|
||||
R2_BUCKET_NAME: 'cherrystudio',
|
||||
// 缓存键名
|
||||
CACHE_KEY: 'cherry-studio-latest-release',
|
||||
VERSION_DB: 'versions.json',
|
||||
LOG_FILE: 'logs.json',
|
||||
MAX_LOGS: 1000 // 最多保存多少条日志
|
||||
};
|
||||
|
||||
// Worker 入口函数
|
||||
const worker = {
|
||||
// 定时器触发配置
|
||||
scheduled: {
|
||||
cron: '*/1 * * * *' // 每分钟执行一次
|
||||
},
|
||||
|
||||
// 定时器执行函数 - 只负责检查和更新
|
||||
async scheduled(event, env, ctx) {
|
||||
try {
|
||||
await initDataFiles(env);
|
||||
console.log('开始定时检查新版本...');
|
||||
// 注意这里使用新的函数
|
||||
await checkNewRelease(env);
|
||||
} catch (error) {
|
||||
console.error('定时任务执行失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// HTTP 请求处理函数 - 只负责返回数据
|
||||
async fetch(request, env, ctx) {
|
||||
if (!env || !env.R2_BUCKET) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'R2 存储桶未正确配置'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const filename = url.pathname.slice(1);
|
||||
|
||||
try {
|
||||
// 处理文件下载请求
|
||||
if (filename) {
|
||||
return await handleDownload(env, filename);
|
||||
}
|
||||
|
||||
// 只返回缓存的版本信息
|
||||
return await getCachedRelease(env);
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default worker;
|
||||
|
||||
/**
|
||||
* 添加日志记录函数
|
||||
*/
|
||||
async function addLog(env, type, event, details = null) {
|
||||
try {
|
||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
|
||||
let logs = { logs: [] };
|
||||
|
||||
if (logFile) {
|
||||
logs = JSON.parse(await logFile.text());
|
||||
}
|
||||
|
||||
logs.logs.unshift({
|
||||
timestamp: new Date().toISOString(),
|
||||
type,
|
||||
event,
|
||||
details
|
||||
});
|
||||
|
||||
// 保持日志数量在限制内
|
||||
if (logs.logs.length > config.MAX_LOGS) {
|
||||
logs.logs = logs.logs.slice(0, config.MAX_LOGS);
|
||||
}
|
||||
|
||||
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2));
|
||||
} catch (error) {
|
||||
console.error('写入日志失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并更新发布版本
|
||||
* 由定时器触发,检查新版本并更新 R2 存储
|
||||
*/
|
||||
async function checkAndUpdateRelease(env) {
|
||||
try {
|
||||
// 获取版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
let versions = { versions: {}, latestVersion: null, lastChecked: null };
|
||||
|
||||
if (versionDB) {
|
||||
versions = JSON.parse(await versionDB.text());
|
||||
}
|
||||
|
||||
// 获取 GitHub 最新版本
|
||||
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
||||
headers: { 'User-Agent': 'CloudflareWorker' },
|
||||
});
|
||||
|
||||
if (!githubResponse.ok) {
|
||||
throw new Error('GitHub API 请求失败');
|
||||
}
|
||||
|
||||
const releaseData = await githubResponse.json();
|
||||
const version = releaseData.tag_name;
|
||||
|
||||
// 更新最后检查时间
|
||||
versions.lastChecked = new Date().toISOString();
|
||||
|
||||
// 检查是否需要更新
|
||||
if (versions.latestVersion !== version) {
|
||||
await addLog(env, 'INFO', `发现新版本: ${version}`);
|
||||
|
||||
// 准备新版本记录
|
||||
const versionRecord = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
uploadedAt: null,
|
||||
files: releaseData.assets.map(asset => ({
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
uploaded: false
|
||||
})),
|
||||
changelog: releaseData.body
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
for (const asset of releaseData.assets) {
|
||||
try {
|
||||
const existingFile = await env.R2_BUCKET.get(asset.name);
|
||||
if (existingFile) {
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(asset.browser_download_url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const file = await response.arrayBuffer();
|
||||
await env.R2_BUCKET.put(asset.name, file, {
|
||||
httpMetadata: { contentType: getContentType(asset.name) }
|
||||
});
|
||||
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
|
||||
await addLog(env, 'INFO', `文件上传成功: ${asset.name}`);
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新版本记录
|
||||
versionRecord.uploadedAt = new Date().toISOString();
|
||||
versions.versions[version] = versionRecord;
|
||||
versions.latestVersion = version;
|
||||
|
||||
// 保存版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
|
||||
// 更新缓存
|
||||
const cacheData = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
changelog: releaseData.body,
|
||||
downloads: versionRecord.files
|
||||
.filter(file => file.uploaded)
|
||||
.map(file => ({
|
||||
name: file.name,
|
||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||
size: formatFileSize(file.size)
|
||||
}))
|
||||
};
|
||||
|
||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
||||
|
||||
// 清理旧版本
|
||||
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a));
|
||||
if (versionList.length > 2) {
|
||||
const oldVersions = versionList.slice(2);
|
||||
for (const oldVersion of oldVersions) {
|
||||
const oldFiles = versions.versions[oldVersion].files;
|
||||
for (const file of oldFiles) {
|
||||
if (file.uploaded) {
|
||||
await env.R2_BUCKET.delete(file.name);
|
||||
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
|
||||
}
|
||||
}
|
||||
delete versions.versions[oldVersion];
|
||||
}
|
||||
// 保存更新后的版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
}
|
||||
|
||||
return cacheData;
|
||||
} else {
|
||||
// 没有新版本,返回缓存数据
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
||||
return cached ? JSON.parse(await cached.text()) : null;
|
||||
}
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '检查更新失败', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新版本信息
|
||||
*/
|
||||
async function getLatestRelease(env) {
|
||||
try {
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
||||
if (!cached) {
|
||||
// 如果缓存不存在,先检查版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
if (versionDB) {
|
||||
const versions = JSON.parse(await versionDB.text());
|
||||
if (versions.latestVersion) {
|
||||
// 从版本数据库重建缓存
|
||||
const latestVersion = versions.versions[versions.latestVersion];
|
||||
const cacheData = {
|
||||
version: latestVersion.version,
|
||||
publishedAt: latestVersion.publishedAt,
|
||||
changelog: latestVersion.changelog,
|
||||
downloads: latestVersion.files
|
||||
.filter(file => file.uploaded)
|
||||
.map(file => ({
|
||||
name: file.name,
|
||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||
size: formatFileSize(file.size)
|
||||
}))
|
||||
};
|
||||
// 更新缓存
|
||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
||||
return new Response(JSON.stringify(cacheData), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// 如果版本数据库也没有数据,才执行检查更新
|
||||
const data = await checkAndUpdateRelease(env);
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const data = await cached.text();
|
||||
return new Response(data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '获取版本信息失败', error.message);
|
||||
return new Response(JSON.stringify({
|
||||
error: '获取版本信息失败: ' + error.message,
|
||||
detail: '请稍后再试'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 修改下载处理函数,直接接收 env
|
||||
async function handleDownload(env, filename) {
|
||||
try {
|
||||
const object = await env.R2_BUCKET.get(filename);
|
||||
|
||||
if (!object) {
|
||||
return new Response('文件未找到', { status: 404 });
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
const headers = new Headers();
|
||||
object.writeHttpMetadata(headers);
|
||||
headers.set('etag', object.httpEtag);
|
||||
headers.set('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
return new Response(object.body, {
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('下载文件时发生错误:', error);
|
||||
return new Response('获取文件失败', { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件扩展名获取对应的 Content-Type
|
||||
*/
|
||||
function getContentType(filename) {
|
||||
const ext = filename.split('.').pop().toLowerCase();
|
||||
const types = {
|
||||
'exe': 'application/x-msdownload', // Windows 可执行文件
|
||||
'dmg': 'application/x-apple-diskimage', // macOS 安装包
|
||||
'zip': 'application/zip', // 压缩包
|
||||
'AppImage': 'application/x-executable', // Linux 可执行文件
|
||||
'blockmap': 'application/octet-stream' // 更新文件
|
||||
};
|
||||
return types[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* 将字节转换为人类可读的格式(B, KB, MB, GB)
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本号比较函数
|
||||
* 用于对版本号进行排序
|
||||
*/
|
||||
function compareVersions(a, b) {
|
||||
const partsA = a.replace('v', '').split('.');
|
||||
const partsB = b.replace('v', '').split('.');
|
||||
|
||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||
const numA = parseInt(partsA[i] || 0);
|
||||
const numB = parseInt(partsB[i] || 0);
|
||||
|
||||
if (numA !== numB) {
|
||||
return numA - numB;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据文件
|
||||
*/
|
||||
async function initDataFiles(env) {
|
||||
try {
|
||||
// 检查并初始化版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
if (!versionDB) {
|
||||
const initialVersions = {
|
||||
versions: {},
|
||||
latestVersion: null,
|
||||
lastChecked: new Date().toISOString()
|
||||
};
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2));
|
||||
await addLog(env, 'INFO', 'versions.json 初始化成功');
|
||||
}
|
||||
|
||||
// 检查并初始化日志文件
|
||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
|
||||
if (!logFile) {
|
||||
const initialLogs = {
|
||||
logs: [{
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'INFO',
|
||||
event: '系统初始化'
|
||||
}]
|
||||
};
|
||||
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2));
|
||||
console.log('logs.json 初始化成功');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('初始化数据文件失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:只获取缓存的版本信息
|
||||
async function getCachedRelease(env) {
|
||||
try {
|
||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
||||
if (!cached) {
|
||||
// 如果缓存不存在,从版本数据库获取
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
if (versionDB) {
|
||||
const versions = JSON.parse(await versionDB.text());
|
||||
if (versions.latestVersion) {
|
||||
const latestVersion = versions.versions[versions.latestVersion];
|
||||
const cacheData = {
|
||||
version: latestVersion.version,
|
||||
publishedAt: latestVersion.publishedAt,
|
||||
changelog: latestVersion.changelog,
|
||||
downloads: latestVersion.files
|
||||
.filter(file => file.uploaded)
|
||||
.map(file => ({
|
||||
name: file.name,
|
||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||
size: formatFileSize(file.size)
|
||||
}))
|
||||
};
|
||||
// 重建缓存
|
||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
||||
return new Response(JSON.stringify(cacheData), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// 如果没有任何数据,返回错误
|
||||
return new Response(JSON.stringify({
|
||||
error: '没有可用的版本信息'
|
||||
}), {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 返回缓存数据
|
||||
return new Response(await cached.text(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:只检查新版本并更新
|
||||
async function checkNewRelease(env) {
|
||||
try {
|
||||
// 获取 GitHub 最新版本
|
||||
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
||||
headers: { 'User-Agent': 'CloudflareWorker' },
|
||||
});
|
||||
|
||||
if (!githubResponse.ok) {
|
||||
throw new Error('GitHub API 请求失败');
|
||||
}
|
||||
|
||||
const releaseData = await githubResponse.json();
|
||||
const version = releaseData.tag_name;
|
||||
|
||||
// 获取版本数据库
|
||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
||||
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() };
|
||||
|
||||
if (versionDB) {
|
||||
versions = JSON.parse(await versionDB.text());
|
||||
}
|
||||
|
||||
// 如果版本相同,不需要更新
|
||||
if (versions.latestVersion === version) {
|
||||
console.log('当前已是最新版本');
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog(env, 'INFO', `发现新版本: ${version}`);
|
||||
|
||||
// 准备新版本记录
|
||||
const versionRecord = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
uploadedAt: null,
|
||||
files: releaseData.assets.map(asset => ({
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
uploaded: false
|
||||
})),
|
||||
changelog: releaseData.body
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
for (const asset of releaseData.assets) {
|
||||
try {
|
||||
const existingFile = await env.R2_BUCKET.get(asset.name);
|
||||
if (existingFile) {
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const response = await fetch(asset.browser_download_url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const file = await response.arrayBuffer();
|
||||
await env.R2_BUCKET.put(asset.name, file, {
|
||||
httpMetadata: { contentType: getContentType(asset.name) }
|
||||
});
|
||||
|
||||
// 更新文件状态
|
||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
||||
if (fileIndex !== -1) {
|
||||
versionRecord.files[fileIndex].uploaded = true;
|
||||
}
|
||||
|
||||
await addLog(env, 'INFO', `文件上传成功: ${asset.name}`);
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新版本记录
|
||||
versionRecord.uploadedAt = new Date().toISOString();
|
||||
versions.versions[version] = versionRecord;
|
||||
versions.latestVersion = version;
|
||||
|
||||
// 保存版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
|
||||
// 更新缓存
|
||||
const cacheData = {
|
||||
version,
|
||||
publishedAt: releaseData.published_at,
|
||||
changelog: releaseData.body,
|
||||
downloads: versionRecord.files
|
||||
.filter(file => file.uploaded)
|
||||
.map(file => ({
|
||||
name: file.name,
|
||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||
size: formatFileSize(file.size)
|
||||
}))
|
||||
};
|
||||
|
||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
||||
|
||||
// 清理旧版本
|
||||
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a));
|
||||
if (versionList.length > 2) {
|
||||
const oldVersions = versionList.slice(2);
|
||||
for (const oldVersion of oldVersions) {
|
||||
const oldFiles = versions.versions[oldVersion].files;
|
||||
for (const file of oldFiles) {
|
||||
if (file.uploaded) {
|
||||
await env.R2_BUCKET.delete(file.name);
|
||||
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
|
||||
}
|
||||
}
|
||||
delete versions.versions[oldVersion];
|
||||
}
|
||||
// 保存更新后的版本数据库
|
||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
||||
}
|
||||
|
||||
return cacheData;
|
||||
} catch (error) {
|
||||
await addLog(env, 'ERROR', '检查新版本失败', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -1,91 +1,3 @@
|
||||
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
'.md', // Markdown 文件
|
||||
'.mdx', // Markdown 文件
|
||||
'.html', // HTML 文件
|
||||
'.htm', // HTML 文件的另一种扩展名
|
||||
'.xml', // XML 文件
|
||||
'.json', // JSON 文件
|
||||
'.yaml', // YAML 文件
|
||||
'.yml', // YAML 文件的另一种扩展名
|
||||
'.csv', // 逗号分隔值文件
|
||||
'.tsv', // 制表符分隔值文件
|
||||
'.ini', // 配置文件
|
||||
'.log', // 日志文件
|
||||
'.rtf', // 富文本格式文件
|
||||
'.tex', // LaTeX 文件
|
||||
'.srt', // 字幕文件
|
||||
'.xhtml', // XHTML 文件
|
||||
'.nfo', // 信息文件(主要用于场景发布)
|
||||
'.conf', // 配置文件
|
||||
'.config', // 配置文件
|
||||
'.env', // 环境变量文件
|
||||
'.rst', // reStructuredText 文件
|
||||
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||
'.ts', // TypeScript 文件
|
||||
'.jsp', // JavaServer Pages 文件
|
||||
'.aspx', // ASP.NET 文件
|
||||
'.bat', // Windows 批处理文件
|
||||
'.sh', // Unix/Linux Shell 脚本文件
|
||||
'.py', // Python 脚本文件
|
||||
'.rb', // Ruby 脚本文件
|
||||
'.pl', // Perl 脚本文件
|
||||
'.sql', // SQL 脚本文件
|
||||
'.css', // Cascading Style Sheets 文件
|
||||
'.less', // Less CSS 预处理器文件
|
||||
'.scss', // Sass CSS 预处理器文件
|
||||
'.sass', // Sass 文件
|
||||
'.styl', // Stylus CSS 预处理器文件
|
||||
'.coffee', // CoffeeScript 文件
|
||||
'.ino', // Arduino 代码文件
|
||||
'.asm', // Assembly 语言文件
|
||||
'.go', // Go 语言文件
|
||||
'.scala', // Scala 语言文件
|
||||
'.swift', // Swift 语言文件
|
||||
'.kt', // Kotlin 语言文件
|
||||
'.rs', // Rust 语言文件
|
||||
'.lua', // Lua 语言文件
|
||||
'.groovy', // Groovy 语言文件
|
||||
'.dart', // Dart 语言文件
|
||||
'.hs', // Haskell 语言文件
|
||||
'.clj', // Clojure 语言文件
|
||||
'.cljs', // ClojureScript 语言文件
|
||||
'.elm', // Elm 语言文件
|
||||
'.erl', // Erlang 语言文件
|
||||
'.ex', // Elixir 语言文件
|
||||
'.exs', // Elixir 脚本文件
|
||||
'.pug', // Pug (formerly Jade) 模板文件
|
||||
'.haml', // Haml 模板文件
|
||||
'.slim', // Slim 模板文件
|
||||
'.tpl', // 模板文件(通用)
|
||||
'.ejs', // Embedded JavaScript 模板文件
|
||||
'.hbs', // Handlebars 模板文件
|
||||
'.mustache', // Mustache 模板文件
|
||||
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||
'.twig', // Twig 模板文件
|
||||
'.blade', // Blade 模板文件 (Laravel)
|
||||
'.vue', // Vue.js 单文件组件
|
||||
'.jsx', // React JSX 文件
|
||||
'.tsx', // React TSX 文件
|
||||
'.graphql', // GraphQL 查询语言文件
|
||||
'.gql', // GraphQL 查询语言文件
|
||||
'.proto', // Protocol Buffers 文件
|
||||
'.thrift', // Thrift 文件
|
||||
'.toml', // TOML 配置文件
|
||||
'.edn', // Clojure 数据表示文件
|
||||
'.cake', // CakePHP 配置文件
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java' // Java 代码文件
|
||||
]
|
||||
export const isMac = process.platform === 'darwin'
|
||||
export const isWin = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
||||
9
src/main/electron.d.ts
vendored
Normal file
9
src/main/electron.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
declare global {
|
||||
namespace Electron {
|
||||
interface App {
|
||||
isQuitting: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@ -3,67 +3,67 @@ import { app, BrowserWindow } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { registerZoomShortcut } from './services/ShortcutService'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
import { createMainWindow } from './window'
|
||||
|
||||
// Check for single instance lock
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit()
|
||||
}
|
||||
process.exit(0)
|
||||
} else {
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
const mainWindow = windowService.createMainWindow()
|
||||
new TrayService()
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
windowService.createMainWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
|
||||
registerShortcuts(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', () => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
||||
if (mainWindow) {
|
||||
mainWindow.isMinimized() && mainWindow.restore()
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
app.on('before-quit', () => {
|
||||
app.isQuitting = true
|
||||
})
|
||||
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
registerZoomShortcut(mainWindow)
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', () => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
||||
if (mainWindow) {
|
||||
mainWindow.isMinimized() && mainWindow.restore()
|
||||
mainWindow.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
@ -8,8 +11,9 @@ import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { createMinappWindow } from './window'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
@ -22,25 +26,65 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged,
|
||||
appPath: app.getAppPath(),
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
|
||||
appDataPath: app.getPath('userData'),
|
||||
logsPath: log.transports.file.getFile().path
|
||||
}))
|
||||
|
||||
ipcMain.handle('app:proxy', (_, proxy: string) => session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}))
|
||||
ipcMain.handle('app:proxy', async (_, proxy: string) => {
|
||||
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
||||
const proxyConfig: ProxyConfig = proxy === 'system' ? { mode: 'system' } : proxy ? { proxyRules: proxy } : {}
|
||||
await Promise.all(sessions.map((session) => session.setProxy(proxyConfig)))
|
||||
})
|
||||
|
||||
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
||||
|
||||
// language
|
||||
ipcMain.handle('app:set-language', (_, language) => {
|
||||
configManager.setLanguage(language)
|
||||
})
|
||||
|
||||
// tray
|
||||
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
|
||||
configManager.setTray(isActive)
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle('app:set-theme', (_, theme: 'light' | 'dark') => {
|
||||
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
|
||||
configManager.setTheme(theme)
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
})
|
||||
|
||||
// clear cache
|
||||
ipcMain.handle('app:clear-cache', async () => {
|
||||
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
sessions.map(async (session) => {
|
||||
await session.clearCache()
|
||||
await session.clearStorageData({
|
||||
storages: ['cookies', 'filesystem', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
|
||||
})
|
||||
})
|
||||
)
|
||||
await fileManager.clearTemp()
|
||||
await fs.writeFileSync(log.transports.file.getFile().path, '')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
log.error('Failed to clear cache:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
// check for update
|
||||
ipcMain.handle('app:check-for-update', async () => {
|
||||
const update = await autoUpdater.checkForUpdates()
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
updateInfo: update?.updateInfo
|
||||
}
|
||||
})
|
||||
|
||||
@ -73,7 +117,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
createMinappWindow({
|
||||
windowService.createMinappWindow({
|
||||
url: args.url,
|
||||
parent: mainWindow,
|
||||
windowOptions: {
|
||||
@ -85,4 +129,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// export
|
||||
ipcMain.handle('export:word', exportService.exportToWord)
|
||||
|
||||
// open path
|
||||
ipcMain.handle('open:path', async (_, path: string) => {
|
||||
await shell.openPath(path)
|
||||
})
|
||||
|
||||
// shortcuts
|
||||
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
|
||||
configManager.setShortcuts(shortcuts)
|
||||
// Refresh shortcuts registration
|
||||
if (mainWindow) {
|
||||
unregisterAllShortcuts()
|
||||
registerShortcuts(mainWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
BIN
src/main/resources/icon.ico
Normal file
BIN
src/main/resources/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
@ -1,4 +1,4 @@
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import { app, BrowserWindow, dialog } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
|
||||
@ -6,10 +6,11 @@ export default class AppUpdater {
|
||||
autoUpdater: _AppUpdater = autoUpdater
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
logger.transports.file.level = 'debug'
|
||||
logger.transports.file.level = 'info'
|
||||
|
||||
autoUpdater.logger = logger
|
||||
autoUpdater.forceDevUpdateConfig = true
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.forceDevUpdateConfig = !app.isPackaged
|
||||
autoUpdater.autoDownload = true
|
||||
|
||||
// 检测下载错误
|
||||
autoUpdater.on('error', (error) => {
|
||||
@ -18,40 +19,8 @@ export default class AppUpdater {
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||
logger.info('检测到新版本', releaseInfo)
|
||||
mainWindow.webContents.send('update-available', releaseInfo)
|
||||
|
||||
const releaseNotes = releaseInfo.releaseNotes
|
||||
let releaseContent = ''
|
||||
|
||||
if (releaseNotes) {
|
||||
if (typeof releaseNotes === 'string') {
|
||||
releaseContent = <string>releaseNotes
|
||||
} else if (releaseNotes instanceof Array) {
|
||||
releaseNotes.forEach((releaseNote) => {
|
||||
releaseContent += `${releaseNote}\n`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
releaseContent = '暂无更新说明'
|
||||
}
|
||||
|
||||
// 弹框确认是否下载更新(releaseContent是更新日志)
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: '应用有新的更新',
|
||||
detail: releaseContent,
|
||||
message: '发现新版本,是否现在更新?',
|
||||
buttons: ['下次再说', '更新']
|
||||
})
|
||||
.then(({ response }) => {
|
||||
if (response === 1) {
|
||||
logger.info('用户选择更新,准备下载更新')
|
||||
mainWindow.webContents.send('download-update')
|
||||
autoUpdater.downloadUpdate()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 检测到不需要更新时
|
||||
@ -61,23 +30,52 @@ export default class AppUpdater {
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
logger.info('下载进度', progress)
|
||||
mainWindow.webContents.send('download-progress', progress)
|
||||
})
|
||||
|
||||
// 当需要更新的内容下载完成后
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
logger.info('下载完成,准备更新')
|
||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||
mainWindow.webContents.send('update-downloaded')
|
||||
|
||||
logger.info('下载完成,询问用户是否更新', releaseInfo)
|
||||
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: '安装更新',
|
||||
message: '更新下载完毕,应用将重启并进行安装'
|
||||
message: `新版本 ${releaseInfo.version} 已准备就绪`,
|
||||
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
|
||||
buttons: ['稍后安装', '立即安装'],
|
||||
defaultId: 1,
|
||||
cancelId: 0
|
||||
})
|
||||
.then(() => {
|
||||
setImmediate(() => autoUpdater.quitAndInstall())
|
||||
.then(({ response }) => {
|
||||
if (response === 1) {
|
||||
app.isQuitting = true
|
||||
setImmediate(() => autoUpdater.quitAndInstall())
|
||||
} else {
|
||||
mainWindow.webContents.send('update-downloaded-cancelled')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.autoUpdater = autoUpdater
|
||||
}
|
||||
|
||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||
if (!releaseNotes) {
|
||||
return '暂无更新说明'
|
||||
}
|
||||
|
||||
if (typeof releaseNotes === 'string') {
|
||||
return releaseNotes
|
||||
}
|
||||
|
||||
return releaseNotes.map((note) => note.note).join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
interface ReleaseNoteInfo {
|
||||
readonly version: string
|
||||
readonly note: string | null
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import archiver from 'archiver'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as unzipper from 'unzipper'
|
||||
|
||||
import WebDav from './WebDav'
|
||||
|
||||
@ -26,7 +25,6 @@ class BackupManager {
|
||||
destinationPath: string = this.backupDir
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 将 data 写入临时文件
|
||||
@ -38,21 +36,16 @@ class BackupManager {
|
||||
const tempDataDir = path.join(this.tempDir, 'Data')
|
||||
await fs.copy(sourcePath, tempDataDir)
|
||||
|
||||
// 创建 zip 文件
|
||||
const output = fs.createWriteStream(path.join(destinationPath, fileName))
|
||||
const archive = archiver('zip', { zlib: { level: 9 } })
|
||||
|
||||
archive.pipe(output)
|
||||
archive.directory(this.tempDir, false)
|
||||
await archive.finalize()
|
||||
// 使用 adm-zip 创建压缩文件
|
||||
const zip = new AdmZip()
|
||||
zip.addLocalFolder(this.tempDir)
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
zip.writeZip(backupedFilePath)
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('Backup completed successfully')
|
||||
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
|
||||
return backupedFilePath
|
||||
} catch (error) {
|
||||
Logger.error('Backup failed:', error)
|
||||
@ -61,31 +54,43 @@ class BackupManager {
|
||||
}
|
||||
|
||||
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
try {
|
||||
// 创建临时目录
|
||||
await fs.ensureDir(this.tempDir)
|
||||
|
||||
// 解压备份文件到临时目录
|
||||
await fs
|
||||
.createReadStream(backupPath)
|
||||
.pipe(unzipper.Extract({ path: this.tempDir }))
|
||||
.promise()
|
||||
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
|
||||
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
// 使用 adm-zip 解压
|
||||
const zip = new AdmZip(backupPath)
|
||||
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
|
||||
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
Logger.log('[backup] step 2: read data.json')
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
|
||||
Logger.log('Restore completed successfully')
|
||||
Logger.log('[backup] step 3: restore Data directory')
|
||||
|
||||
return data
|
||||
// 恢复 Data 目录
|
||||
const sourcePath = path.join(this.tempDir, 'Data')
|
||||
const destPath = path.join(app.getPath('userData'), 'Data')
|
||||
await fs.remove(destPath)
|
||||
await fs.copy(sourcePath, destPath)
|
||||
|
||||
Logger.log('[backup] step 4: clean up temp directory')
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
|
||||
Logger.log('[backup] step 5: Restore completed successfully')
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
Logger.error('[backup] Restore failed:', error)
|
||||
await fs.remove(this.tempDir).catch(() => {})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||
|
||||
@ -1,19 +1,85 @@
|
||||
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
|
||||
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
|
||||
import { app } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
|
||||
import { locales } from '../utils/locales'
|
||||
|
||||
export class ConfigManager {
|
||||
private store: Store
|
||||
private subscribers: Map<string, Array<(newValue: any) => void>> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.store = new Store()
|
||||
}
|
||||
|
||||
getTheme(): 'light' | 'dark' {
|
||||
return this.store.get('theme', 'light') as 'light' | 'dark'
|
||||
getLanguage(): LanguageVarious {
|
||||
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US'
|
||||
return this.store.get('language', locale) as LanguageVarious
|
||||
}
|
||||
|
||||
setTheme(theme: 'light' | 'dark') {
|
||||
setLanguage(theme: LanguageVarious) {
|
||||
this.store.set('language', theme)
|
||||
}
|
||||
|
||||
getTheme(): ThemeMode {
|
||||
return this.store.get('theme', ThemeMode.light) as ThemeMode
|
||||
}
|
||||
|
||||
setTheme(theme: ThemeMode) {
|
||||
this.store.set('theme', theme)
|
||||
}
|
||||
|
||||
isTray(): boolean {
|
||||
return !!this.store.get('tray', true)
|
||||
}
|
||||
|
||||
setTray(value: boolean) {
|
||||
this.store.set('tray', value)
|
||||
this.notifySubscribers('tray', value)
|
||||
}
|
||||
|
||||
getZoomFactor(): number {
|
||||
return this.store.get('zoomFactor', 1) as number
|
||||
}
|
||||
|
||||
setZoomFactor(factor: number) {
|
||||
this.store.set('zoomFactor', factor)
|
||||
this.notifySubscribers('zoomFactor', factor)
|
||||
}
|
||||
|
||||
subscribe<T>(key: string, callback: (newValue: T) => void) {
|
||||
if (!this.subscribers.has(key)) {
|
||||
this.subscribers.set(key, [])
|
||||
}
|
||||
this.subscribers.get(key)!.push(callback)
|
||||
}
|
||||
|
||||
unsubscribe<T>(key: string, callback: (newValue: T) => void) {
|
||||
const subscribers = this.subscribers.get(key)
|
||||
if (subscribers) {
|
||||
this.subscribers.set(
|
||||
key,
|
||||
subscribers.filter((subscriber) => subscriber !== callback)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private notifySubscribers<T>(key: string, newValue: T) {
|
||||
const subscribers = this.subscribers.get(key)
|
||||
if (subscribers) {
|
||||
subscribers.forEach((subscriber) => subscriber(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
getShortcuts() {
|
||||
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | []
|
||||
}
|
||||
|
||||
setShortcuts(shortcuts: Shortcut[]) {
|
||||
this.store.set('shortcuts', shortcuts)
|
||||
this.notifySubscribers('shortcuts', shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { documentExts, imageExts } from '@main/constant'
|
||||
import { getFileType } from '@main/utils/file'
|
||||
import { documentExts, imageExts } from '@shared/config/constant'
|
||||
import { FileType } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import {
|
||||
@ -267,6 +267,11 @@ class FileStorage {
|
||||
await this.initStorageDir()
|
||||
}
|
||||
|
||||
public clearTemp = async (): Promise<void> => {
|
||||
await fs.promises.rmdir(this.tempDir, { recursive: true })
|
||||
await fs.promises.mkdir(this.tempDir, { recursive: true })
|
||||
}
|
||||
|
||||
public open = async (
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
options: OpenDialogOptions
|
||||
|
||||
@ -1,66 +1,131 @@
|
||||
import { Shortcut } from '@types'
|
||||
import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
export function registerZoomShortcut(mainWindow: BrowserWindow) {
|
||||
const registerShortcuts = () => {
|
||||
// 注册放大快捷键 (Ctrl+Plus 或 Cmd+Plus)
|
||||
globalShortcut.register('CommandOrControl+=', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + 0.1
|
||||
// Prevent zoom factor from exceeding reasonable limits
|
||||
if (newZoom <= 5.0) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
import { configManager } from './ConfigManager'
|
||||
|
||||
let showAppAccelerator: string | null = null
|
||||
|
||||
function getShortcutHandler(shortcut: Shortcut) {
|
||||
switch (shortcut.key) {
|
||||
case 'zoom_in':
|
||||
return (window: BrowserWindow) => handleZoom(0.1)(window)
|
||||
case 'zoom_out':
|
||||
return (window: BrowserWindow) => handleZoom(-0.1)(window)
|
||||
case 'zoom_reset':
|
||||
return (window: BrowserWindow) => {
|
||||
window.webContents.setZoomFactor(1)
|
||||
configManager.setZoomFactor(1)
|
||||
}
|
||||
case 'show_app':
|
||||
return (window: BrowserWindow) => {
|
||||
if (window.isVisible()) {
|
||||
window.hide()
|
||||
} else {
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册缩小快捷键 (Ctrl+Minus 或 Cmd+Minus)
|
||||
globalShortcut.register('CommandOrControl+-', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom - 0.1
|
||||
// Prevent zoom factor from going below 0.1
|
||||
if (newZoom >= 0.1) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册重置缩放快捷键 (Ctrl+0 或 Cmd+0)
|
||||
globalShortcut.register('CommandOrControl+0', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomFactor(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unregisterShortcuts = () => {
|
||||
globalShortcut.unregister('CommandOrControl+=')
|
||||
globalShortcut.unregister('CommandOrControl+-')
|
||||
globalShortcut.unregister('CommandOrControl+0')
|
||||
}
|
||||
|
||||
// Add check for window destruction
|
||||
if (mainWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
// When window gains focus, register shortcuts
|
||||
mainWindow.on('focus', () => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
registerShortcuts()
|
||||
}
|
||||
})
|
||||
|
||||
// When window loses focus, unregister shortcuts
|
||||
mainWindow.on('blur', () => {
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
unregisterShortcuts()
|
||||
}
|
||||
})
|
||||
|
||||
// Initial registration (if window is already focused)
|
||||
if (!mainWindow.isDestroyed() && mainWindow.isFocused()) {
|
||||
registerShortcuts()
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatShortcutKey(shortcut: string[]): string {
|
||||
return shortcut.join('+')
|
||||
}
|
||||
|
||||
function handleZoom(delta: number) {
|
||||
return (window: BrowserWindow) => {
|
||||
const currentZoom = window.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + delta
|
||||
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
||||
window.webContents.setZoomFactor(newZoom)
|
||||
configManager.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerShortcuts(window: BrowserWindow) {
|
||||
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||
|
||||
const register = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
const shortcuts = configManager.getShortcuts()
|
||||
if (!shortcuts) return
|
||||
|
||||
shortcuts.forEach((shortcut) => {
|
||||
try {
|
||||
if (shortcut.shortcut.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const handler = getShortcutHandler(shortcut)
|
||||
|
||||
if (!handler) {
|
||||
return
|
||||
}
|
||||
|
||||
const accelerator = formatShortcutKey(shortcut.shortcut)
|
||||
|
||||
if (shortcut.key === 'show_app') {
|
||||
showAppAccelerator = accelerator
|
||||
}
|
||||
|
||||
if (shortcut.key.includes('zoom')) {
|
||||
switch (shortcut.key) {
|
||||
case 'zoom_in':
|
||||
globalShortcut.register('CommandOrControl+=', () => shortcut.enabled && handler(window))
|
||||
globalShortcut.register('CommandOrControl+numadd', () => shortcut.enabled && handler(window))
|
||||
return
|
||||
case 'zoom_out':
|
||||
globalShortcut.register('CommandOrControl+-', () => shortcut.enabled && handler(window))
|
||||
globalShortcut.register('CommandOrControl+numsub', () => shortcut.enabled && handler(window))
|
||||
return
|
||||
case 'zoom_reset':
|
||||
globalShortcut.register('CommandOrControl+0', () => shortcut.enabled && handler(window))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (shortcut.enabled) {
|
||||
globalShortcut.register(accelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unregister = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
try {
|
||||
globalShortcut.unregisterAll()
|
||||
|
||||
if (showAppAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
||||
}
|
||||
}
|
||||
|
||||
window.on('focus', () => register())
|
||||
window.on('blur', () => unregister())
|
||||
|
||||
if (!window.isDestroyed() && window.isFocused()) {
|
||||
register()
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterAllShortcuts() {
|
||||
try {
|
||||
showAppAccelerator = null
|
||||
globalShortcut.unregisterAll()
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
|
||||
}
|
||||
}
|
||||
|
||||
90
src/main/services/TrayService.ts
Normal file
90
src/main/services/TrayService.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { isMac } from '@main/constant'
|
||||
import { locales } from '@main/utils/locales'
|
||||
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron'
|
||||
|
||||
import icon from '../../../build/tray_icon.png?asset'
|
||||
import iconDark from '../../../build/tray_icon_dark.png?asset'
|
||||
import iconLight from '../../../build/tray_icon_light.png?asset'
|
||||
import { configManager } from './ConfigManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export class TrayService {
|
||||
private tray: Tray | null = null
|
||||
|
||||
constructor() {
|
||||
this.updateTray()
|
||||
this.watchTrayChanges()
|
||||
}
|
||||
|
||||
private createTray() {
|
||||
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
|
||||
const tray = new Tray(iconPath)
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
tray.setImage(iconPath)
|
||||
} else if (process.platform === 'darwin') {
|
||||
const image = nativeImage.createFromPath(iconPath)
|
||||
const resizedImage = image.resize({ width: 16, height: 16 })
|
||||
resizedImage.setTemplateImage(true)
|
||||
tray.setImage(resizedImage)
|
||||
} else if (process.platform === 'linux') {
|
||||
const image = nativeImage.createFromPath(iconPath)
|
||||
const resizedImage = image.resize({ width: 16, height: 16 })
|
||||
tray.setImage(resizedImage)
|
||||
}
|
||||
|
||||
this.tray = tray
|
||||
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { tray: trayLocale } = locale.translation
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: trayLocale.show_window,
|
||||
click: () => windowService.showMainWindow()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: trayLocale.quit,
|
||||
click: () => this.quit()
|
||||
}
|
||||
])
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
this.tray.setContextMenu(contextMenu)
|
||||
}
|
||||
|
||||
this.tray.setToolTip('Cherry Studio')
|
||||
|
||||
this.tray.on('right-click', () => {
|
||||
this.tray?.popUpContextMenu(contextMenu)
|
||||
})
|
||||
|
||||
this.tray.on('click', () => {
|
||||
windowService.showMainWindow()
|
||||
})
|
||||
}
|
||||
|
||||
private updateTray() {
|
||||
if (configManager.isTray()) {
|
||||
this.createTray()
|
||||
} else {
|
||||
this.destroyTray()
|
||||
}
|
||||
}
|
||||
|
||||
private destroyTray() {
|
||||
if (this.tray) {
|
||||
this.tray.destroy()
|
||||
this.tray = null
|
||||
}
|
||||
}
|
||||
|
||||
private watchTrayChanges() {
|
||||
configManager.subscribe<boolean>('tray', () => this.updateTray())
|
||||
}
|
||||
|
||||
private quit() {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
198
src/main/services/WindowService.ts
Normal file
198
src/main/services/WindowService.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { isLinux, isWin } from '@main/constant'
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../../build/icon.png?asset'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||
import { locales } from '../utils/locales'
|
||||
import { configManager } from './ConfigManager'
|
||||
|
||||
export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
|
||||
public static getInstance(): WindowService {
|
||||
if (!WindowService.instance) {
|
||||
WindowService.instance = new WindowService()
|
||||
}
|
||||
return WindowService.instance
|
||||
}
|
||||
|
||||
public createMainWindow(): BrowserWindow {
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
}
|
||||
})
|
||||
|
||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
public createMinappWindow({
|
||||
url,
|
||||
parent,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
parent?: BrowserWindow
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}): BrowserWindow {
|
||||
const width = windowOptions?.width || 1000
|
||||
const height = windowOptions?.height || 680
|
||||
|
||||
const minappWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Cherry Studio',
|
||||
...windowOptions,
|
||||
parent,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
minappWindow.loadURL(url)
|
||||
return minappWindow
|
||||
}
|
||||
|
||||
private setupMainWindow(mainWindow: BrowserWindow, mainWindowState: any) {
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
this.setupContextMenu(mainWindow)
|
||||
this.setupWindowEvents(mainWindow)
|
||||
this.setupWebContentsHandlers(mainWindow)
|
||||
this.setupWindowLifecycleEvents(mainWindow)
|
||||
this.loadMainWindowContent(mainWindow)
|
||||
}
|
||||
|
||||
private setupContextMenu(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { common } = locale.translation
|
||||
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: common.copy, role: 'copy' }))
|
||||
menu.append(new MenuItem({ label: common.paste, role: 'paste' }))
|
||||
menu.append(new MenuItem({ label: common.cut, role: 'cut' }))
|
||||
menu.popup()
|
||||
})
|
||||
}
|
||||
|
||||
private setupWindowEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
}
|
||||
|
||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
this.setupWebRequestHeaders(mainWindow)
|
||||
}
|
||||
|
||||
private setupWebRequestHeaders(mainWindow: BrowserWindow) {
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||
delete details.responseHeaders['X-Frame-Options']
|
||||
}
|
||||
if (details.responseHeaders?.['x-frame-options']) {
|
||||
delete details.responseHeaders['x-frame-options']
|
||||
}
|
||||
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||
delete details.responseHeaders['Content-Security-Policy']
|
||||
}
|
||||
if (details.responseHeaders?.['content-security-policy']) {
|
||||
delete details.responseHeaders['content-security-policy']
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||
})
|
||||
}
|
||||
|
||||
private loadMainWindowContent(mainWindow: BrowserWindow) {
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
public getMainWindow(): BrowserWindow | null {
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('close', (event) => {
|
||||
const notInTray = !configManager.isTray()
|
||||
|
||||
// Windows and Linux
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// Mac
|
||||
if (!app.isQuitting) {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public showMainWindow() {
|
||||
if (this.mainWindow) {
|
||||
if (this.mainWindow.isMinimized()) {
|
||||
return this.mainWindow.restore()
|
||||
}
|
||||
this.mainWindow.show()
|
||||
this.mainWindow.focus()
|
||||
} else {
|
||||
this.createMainWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const windowService = WindowService.getInstance()
|
||||
@ -1,6 +1,5 @@
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
|
||||
|
||||
import { FileTypes } from '../../renderer/src/types'
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
|
||||
import { FileTypes } from '@types'
|
||||
|
||||
export function getFileType(ext: string): FileTypes {
|
||||
ext = ext.toLowerCase()
|
||||
|
||||
13
src/main/utils/locales.ts
Normal file
13
src/main/utils/locales.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
|
||||
import RuRu from '../../renderer/src/i18n/locales/ru-ru.json'
|
||||
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
|
||||
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
|
||||
|
||||
const locales = {
|
||||
'en-US': EnUs,
|
||||
'zh-CN': ZhCn,
|
||||
'zh-TW': ZhTw,
|
||||
'ru-RU': RuRu
|
||||
}
|
||||
|
||||
export { locales }
|
||||
16
src/main/utils/windowUtil.ts
Normal file
16
src/main/utils/windowUtil.ts
Normal file
@ -0,0 +1,16 @@
|
||||
function isTilingWindowManager() {
|
||||
if (process.platform === 'darwin') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (process.platform !== 'linux') {
|
||||
return true
|
||||
}
|
||||
|
||||
const desktopEnv = process.env.XDG_CURRENT_DESKTOP?.toLowerCase()
|
||||
const tilingSystems = ['hyprland', 'i3', 'sway', 'bspwm', 'dwm', 'awesome', 'qtile', 'herbstluftwm', 'xmonad']
|
||||
|
||||
return tilingSystems.some((system) => desktopEnv?.includes(system))
|
||||
}
|
||||
|
||||
export { isTilingWindowManager }
|
||||
@ -1,128 +0,0 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
|
||||
export function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
|
||||
// Create the browser window.
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'fullscreen-ui',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
mainWindow.webContents.on('context-menu', () => {
|
||||
const menu = new Menu()
|
||||
menu.append(new MenuItem({ label: '复制', role: 'copy' }))
|
||||
menu.append(new MenuItem({ label: '粘贴', role: 'paste' }))
|
||||
menu.append(new MenuItem({ label: '剪切', role: 'cut' }))
|
||||
menu.popup()
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||
delete details.responseHeaders['X-Frame-Options']
|
||||
}
|
||||
if (details.responseHeaders?.['x-frame-options']) {
|
||||
delete details.responseHeaders['x-frame-options']
|
||||
}
|
||||
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||
delete details.responseHeaders['Content-Security-Policy']
|
||||
}
|
||||
if (details.responseHeaders?.['content-security-policy']) {
|
||||
delete details.responseHeaders['content-security-policy']
|
||||
}
|
||||
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
export function createMinappWindow({
|
||||
url,
|
||||
parent,
|
||||
windowOptions
|
||||
}: {
|
||||
url: string
|
||||
parent?: BrowserWindow
|
||||
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||
}) {
|
||||
const width = windowOptions?.width || 1000
|
||||
const height = windowOptions?.height || 680
|
||||
|
||||
const minappWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
autoHideMenuBar: true,
|
||||
title: 'Cherry Studio',
|
||||
...windowOptions,
|
||||
parent,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/minapp.js'),
|
||||
sandbox: false,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
minappWindow.loadURL(url)
|
||||
|
||||
return minappWindow
|
||||
}
|
||||
15
src/preload/index.d.ts
vendored
15
src/preload/index.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import { AppInfo, LanguageVarious } from '@renderer/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
@ -8,18 +9,16 @@ declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
appPath: string
|
||||
filesPath: string
|
||||
}>
|
||||
getAppInfo: () => Promise<AppInfo>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
setLanguage: (theme: LanguageVarious) => void
|
||||
setTray: (isActive: boolean) => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||
reload: () => void
|
||||
clearCache: () => Promise<{ success: boolean; error?: string }>
|
||||
zip: {
|
||||
compress: (text: string) => Promise<Buffer>
|
||||
decompress: (text: Buffer) => Promise<string>
|
||||
@ -54,6 +53,10 @@ declare global {
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
}
|
||||
openPath: (path: string) => Promise<void>
|
||||
shortcuts: {
|
||||
update: (shortcuts: Shortcut[]) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { WebDavConfig } from '@types'
|
||||
import { Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@ -8,9 +8,12 @@ const api = {
|
||||
reload: () => ipcRenderer.invoke('app:reload'),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
|
||||
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
||||
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
|
||||
zip: {
|
||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||
@ -43,6 +46,10 @@ const api = {
|
||||
},
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
|
||||
},
|
||||
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
|
||||
shortcuts: {
|
||||
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
||||
@ -8,11 +8,11 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HistoryPage from './pages/history/HistoryPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
@ -23,23 +23,24 @@ function App(): JSX.Element {
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/messages/*" element={<HistoryPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
<SyntaxHighlighterProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</SyntaxHighlighterProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'iconfont'; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||
font-family: 'iconfont'; /* Project id 4753420 */
|
||||
src: url('iconfont.woff2?t=1733224456443') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -11,6 +11,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-at1:before {
|
||||
content: '\e7df';
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: '\e630';
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: '\e6cd';
|
||||
}
|
||||
@ -27,10 +35,6 @@
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-grid-row-2copy:before {
|
||||
content: '\e681';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: '\e869';
|
||||
}
|
||||
@ -71,10 +75,6 @@
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: '\e658';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
src/renderer/src/assets/images/apps/duckduckgo.webp
Normal file
BIN
src/renderer/src/assets/images/apps/duckduckgo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@ -42,35 +42,6 @@
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.segmented-tab {
|
||||
.ant-segmented-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-segmented-item-selected {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
.ant-segmented-item-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 13px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.anticon-setting {
|
||||
font-size: 12px;
|
||||
}
|
||||
.icon-business-smart-assistant {
|
||||
margin-right: -2px;
|
||||
}
|
||||
.ant-segmented-item-icon + * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-attachments {
|
||||
.ant-upload-list-item:hover {
|
||||
background-color: initial !important;
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
--color-white-mute: rgba(255, 255, 255, 0.94);
|
||||
|
||||
--color-black: #151515;
|
||||
--color-black-soft: #202020;
|
||||
--color-black-mute: #262626;
|
||||
--color-black-soft: #222222;
|
||||
--color-black-mute: #333333;
|
||||
|
||||
--color-gray-1: #515c67;
|
||||
--color-gray-2: #414853;
|
||||
@ -32,14 +32,16 @@
|
||||
--color-text: var(--color-text-1);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff24;
|
||||
--color-border-soft: #ffffff20;
|
||||
--color-border: #ffffff22;
|
||||
--color-border-soft: #ffffff11;
|
||||
--color-border-mute: #ffffff11;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #323232;
|
||||
--color-hover: rgba(40, 40, 40, 1);
|
||||
--color-active: rgba(55, 55, 55, 1);
|
||||
--color-frame-border: #333;
|
||||
--color-group-background: var(--color-background-soft);
|
||||
|
||||
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
||||
--navbar-background: rgba(30, 30, 30);
|
||||
@ -88,13 +90,15 @@ body[theme-mode='light'] {
|
||||
--color-icon: #00000099;
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000028;
|
||||
--color-border-soft: #00000028;
|
||||
--color-border-mute: #00000011;
|
||||
--color-border-soft: #00000020;
|
||||
--color-border-mute: #00000010;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-hover: var(--color-white-mute);
|
||||
--color-active: var(--color-white-soft);
|
||||
--color-frame-border: #ddd;
|
||||
--color-group-background: var(--color-white);
|
||||
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
||||
--navbar-background: rgba(255, 255, 255);
|
||||
@ -168,8 +172,8 @@ body,
|
||||
body[os='mac'] {
|
||||
#content-container {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,3 +212,43 @@ body[os='windows'] {
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background-color: var(--chat-background);
|
||||
#chat-main {
|
||||
background-color: var(--chat-background);
|
||||
}
|
||||
#messages {
|
||||
background-color: var(--chat-background);
|
||||
}
|
||||
#inputbar {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
background: var(--color-background);
|
||||
}
|
||||
.system-prompt {
|
||||
background-color: var(--chat-background-assistant);
|
||||
}
|
||||
.message-content-container {
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
.message-user {
|
||||
color: var(--chat-text-user);
|
||||
.markdown,
|
||||
.anticon,
|
||||
.iconfont,
|
||||
.message-tokens {
|
||||
color: var(--chat-text-user) !important;
|
||||
}
|
||||
.message-action-button:hover {
|
||||
background-color: var(--color-white-soft);
|
||||
}
|
||||
}
|
||||
code {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,10 +98,6 @@
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
p code,
|
||||
li code {
|
||||
background: var(--color-background-mute);
|
||||
@ -111,11 +107,18 @@
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||
background-color: var(--color-background-mute);
|
||||
&:has(> .mermaid) {
|
||||
background-color: transparent;
|
||||
}
|
||||
&:not(pre pre) {
|
||||
> code:not(pre pre > code) {
|
||||
padding: 15px;
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
:root {
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);
|
||||
--color-scrollbar-thumb-right: rgba(255, 255, 255, 0.25);
|
||||
--color-scrollbar-thumb-right-hover: rgba(255, 255, 255, 0.35);
|
||||
--color-scrollbar-thumb-right: rgba(255, 255, 255, 0.18);
|
||||
--color-scrollbar-thumb-right-hover: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.2);
|
||||
--color-scrollbar-thumb-right: rgba(0, 0, 0, 0.25);
|
||||
--color-scrollbar-thumb-right-hover: rgba(0, 0, 0, 0.35);
|
||||
--color-scrollbar-thumb-right: rgba(0, 0, 0, 0.18);
|
||||
--color-scrollbar-thumb-right-hover: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* 全局初始化滚动条样式 */
|
||||
|
||||
@ -19,6 +19,8 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
|
||||
const [messages, setMessagess] = useState<AssistantMessage[]>(assistant?.messages || [])
|
||||
const [hideMessages, setHideMessages] = useState(assistant?.settings?.hideMessages || false)
|
||||
|
||||
const showSaveButton = (assistant?.messages || []).length !== messages.length
|
||||
|
||||
const onSave = () => {
|
||||
// 检查是否有空对话组
|
||||
for (let i = 0; i < messages.length; i += 2) {
|
||||
@ -129,7 +131,7 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
|
||||
</Form.Item>
|
||||
<Divider style={{ marginBottom: 15 }} />
|
||||
<Form.Item>
|
||||
{messages.length > 0 && (
|
||||
{showSaveButton && (
|
||||
<Button type="primary" onClick={onSave}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||
import { Button, Col, Divider, Row, Slider, Switch, Tooltip } from 'antd'
|
||||
@ -19,7 +19,7 @@ interface Props {
|
||||
|
||||
const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
|
||||
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
||||
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false)
|
||||
@ -33,7 +33,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
}
|
||||
}
|
||||
|
||||
const onConextCountChange = (value) => {
|
||||
const onContextCountChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
updateAssistantSettings({ contextCount: value })
|
||||
}
|
||||
@ -47,13 +47,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_TEMPERATURE)
|
||||
setConextCount(DEFAULT_CONEXTCOUNT)
|
||||
setContextCount(DEFAULT_CONTEXTCOUNT)
|
||||
setEnableMaxTokens(false)
|
||||
setMaxTokens(0)
|
||||
setStreamOutput(true)
|
||||
updateAssistantSettings({
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
contextCount: DEFAULT_CONEXTCOUNT,
|
||||
contextCount: DEFAULT_CONTEXTCOUNT,
|
||||
enableMaxTokens: false,
|
||||
maxTokens: 0,
|
||||
streamOutput: true
|
||||
@ -122,8 +122,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
</Row>
|
||||
<Row align="middle">
|
||||
<Label>
|
||||
{t('chat.settings.conext_count')}{' '}
|
||||
<Tooltip title={t('chat.settings.conext_count.tip')}>
|
||||
{t('chat.settings.context_count')}{' '}
|
||||
<Tooltip title={t('chat.settings.context_count.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Label>
|
||||
@ -133,8 +133,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Slider
|
||||
min={0}
|
||||
max={20}
|
||||
onChange={setConextCount}
|
||||
onChangeComplete={onConextCountChange}
|
||||
onChange={setContextCount}
|
||||
onChangeComplete={onContextCountChange}
|
||||
value={typeof contextCount === 'number' ? contextCount : 0}
|
||||
step={1}
|
||||
/>
|
||||
|
||||
@ -74,7 +74,8 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, ...props })
|
||||
content: {
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
background: 'var(--color-background)'
|
||||
background: 'var(--color-background)',
|
||||
border: `1px solid var(--color-frame-border)`
|
||||
},
|
||||
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 }
|
||||
}}
|
||||
@ -142,10 +143,11 @@ const StyledModal = styled(Modal)`
|
||||
}
|
||||
.ant-menu-item {
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
color: var(--color-text-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0.5px solid transparent;
|
||||
border-radius: 6px;
|
||||
.ant-menu-title-content {
|
||||
line-height: 36px;
|
||||
}
|
||||
@ -156,6 +158,7 @@ const StyledModal = styled(Modal)`
|
||||
}
|
||||
.ant-menu-item-selected {
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
.ant-menu-title-content {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
|
||||
@ -114,7 +114,15 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} />
|
||||
</EmptyView>
|
||||
)}
|
||||
{opened && <webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />}
|
||||
{opened && (
|
||||
<webview
|
||||
src={app.url}
|
||||
ref={webviewRef}
|
||||
style={WebviewStyle}
|
||||
allowpopups={'true' as any}
|
||||
partition="persist:webview"
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -32,9 +32,22 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const agents = useMemo(() => {
|
||||
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||
const list = [defaultAssistant, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
|
||||
return searchText
|
||||
const filtered = searchText
|
||||
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
|
||||
: list
|
||||
|
||||
if (searchText.trim()) {
|
||||
const newAgent: Agent = {
|
||||
id: 'new',
|
||||
name: searchText.trim(),
|
||||
prompt: '',
|
||||
topics: [],
|
||||
type: 'assistant',
|
||||
emoji: '⭐️'
|
||||
}
|
||||
return [newAgent, ...filtered]
|
||||
}
|
||||
return filtered
|
||||
}, [assistants, defaultAssistant, searchText, userAgents])
|
||||
|
||||
const onCreateAssistant = async (agent: Agent) => {
|
||||
@ -72,7 +85,14 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="ant-move-up"
|
||||
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
|
||||
styles={{
|
||||
content: {
|
||||
borderRadius: 20,
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 20
|
||||
}
|
||||
}}
|
||||
closeIcon={null}
|
||||
footer={null}>
|
||||
<HStack style={{ padding: '0 12px', marginTop: 5 }}>
|
||||
@ -105,6 +125,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
</HStack>
|
||||
{agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
|
||||
{agent.type === 'agent' && <Tag color="orange">{t('agents.tag.agent')}</Tag>}
|
||||
{agent.id === 'new' && <Tag color="green">{t('agents.tag.new')}</Tag>}
|
||||
</AgentItem>
|
||||
))}
|
||||
</Container>
|
||||
@ -148,7 +169,7 @@ const SearchIcon = styled.div`
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-background-soft);
|
||||
background-color: var(--color-background-mute);
|
||||
margin-right: 2px;
|
||||
`
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Input, InputProps, Modal } from 'antd'
|
||||
import { Input, Modal } from 'antd'
|
||||
import { TextAreaProps } from 'antd/es/input'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
@ -9,7 +10,7 @@ interface PromptPopupShowParams {
|
||||
message: string
|
||||
defaultValue?: string
|
||||
inputPlaceholder?: string
|
||||
inputProps?: InputProps
|
||||
inputProps?: TextAreaProps
|
||||
}
|
||||
|
||||
interface Props extends PromptPopupShowParams {
|
||||
@ -42,13 +43,14 @@ const PromptPopupContainer: React.FC<Props> = ({
|
||||
return (
|
||||
<Modal title={title} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose} centered>
|
||||
<Box mb={8}>{message}</Box>
|
||||
<Input
|
||||
<Input.TextArea
|
||||
placeholder={inputPlaceholder}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
allowClear
|
||||
autoFocus
|
||||
onPressEnter={onOk}
|
||||
rows={1}
|
||||
{...inputProps}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
69
src/renderer/src/components/Popups/SearchPopup.tsx
Normal file
69
src/renderer/src/components/Popups/SearchPopup.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import HistoryPage from '@renderer/pages/history/HistoryPage'
|
||||
import { Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface Props {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
SearchPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
title={null}
|
||||
width="920px"
|
||||
transitionName="ant-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
padding: 0,
|
||||
border: `1px solid var(--color-frame-border)`
|
||||
},
|
||||
body: { height: '85vh' }
|
||||
}}
|
||||
centered
|
||||
footer={null}>
|
||||
<HistoryPage />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class SearchPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('SearchPopup')
|
||||
}
|
||||
static show() {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide('SearchPopup')
|
||||
}}
|
||||
/>,
|
||||
'SearchPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { getModelLogo, isVisionModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
@ -30,14 +31,40 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const { providers } = useProviders()
|
||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const loadPinnedModels = async () => {
|
||||
const setting = await db.settings.get('pinned:models')
|
||||
const savedPinnedModels = setting?.value || []
|
||||
|
||||
// Filter out invalid pinned models
|
||||
const allModelIds = providers.flatMap((p) => p.models || []).map((m) => getModelUniqId(m))
|
||||
const validPinnedModels = savedPinnedModels.filter((id) => allModelIds.includes(id))
|
||||
|
||||
// Update storage if there were invalid models
|
||||
if (validPinnedModels.length !== savedPinnedModels.length) {
|
||||
await db.settings.put({ id: 'pinned:models', value: validPinnedModels })
|
||||
}
|
||||
|
||||
setPinnedModels(validPinnedModels)
|
||||
}
|
||||
loadPinnedModels()
|
||||
}, [providers])
|
||||
|
||||
const togglePin = async (modelId: string) => {
|
||||
const newPinnedModels = pinnedModels.includes(modelId)
|
||||
? pinnedModels.filter((id) => id !== modelId)
|
||||
: [...pinnedModels, modelId]
|
||||
|
||||
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
|
||||
setPinnedModels(newPinnedModels)
|
||||
}
|
||||
|
||||
const filteredItems: MenuItem[] = providers
|
||||
.filter((p) => p.models && p.models.length > 0)
|
||||
.map((p) => ({
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: reverse(sortBy(p.models, 'name'))
|
||||
.map((p) => {
|
||||
const filteredModels = reverse(sortBy(p.models, 'name'))
|
||||
.filter((m) =>
|
||||
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
@ -45,7 +72,17 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
key: getModelUniqId(m),
|
||||
label: (
|
||||
<ModelItem>
|
||||
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
||||
<span>
|
||||
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
||||
</span>
|
||||
<PinIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
togglePin(getModelUniqId(m))
|
||||
}}
|
||||
isPinned={pinnedModels.includes(getModelUniqId(m))}>
|
||||
<PushpinOutlined />
|
||||
</PinIcon>
|
||||
</ModelItem>
|
||||
),
|
||||
icon: (
|
||||
@ -58,8 +95,58 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
setOpen(false)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
.filter((item) => item.children && item.children.length > 0) as MenuItem[]
|
||||
|
||||
// Only return the group if it has filtered models
|
||||
return filteredModels.length > 0
|
||||
? {
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: filteredModels
|
||||
}
|
||||
: null
|
||||
})
|
||||
.filter(Boolean) as MenuItem[] // Filter out null items
|
||||
|
||||
if (pinnedModels.length > 0 && searchText.length === 0) {
|
||||
const pinnedItems = providers
|
||||
.flatMap((p) => p.models || [])
|
||||
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||
.map((m) => ({
|
||||
key: getModelUniqId(m),
|
||||
label: (
|
||||
<ModelItem>
|
||||
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
||||
<PinIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
togglePin(getModelUniqId(m))
|
||||
}}
|
||||
isPinned={true}>
|
||||
<PushpinOutlined />
|
||||
</PinIcon>
|
||||
</ModelItem>
|
||||
),
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
||||
{first(m?.name)}
|
||||
</Avatar>
|
||||
),
|
||||
onClick: () => {
|
||||
resolve(m)
|
||||
setOpen(false)
|
||||
}
|
||||
}))
|
||||
|
||||
if (pinnedItems.length > 0) {
|
||||
filteredItems.unshift({
|
||||
key: 'pinned',
|
||||
label: t('model.pinned'),
|
||||
type: 'group',
|
||||
children: pinnedItems
|
||||
} as MenuItem)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
@ -81,7 +168,15 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="ant-move-down"
|
||||
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
|
||||
styles={{
|
||||
content: {
|
||||
borderRadius: 20,
|
||||
padding: 0,
|
||||
overflow: 'hidden',
|
||||
paddingBottom: 20,
|
||||
border: '1px solid var(--color-border)'
|
||||
}
|
||||
}}
|
||||
closeIcon={null}
|
||||
footer={null}>
|
||||
<HStack style={{ padding: '0 12px', marginTop: 5 }}>
|
||||
@ -141,6 +236,23 @@ const StyledMenu = styled(Menu)`
|
||||
.ant-menu-item {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background-color: var(--color-background-mute) !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
}
|
||||
|
||||
&:not([data-menu-id^='pinned-']) {
|
||||
.pin-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-icon {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -148,6 +260,8 @@ const ModelItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const EmptyState = styled.div`
|
||||
@ -169,8 +283,23 @@ const SearchIcon = styled.div`
|
||||
margin-right: 2px;
|
||||
`
|
||||
|
||||
const PinIcon = styled.span.attrs({ className: 'pin-icon' })<{ isPinned: boolean }>`
|
||||
margin-left: auto;
|
||||
padding: 0 8px;
|
||||
opacity: ${(props) => (props.isPinned ? 1 : 'inherit')};
|
||||
transition: opacity 0.2s;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
color: ${(props) => (props.isPinned ? 'var(--color-primary)' : 'inherit')};
|
||||
transform: ${(props) => (props.isPinned ? 'rotate(-45deg)' : 'none')};
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
color: ${(props) => (props.isPinned ? 'var(--color-primary)' : 'inherit')};
|
||||
}
|
||||
`
|
||||
|
||||
export default class SelectModelPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('SelectModelPopup')
|
||||
}
|
||||
|
||||
@ -27,17 +27,28 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
TemplatePopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose}>
|
||||
<Modal
|
||||
title={title}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="ant-move-down"
|
||||
centered>
|
||||
<Box mb={8}>Name</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'TemplatePopup'
|
||||
|
||||
export default class TemplatePopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('TemplatePopup')
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
@ -46,10 +57,10 @@ export default class TemplatePopup {
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
'TemplatePopup'
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -49,6 +49,8 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
setTimeout(resizeTextArea, 0)
|
||||
}, [])
|
||||
|
||||
TextEditPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('common.edit')}
|
||||
@ -75,10 +77,12 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'TextEditPopup'
|
||||
|
||||
export default class TextEditPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('TextEditPopup')
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
@ -87,10 +91,10 @@ export default class TextEditPopup {
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
'TextEditPopup'
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,27 +1,41 @@
|
||||
import { TranslationOutlined } from '@ant-design/icons'
|
||||
import { LoadingOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { getUserMessage } from '@renderer/services/MessagesService'
|
||||
import { Button } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
text?: string
|
||||
onTranslated: (translatedText: string) => void
|
||||
disabled?: boolean
|
||||
style?: React.CSSProperties
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) => {
|
||||
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoading }) => {
|
||||
const { t } = useTranslation()
|
||||
const { translateModel } = useDefaultModel()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
|
||||
const translateConfirm = () => {
|
||||
return window?.modal?.confirm({
|
||||
title: t('translate.confirm.title'),
|
||||
content: t('translate.confirm.content'),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
|
||||
const handleTranslate = async () => {
|
||||
if (!text?.trim()) return
|
||||
|
||||
if (!(await translateConfirm())) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!translateModel) {
|
||||
window.message.error({
|
||||
content: t('translate.error.not_configured'),
|
||||
@ -39,7 +53,8 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) =>
|
||||
const message = getUserMessage({
|
||||
assistant,
|
||||
topic: getDefaultTopic('default'),
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
content: text
|
||||
})
|
||||
|
||||
const translatedText = await fetchTranslate({ message, assistant })
|
||||
@ -55,16 +70,53 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) =>
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsTranslating(isLoading ?? false)
|
||||
}, [isLoading])
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<TranslationOutlined style={{ fontSize: 14 }} />}
|
||||
onClick={handleTranslate}
|
||||
disabled={disabled || isTranslating}
|
||||
loading={isTranslating}
|
||||
style={style}
|
||||
size="small"
|
||||
/>
|
||||
<Tooltip placement="top" title={t('chat.input.translate')} arrow>
|
||||
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
|
||||
{isTranslating ? <LoadingOutlined spin /> : <TranslationOutlined />}
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarButton = styled(Button)`
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
font-size: 17px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
&.anticon,
|
||||
&.iconfont {
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon,
|
||||
.iconfont {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-primary) !important;
|
||||
.anticon,
|
||||
.iconfont {
|
||||
color: var(--color-white-soft);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default TranslateButton
|
||||
|
||||
@ -38,7 +38,6 @@ const NavbarContainer = styled.div`
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
||||
transition: background-color 0.3s ease;
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Avatar } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -50,57 +51,67 @@ const Sidebar: FC = () => {
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
<MainMenus>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<StyledLink onClick={() => to('/')}>
|
||||
<Icon className={isRoute('/')}>
|
||||
<i className="iconfont icon-chat" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink onClick={() => to('/agents')}>
|
||||
<Icon className={isRoutes('/agents')}>
|
||||
<i className="iconfont icon-business-smart-assistant" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink onClick={() => to('/paintings')}>
|
||||
<Icon className={isRoute('/paintings')}>
|
||||
<PictureOutlined style={{ fontSize: 16 }} />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink onClick={() => to('/translate')}>
|
||||
<Icon className={isRoute('/translate')}>
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink onClick={() => to('/apps')}>
|
||||
<Icon className={isRoute('/apps')}>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink onClick={() => to('/files')}>
|
||||
<Icon className={isRoute('/files')}>
|
||||
<FolderOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink onClick={() => to('/messages')}>
|
||||
<Icon className={isRoutes('/messages')}>
|
||||
<FileSearchOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/')}>
|
||||
<Icon className={isRoute('/')}>
|
||||
<i className="iconfont icon-chat" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/agents')}>
|
||||
<Icon className={isRoutes('/agents')}>
|
||||
<i className="iconfont icon-business-smart-assistant" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/paintings')}>
|
||||
<Icon className={isRoute('/paintings')}>
|
||||
<PictureOutlined style={{ fontSize: 16 }} />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/translate')}>
|
||||
<Icon className={isRoute('/translate')}>
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/apps')}>
|
||||
<Icon className={isRoute('/apps')}>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/files')}>
|
||||
<Icon className={isRoute('/files')}>
|
||||
<FolderOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<Icon onClick={() => toggleTheme()}>
|
||||
{theme === 'dark' ? (
|
||||
<i className="iconfont icon-theme icon-dark1" />
|
||||
) : (
|
||||
<i className="iconfont icon-theme icon-theme-light" />
|
||||
)}
|
||||
</Icon>
|
||||
|
||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting" />
|
||||
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<Icon onClick={() => toggleTheme()}>
|
||||
{theme === 'dark' ? (
|
||||
<i className="iconfont icon-theme icon-dark1" />
|
||||
) : (
|
||||
<i className="iconfont icon-theme icon-theme-light" />
|
||||
)}
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
</Menus>
|
||||
</Container>
|
||||
)
|
||||
@ -116,7 +127,6 @@ const Container = styled.div`
|
||||
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
|
||||
-webkit-app-region: drag !important;
|
||||
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
|
||||
transition: background-color 0.3s ease;
|
||||
`
|
||||
|
||||
const AvatarImg = styled(Avatar)`
|
||||
@ -147,14 +157,12 @@ const Icon = styled.div`
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 5px;
|
||||
transition: background-color 0.2s ease;
|
||||
-webkit-app-region: none;
|
||||
transition: all 0.2s ease;
|
||||
border: 0.5px solid transparent;
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 20px;
|
||||
transition: color 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
.anticon {
|
||||
@ -170,6 +178,7 @@ const Icon = styled.div`
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-active);
|
||||
border: 0.5px solid var(--color-border);
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon-white);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,99 +1,10 @@
|
||||
export const DEFAULT_TEMPERATURE = 0.7
|
||||
export const DEFAULT_CONEXTCOUNT = 5
|
||||
export const DEFAULT_CONTEXTCOUNT = 5
|
||||
export const DEFAULT_MAX_TOKENS = 4096
|
||||
export const FONT_FAMILY =
|
||||
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
||||
|
||||
export const platform = window.electron?.process?.platform
|
||||
export const isMac = platform === 'darwin'
|
||||
export const isWindows = platform === 'win32' || platform === 'win64'
|
||||
export const isLinux = platform === 'linux'
|
||||
|
||||
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
'.md', // Markdown 文件
|
||||
'.mdx', // Markdown 文件
|
||||
'.html', // HTML 文件
|
||||
'.htm', // HTML 文件的另一种扩展名
|
||||
'.xml', // XML 文件
|
||||
'.json', // JSON 文件
|
||||
'.yaml', // YAML 文件
|
||||
'.yml', // YAML 文件的另一种扩展名
|
||||
'.csv', // 逗号分隔值文件
|
||||
'.tsv', // 制表符分隔值文件
|
||||
'.ini', // 配置文件
|
||||
'.log', // 日志文件
|
||||
'.rtf', // 富文本格式文件
|
||||
'.tex', // LaTeX 文件
|
||||
'.srt', // 字幕文件
|
||||
'.xhtml', // XHTML 文件
|
||||
'.nfo', // 信息文件(主要用于场景发布)
|
||||
'.conf', // 配置文件
|
||||
'.config', // 配置文件
|
||||
'.env', // 环境变量文件
|
||||
'.rst', // reStructuredText 文件
|
||||
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||
'.ts', // TypeScript 文件
|
||||
'.jsp', // JavaServer Pages 文件
|
||||
'.aspx', // ASP.NET 文件
|
||||
'.bat', // Windows 批处理文件
|
||||
'.sh', // Unix/Linux Shell 脚本文件
|
||||
'.py', // Python 脚本文件
|
||||
'.rb', // Ruby 脚本文件
|
||||
'.pl', // Perl 脚本文件
|
||||
'.sql', // SQL 脚本文件
|
||||
'.css', // Cascading Style Sheets 文件
|
||||
'.less', // Less CSS 预处理器文件
|
||||
'.scss', // Sass CSS 预处理器文件
|
||||
'.sass', // Sass 文件
|
||||
'.styl', // Stylus CSS 预处理器文件
|
||||
'.coffee', // CoffeeScript 文件
|
||||
'.ino', // Arduino 代码文件
|
||||
'.asm', // Assembly 语言文件
|
||||
'.go', // Go 语言文件
|
||||
'.scala', // Scala 语言文件
|
||||
'.swift', // Swift 语言文件
|
||||
'.kt', // Kotlin 语言文件
|
||||
'.rs', // Rust 语言文件
|
||||
'.lua', // Lua 语言文件
|
||||
'.groovy', // Groovy 语言文件
|
||||
'.dart', // Dart 语言文件
|
||||
'.hs', // Haskell 语言文件
|
||||
'.clj', // Clojure 语言文件
|
||||
'.cljs', // ClojureScript 语言文件
|
||||
'.elm', // Elm 语言文件
|
||||
'.erl', // Erlang 语言文件
|
||||
'.ex', // Elixir 语言文件
|
||||
'.exs', // Elixir 脚本文件
|
||||
'.pug', // Pug (formerly Jade) 模板文件
|
||||
'.haml', // Haml 模板文件
|
||||
'.slim', // Slim 模板文件
|
||||
'.tpl', // 模板文件(通用)
|
||||
'.ejs', // Embedded JavaScript 模板文件
|
||||
'.hbs', // Handlebars 模板文件
|
||||
'.mustache', // Mustache 模板文件
|
||||
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||
'.twig', // Twig 模板文件
|
||||
'.blade', // Blade 模板文件 (Laravel)
|
||||
'.vue', // Vue.js 单文件组件
|
||||
'.jsx', // React JSX 文件
|
||||
'.tsx', // React TSX 文件
|
||||
'.graphql', // GraphQL 查询语言文件
|
||||
'.gql', // GraphQL 查询语言文件
|
||||
'.proto', // Protocol Buffers 文件
|
||||
'.thrift', // Thrift 文件
|
||||
'.toml', // TOML 配置文件
|
||||
'.edn', // Clojure 数据表示文件
|
||||
'.cake', // CakePHP 配置文件
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java' // Java 代码文件
|
||||
]
|
||||
|
||||
@ -5,6 +5,7 @@ import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
|
||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
|
||||
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
|
||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
|
||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
|
||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
|
||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
|
||||
@ -215,6 +216,12 @@ const _apps: MinAppType[] = [
|
||||
logo: BoltAppLogo,
|
||||
url: 'https://bolt.new/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'duckduckgo',
|
||||
name: 'DuckDuckGo',
|
||||
logo: DuckDuckGoAppLogo,
|
||||
url: 'https://duck.ai'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -129,19 +129,23 @@ const visionAllowedModels = [
|
||||
'moondream',
|
||||
'minicpm',
|
||||
'gemini-1\\.5',
|
||||
'gemini-exp',
|
||||
'claude-3',
|
||||
'vision',
|
||||
'glm-4v',
|
||||
'qwen-vl',
|
||||
'qwen2-vl',
|
||||
'internvl2',
|
||||
'grok',
|
||||
'pixtral',
|
||||
'gpt-4(?:-[\\w-]+)',
|
||||
'gpt-4o(?:-[\\w-]+)?'
|
||||
'gpt-4o(?:-[\\w-]+)?',
|
||||
'chatgpt-4o(?:-[\\w-]+)?'
|
||||
]
|
||||
|
||||
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
|
||||
|
||||
const VISION_REGEX = new RegExp(
|
||||
export const VISION_REGEX = new RegExp(
|
||||
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
|
||||
'i'
|
||||
)
|
||||
@ -187,6 +191,7 @@ export function getModelLogo(modelId: string) {
|
||||
palm: isLight ? PalmModelLogo : PalmModelLogoDark,
|
||||
step: isLight ? StepModelLogo : StepModelLogoDark,
|
||||
hailuo: isLight ? HailuoModelLogo : HailuoModelLogoDark,
|
||||
doubao: isLight ? DoubaoModelLogo : DoubaoModelLogoDark,
|
||||
'ep-202': isLight ? DoubaoModelLogo : DoubaoModelLogoDark,
|
||||
cohere: isLight ? CohereModelLogo : CohereModelLogoDark,
|
||||
command: isLight ? CohereModelLogo : CohereModelLogoDark,
|
||||
@ -326,6 +331,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: ' GPT-4o-mini',
|
||||
group: 'GPT 4o'
|
||||
},
|
||||
{
|
||||
id: 'chatgpt-4o-latest',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4o-latest',
|
||||
group: 'GPT 4o'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-turbo',
|
||||
provider: 'openai',
|
||||
@ -338,12 +349,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: ' GPT-4',
|
||||
group: 'GPT 4'
|
||||
},
|
||||
{
|
||||
id: 'gpt-3.5-turbo',
|
||||
provider: 'openai',
|
||||
name: ' GPT-3.5-turbo',
|
||||
group: 'GPT 3.5'
|
||||
},
|
||||
{
|
||||
id: 'o1-mini',
|
||||
provider: 'openai',
|
||||
@ -1039,7 +1044,7 @@ export function isEmbeddingModel(model: Model): boolean {
|
||||
}
|
||||
|
||||
export function isVisionModel(model: Model): boolean {
|
||||
return VISION_REGEX.test(model.id)
|
||||
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
||||
}
|
||||
|
||||
export function isSupportedModel(model: OpenAI.Models.Model): boolean {
|
||||
|
||||
@ -46,3 +46,6 @@ export const AGENT_PROMPT = `
|
||||
|
||||
export const SUMMARIZE_PROMPT =
|
||||
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
|
||||
export const TRANSLATE_PROMPT =
|
||||
'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.'
|
||||
|
||||
@ -11,8 +11,12 @@ import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.p
|
||||
import GithubProviderLogo from '@renderer/assets/images/providers/github.png'
|
||||
import GoogleProviderLogo from '@renderer/assets/images/providers/google.png'
|
||||
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
|
||||
import GrokProviderLogo from '@renderer/assets/images/providers/grok.png'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
|
||||
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
|
||||
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
||||
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.png'
|
||||
import NvidiaProviderLogo from '@renderer/assets/images/providers/nvidia.png'
|
||||
import OcoolAiProviderLogo from '@renderer/assets/images/providers/ocoolai.png'
|
||||
@ -24,10 +28,6 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
|
||||
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
|
||||
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import GrokProviderLogo from '@renderer/assets/images/providers/grok.png'
|
||||
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
|
||||
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
|
||||
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
switch (providerId) {
|
||||
@ -326,6 +326,7 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
websites: {
|
||||
official: 'https://app.hyperbolic.xyz',
|
||||
apiKey: 'https://app.hyperbolic.xyz/settings',
|
||||
docs: 'https://docs.hyperbolic.xyz',
|
||||
models: 'https://app.hyperbolic.xyz/models'
|
||||
}
|
||||
@ -336,6 +337,7 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
websites: {
|
||||
official: 'https://mistral.ai',
|
||||
apiKey: 'https://console.mistral.ai/api-keys/',
|
||||
docs: 'https://docs.mistral.ai',
|
||||
models: 'https://docs.mistral.ai/getting-started/models/models_overview'
|
||||
}
|
||||
@ -346,6 +348,7 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
websites: {
|
||||
official: 'https://jina.ai',
|
||||
apiKey: 'https://jina.ai/',
|
||||
docs: 'https://jina.ai',
|
||||
models: 'https://jina.ai'
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { LanguageVarious } from '@renderer/types'
|
||||
import { ConfigProvider, theme } from 'antd'
|
||||
import enUS from 'antd/locale/en_US'
|
||||
import ruRU from 'antd/locale/ru_RU'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import zhTW from 'antd/locale/zh_TW'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
|
||||
import { useTheme } from './ThemeProvider'
|
||||
@ -18,11 +22,11 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
components: {
|
||||
Segmented: {
|
||||
trackBg: 'transparent',
|
||||
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
|
||||
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
||||
boxShadowTertiary: undefined,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 12,
|
||||
borderRadiusXS: 12
|
||||
borderRadiusLG: 16,
|
||||
borderRadiusSM: 16,
|
||||
borderRadiusXS: 16
|
||||
},
|
||||
Menu: {
|
||||
activeBarBorderWidth: 0,
|
||||
@ -38,12 +42,17 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
)
|
||||
}
|
||||
|
||||
function getAntdLocale(language: string) {
|
||||
function getAntdLocale(language: LanguageVarious) {
|
||||
switch (language) {
|
||||
case 'zh-CN':
|
||||
return zhCN
|
||||
case 'zh-TW':
|
||||
return zhTW
|
||||
case 'en-US':
|
||||
return undefined
|
||||
return enUS
|
||||
case 'ru-RU':
|
||||
return ruRU
|
||||
|
||||
default:
|
||||
return zhCN
|
||||
}
|
||||
|
||||
88
src/renderer/src/context/SyntaxHighlighterProvider.tsx
Normal file
88
src/renderer/src/context/SyntaxHighlighterProvider.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { CodeStyleVarious, ThemeMode } from '@renderer/types'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
BundledLanguage,
|
||||
bundledLanguages,
|
||||
BundledTheme,
|
||||
bundledThemes,
|
||||
createHighlighter,
|
||||
HighlighterGeneric
|
||||
} from 'shiki'
|
||||
|
||||
interface SyntaxHighlighterContextType {
|
||||
codeToHtml: (code: string, language: string) => Promise<string>
|
||||
}
|
||||
|
||||
const SyntaxHighlighterContext = createContext<SyntaxHighlighterContextType | undefined>(undefined)
|
||||
|
||||
export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
const [highlighter, setHighlighter] = useState<HighlighterGeneric<BundledLanguage, BundledTheme> | null>(null)
|
||||
const { codeStyle } = useSettings()
|
||||
useMermaid()
|
||||
|
||||
const highlighterTheme = useMemo(() => {
|
||||
if (!codeStyle || codeStyle === 'auto') {
|
||||
return theme === ThemeMode.light ? 'one-light' : 'material-theme-darker'
|
||||
}
|
||||
|
||||
return codeStyle
|
||||
}, [theme, codeStyle])
|
||||
|
||||
useEffect(() => {
|
||||
const initHighlighter = async () => {
|
||||
const commonLanguages = ['javascript', 'typescript', 'python', 'java', 'markdown']
|
||||
|
||||
const hl = await createHighlighter({
|
||||
themes: [highlighterTheme],
|
||||
langs: commonLanguages
|
||||
})
|
||||
|
||||
setHighlighter(hl)
|
||||
|
||||
// Load all themes and languages
|
||||
// hl.loadTheme(...(Object.keys(bundledThemes) as BundledTheme[]))
|
||||
// hl.loadLanguage(...(Object.keys(bundledLanguages) as BundledLanguage[]))
|
||||
}
|
||||
|
||||
initHighlighter()
|
||||
}, [highlighterTheme])
|
||||
|
||||
const codeToHtml = async (code: string, language: string) => {
|
||||
if (!highlighter) return ''
|
||||
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
|
||||
if (language in bundledLanguages || language === 'text') {
|
||||
await highlighter.loadLanguage(language as BundledLanguage)
|
||||
console.log(`Loaded language: ${language}`)
|
||||
} else {
|
||||
return `<pre style="padding: 10px"><code>${code}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: language,
|
||||
theme: highlighterTheme
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error highlighting code for language '${language}':`, error)
|
||||
return `<pre style="padding: 10px"><code>${code}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
return <SyntaxHighlighterContext.Provider value={{ codeToHtml }}>{children}</SyntaxHighlighterContext.Provider>
|
||||
}
|
||||
|
||||
export const useSyntaxHighlighter = () => {
|
||||
const context = useContext(SyntaxHighlighterContext)
|
||||
if (!context) {
|
||||
throw new Error('useSyntaxHighlighter must be used within a SyntaxHighlighterProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const codeThemes = ['auto', ...Object.keys(bundledThemes)] as CodeStyleVarious[]
|
||||
@ -1,8 +1,6 @@
|
||||
import { FileType, Topic } from '@renderer/types'
|
||||
import { Dexie, type EntityTable } from 'dexie'
|
||||
|
||||
import { populateTopics } from './populate'
|
||||
|
||||
// Database declaration (move this to its own module also)
|
||||
export const db = new Dexie('CherryStudio') as Dexie & {
|
||||
files: EntityTable<FileType, 'id'>
|
||||
@ -14,14 +12,10 @@ db.version(1).stores({
|
||||
files: 'id, name, origin_name, path, size, ext, type, created_at, count'
|
||||
})
|
||||
|
||||
db.version(2)
|
||||
.stores({
|
||||
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
|
||||
topics: '&id, messages',
|
||||
settings: '&id, value'
|
||||
})
|
||||
.upgrade(populateTopics)
|
||||
|
||||
db.on('populate', populateTopics)
|
||||
db.version(2).stores({
|
||||
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
|
||||
topics: '&id, messages',
|
||||
settings: '&id, value'
|
||||
})
|
||||
|
||||
export default db
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import { Transaction } from 'dexie'
|
||||
import localforage from 'localforage'
|
||||
|
||||
export async function populateTopics(trans: Transaction) {
|
||||
const indexedKeys = await localforage.keys()
|
||||
|
||||
if (indexedKeys.length > 0) {
|
||||
for (const key of indexedKeys) {
|
||||
const value: any = await localforage.getItem(key)
|
||||
if (key.startsWith('topic:')) {
|
||||
await trans.db.table('topics').add({ id: value.id, messages: value.messages })
|
||||
}
|
||||
if (key === 'image://avatar') {
|
||||
await trans.db.table('settings').add({ id: key, value: await localforage.getItem(key) })
|
||||
}
|
||||
}
|
||||
|
||||
window.modal.success({
|
||||
title: i18n.t('message.upgrade.success.title'),
|
||||
content: i18n.t('message.upgrade.success.content'),
|
||||
okText: i18n.t('message.upgrade.success.button'),
|
||||
centered: true,
|
||||
onOk: () => window.api.reload()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ import { useSettings } from './useSettings'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl, language, windowStyle, manualUpdateCheck } = useSettings()
|
||||
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode } = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
@ -35,8 +35,14 @@ export function useAppInit() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
proxyUrl && window.api.setProxy(proxyUrl)
|
||||
}, [proxyUrl])
|
||||
if (proxyMode === 'system') {
|
||||
window.api.setProxy('system')
|
||||
} else if (proxyMode === 'custom') {
|
||||
proxyUrl && window.api.setProxy(proxyUrl)
|
||||
} else {
|
||||
window.api.setProxy('')
|
||||
}
|
||||
}, [proxyUrl, proxyMode])
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || navigator.language || 'en-US')
|
||||
|
||||
40
src/renderer/src/hooks/useMermaid.ts
Normal file
40
src/renderer/src/hooks/useMermaid.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { loadScript, runAsyncFunction } from '@renderer/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useRuntime } from './useRuntime'
|
||||
|
||||
export const useMermaid = () => {
|
||||
const { theme } = useTheme()
|
||||
const { generating } = useRuntime()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@11.4.0/dist/mermaid.min.js')
|
||||
}
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
})
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.mermaid || generating) return
|
||||
|
||||
const renderMermaid = () => {
|
||||
const mermaidElements = document.querySelectorAll('.mermaid')
|
||||
mermaidElements.forEach((element) => {
|
||||
if (!element.querySelector('svg')) {
|
||||
element.removeAttribute('data-processed')
|
||||
}
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
}
|
||||
|
||||
setTimeout(renderMermaid, 100)
|
||||
}, [generating])
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { uuid } from '@renderer/utils'
|
||||
export function usePaintings() {
|
||||
const paintings = useAppSelector((state) => state.paintings.paintings)
|
||||
const dispatch = useAppDispatch()
|
||||
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
|
||||
|
||||
return {
|
||||
paintings,
|
||||
@ -20,7 +21,7 @@ export function usePaintings() {
|
||||
negativePrompt: '',
|
||||
imageSize: '1024x1024',
|
||||
numImages: 1,
|
||||
seed: '',
|
||||
seed: generateRandomSeed(),
|
||||
steps: 25,
|
||||
guidanceScale: 4.5,
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setTheme,
|
||||
setTopicPosition,
|
||||
setTray,
|
||||
setWindowStyle
|
||||
} from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
@ -17,6 +18,9 @@ export function useSettings() {
|
||||
setSendMessageShortcut(shortcut: SendMessageShortcut) {
|
||||
dispatch(_setSendMessageShortcut(shortcut))
|
||||
},
|
||||
setTray(isActive: boolean) {
|
||||
dispatch(setTray(isActive))
|
||||
},
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
},
|
||||
|
||||
60
src/renderer/src/hooks/useShortcuts.ts
Normal file
60
src/renderer/src/hooks/useShortcuts.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { useCallback } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
interface UseShortcutOptions {
|
||||
preventDefault?: boolean
|
||||
enableOnFormTags?: boolean
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
const defaultOptions: UseShortcutOptions = {
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true,
|
||||
enabled: true
|
||||
}
|
||||
|
||||
export const useShortcut = (
|
||||
shortcutKey: string,
|
||||
callback: (e: KeyboardEvent) => void,
|
||||
options: UseShortcutOptions = defaultOptions
|
||||
) => {
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
|
||||
const formatShortcut = useCallback((shortcut: string[]) => {
|
||||
return shortcut
|
||||
.map((key) => {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'command':
|
||||
return 'meta'
|
||||
default:
|
||||
return key.toLowerCase()
|
||||
}
|
||||
})
|
||||
.join('+')
|
||||
}, [])
|
||||
|
||||
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey)
|
||||
|
||||
useHotkeys(
|
||||
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : '',
|
||||
(e) => {
|
||||
if (options.preventDefault) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (options.enabled !== false) {
|
||||
callback(e)
|
||||
}
|
||||
},
|
||||
{
|
||||
enableOnFormTags: options.enableOnFormTags,
|
||||
description: options.description || shortcutConfig?.key
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function useShortcuts() {
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
return { shortcuts }
|
||||
}
|
||||
@ -2,13 +2,15 @@ import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
import enUS from './locales/en-us.json'
|
||||
import ruRU from './locales/ru-ru.json'
|
||||
import zhCN from './locales/zh-cn.json'
|
||||
import zhTW from './locales/zh-tw.json'
|
||||
|
||||
const resources = {
|
||||
'en-US': enUS,
|
||||
'zh-CN': zhCN,
|
||||
'zh-TW': zhTW
|
||||
'zh-TW': zhTW,
|
||||
'ru-RU': ruRU
|
||||
}
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
|
||||
@ -1,407 +1,511 @@
|
||||
{
|
||||
"translation": {
|
||||
"common": {
|
||||
"avatar": "Avatar",
|
||||
"language": "Language",
|
||||
"model": "Model",
|
||||
"models": "Models",
|
||||
"topics": "Topics",
|
||||
"docs": "Docs",
|
||||
"and": "and",
|
||||
"assistant": "Assistant",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"prompt": "Prompt",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"duplicate": "Duplicate",
|
||||
"copy": "Copy",
|
||||
"regenerate": "Regenerate",
|
||||
"provider": "Provider",
|
||||
"you": "You",
|
||||
"save": "Save",
|
||||
"footnotes": "References",
|
||||
"select": "Select",
|
||||
"search": "Search",
|
||||
"default": "Default",
|
||||
"warning": "Warning",
|
||||
"back": "Back",
|
||||
"chat": "Chat",
|
||||
"close": "Close",
|
||||
"cancel": "Cancel",
|
||||
"download": "Download"
|
||||
},
|
||||
"button": {
|
||||
"add": "Add",
|
||||
"added": "Added",
|
||||
"manage": "Manage",
|
||||
"select_model": "Select Model",
|
||||
"show.all": "Show All",
|
||||
"collapse": "Collapse"
|
||||
},
|
||||
"message": {
|
||||
"copied": "Copied!",
|
||||
"assistant.added.content": "Assistant added successfully",
|
||||
"message.delete.title": "Delete Message",
|
||||
"message.delete.content": "Are you sure you want to delete this message?",
|
||||
"error.enter.api.key": "Please enter your API key first",
|
||||
"error.enter.api.host": "Please enter your API host first",
|
||||
"error.enter.model": "Please select a model first",
|
||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||
"api.connection.failed": "Connection failed",
|
||||
"api.connection.success": "Connection successful",
|
||||
"chat.completion.paused": "Chat completion paused",
|
||||
"switch.disabled": "Switching is disabled while the assistant is generating",
|
||||
"restore.success": "Restored successfully",
|
||||
"backup.success": "Backup successful",
|
||||
"backup.failed": "Backup failed",
|
||||
"reset.confirm.content": "Are you sure you want to clear all data?",
|
||||
"reset.double.confirm.title": "DATA LOST !!!",
|
||||
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
||||
"upgrade.success.title": "Upgrade successfully",
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
"upgrade.success.button": "Restart",
|
||||
"topic.added": "New topic added",
|
||||
"save.success.title": "Saved successfully",
|
||||
"message.style": "Message Style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain"
|
||||
},
|
||||
"chat": {
|
||||
"save": "Save",
|
||||
"default.name": "⭐️ Default Assistant",
|
||||
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||
"default.topic.name": "Default Topic",
|
||||
"topics.title": "Topics",
|
||||
"topics.auto_rename": "Auto Rename",
|
||||
"topics.edit.title": "Edit Name",
|
||||
"topics.edit.placeholder": "Enter new name",
|
||||
"topics.clear.title": "Clear Messages",
|
||||
"topics.move_to": "Move to",
|
||||
"topics.list": "Topic List",
|
||||
"topics.export.title": "Export",
|
||||
"topics.export.image": "Export as image",
|
||||
"topics.export.md": "Export as markdown",
|
||||
"topics.export.word": "Export as Word",
|
||||
"input.new_topic": "New Topic",
|
||||
"input.topics": " Topics ",
|
||||
"input.clear": "Clear",
|
||||
"input.new.context": "Clear Context",
|
||||
"input.expand": "Expand",
|
||||
"input.collapse": "Collapse",
|
||||
"input.clear.title": "Clear all messages?",
|
||||
"input.clear.content": "Do you want to clear all messages of the current topic?",
|
||||
"input.placeholder": "Type your message here...",
|
||||
"input.send": "Send",
|
||||
"input.pause": "Pause",
|
||||
"input.settings": "Settings",
|
||||
"input.upload": "Upload image or document file",
|
||||
"input.context_count.tip": "Context Count",
|
||||
"input.estimated_tokens.tip": "Estimated tokens",
|
||||
"settings.temperature": "Temperature",
|
||||
"settings.temperature.tip": "Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.",
|
||||
"settings.conext_count": "Context",
|
||||
"settings.conext_count.tip": "The number of previous messages to keep in the context.",
|
||||
"settings.max_tokens": "Enable Max Tokens Limit",
|
||||
"settings.max_tokens.tip": "The maximum number of tokens the model can generate. Normal chat suggests 500-800. Short text generation suggests 800-2000. Code generation suggests 2000-3600. Long text generation suggests above 4000.",
|
||||
"settings.reset": "Reset",
|
||||
"settings.set_as_default": "Apply to default assistant",
|
||||
"settings.max": "Max",
|
||||
"settings.show_line_numbers": "Show Line Numbers in Code",
|
||||
"suggestions.title": "Suggested Questions",
|
||||
"add.assistant.title": "Add Assistant",
|
||||
"message.new.context": "New Context",
|
||||
"message.new.branch": "New Branch",
|
||||
"message.new.branch.created": "New Branch Created",
|
||||
"assistant.search.placeholder": "Search",
|
||||
"artifacts.button.preview": "Preview",
|
||||
"artifacts.button.download": "Download"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "Assistants",
|
||||
"abbr": "Assistant",
|
||||
"search": "Search assistants...",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
"settings.model": "Model Settings",
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.default_model": "Default Model",
|
||||
"settings.auto_reset_model": "Auto Reset Model",
|
||||
"settings.auto_reset_model.tip": "Automatically reset the model when a new topic is created.",
|
||||
"edit.title": "Edit Assistant",
|
||||
"copy.title": "Copy Assistant",
|
||||
"clear.title": "Clear topics",
|
||||
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
|
||||
"save.title": "Save to agent",
|
||||
"save.success": "Saved successfully",
|
||||
"delete.title": "Delete Assistant",
|
||||
"delete.content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?"
|
||||
},
|
||||
"model": {
|
||||
"stream_output": "Stream Output",
|
||||
"search": "Search models..."
|
||||
},
|
||||
"images": {
|
||||
"title": "Images",
|
||||
"image.size": "Image Size",
|
||||
"button.new.image": "New Image",
|
||||
"button.delete.image": "Delete Image",
|
||||
"button.delete.image.confirm": "Are you sure you want to delete this image?",
|
||||
"number_images": "Number Images",
|
||||
"number_images_tip": "Number of images to generate (1-4)",
|
||||
"seed": "Seed",
|
||||
"seed_tip": "The same seed and prompt can produce similar images",
|
||||
"inference_steps": "Inference Steps",
|
||||
"inference_steps_tip": "The number of inference steps to perform. More steps produce higher quality but take longer",
|
||||
"guidance_scale": "Guidance Scale",
|
||||
"guidance_scale_tip": "Classifier Free Guidance. How close you want the model to stick to your prompt when looking for a related image to show you",
|
||||
"negative_prompt": "Negative Prompt",
|
||||
"negative_prompt_tip": "Describe what you don't want included in the image",
|
||||
"prompt_placeholder": "Describe the image you want to create, e.g. A serene lake at sunset with mountains in the background",
|
||||
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?"
|
||||
},
|
||||
"files": {
|
||||
"title": "Files",
|
||||
"file": "File",
|
||||
"name": "Name",
|
||||
"size": "Size",
|
||||
"count": "Count",
|
||||
"created_at": "Created At",
|
||||
"image": "Image",
|
||||
"text": "Text",
|
||||
"document": "Document",
|
||||
"actions": "Actions",
|
||||
"open": "Open",
|
||||
"all": "All Files"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Agents",
|
||||
"my_agents": "My Agents",
|
||||
"add.title": "Create Agent",
|
||||
"edit.title": "Edit Agent",
|
||||
"add.button": "Add to Assistant",
|
||||
"add.name": "Name",
|
||||
"add.name.placeholder": "Enter name",
|
||||
"add.prompt": "Prompt",
|
||||
"add.prompt.placeholder": "Enter prompt",
|
||||
"add.button": "Add to Assistant",
|
||||
"manage.title": "Manage Agents",
|
||||
"add.title": "Create Agent",
|
||||
"delete.popup.content": "Are you sure you want to delete this agent?",
|
||||
"tag.default": "Default",
|
||||
"tag.system": "System",
|
||||
"tag.agent": "Agent",
|
||||
"edit.message.title": "Preset messages",
|
||||
"edit.message.add.title": "Add",
|
||||
"edit.message.group.title": "Message Group",
|
||||
"edit.message.assistant.title": "Assistant",
|
||||
"edit.message.assistant.placeholder": "Enter assistant message",
|
||||
"edit.message.user.title": "User",
|
||||
"edit.message.user.placeholder": "Enter user message",
|
||||
"edit.message.assistant.title": "Assistant",
|
||||
"edit.message.empty.content": "Conversation input content cannot be empty",
|
||||
"edit.message.group.title": "Message Group",
|
||||
"edit.message.title": "Preset messages",
|
||||
"edit.message.user.placeholder": "Enter user message",
|
||||
"edit.message.user.title": "User",
|
||||
"edit.model.select.title": "Select Model",
|
||||
"edit.settings.hide_preset_messages": "Hide Preset Message",
|
||||
"edit.title": "Edit Agent",
|
||||
"manage.title": "Manage Agents",
|
||||
"my_agents": "My Agents",
|
||||
"search.no_results": "No results found",
|
||||
"sorting.title": "Sorting"
|
||||
"sorting.title": "Sorting",
|
||||
"tag.agent": "Agent",
|
||||
"tag.default": "Default",
|
||||
"tag.new": "New",
|
||||
"tag.system": "System",
|
||||
"title": "Agents"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistant",
|
||||
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
|
||||
"clear.title": "Clear topics",
|
||||
"copy.title": "Copy Assistant",
|
||||
"delete.content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?",
|
||||
"delete.title": "Delete Assistant",
|
||||
"edit.title": "Edit Assistant",
|
||||
"save.success": "Saved successfully",
|
||||
"save.title": "Save to agent",
|
||||
"search": "Search assistants...",
|
||||
"settings.auto_reset_model": "Auto Reset Model",
|
||||
"settings.auto_reset_model.tip": "Automatically reset the model when a new topic is created.",
|
||||
"settings.default_model": "Default Model",
|
||||
"settings.model": "Model Settings",
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
"title": "Assistants"
|
||||
},
|
||||
"button": {
|
||||
"add": "Add",
|
||||
"added": "Added",
|
||||
"collapse": "Collapse",
|
||||
"manage": "Manage",
|
||||
"select_model": "Select Model",
|
||||
"show.all": "Show All"
|
||||
},
|
||||
"chat": {
|
||||
"add.assistant.title": "Add Assistant",
|
||||
"artifacts.button.download": "Download",
|
||||
"artifacts.button.preview": "Preview",
|
||||
"assistant.search.placeholder": "Search",
|
||||
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||
"default.name": "⭐️ Default Assistant",
|
||||
"default.topic.name": "Default Topic",
|
||||
"input.clear": "Clear",
|
||||
"input.clear.content": "Do you want to clear all messages of the current topic?",
|
||||
"input.clear.title": "Clear all messages?",
|
||||
"input.collapse": "Collapse",
|
||||
"input.context_count.tip": "Context Count",
|
||||
"input.estimated_tokens.tip": "Estimated tokens",
|
||||
"input.expand": "Expand",
|
||||
"input.new.context": "Clear Context",
|
||||
"input.new_topic": "New Topic {{Command}}+N",
|
||||
"input.pause": "Pause",
|
||||
"input.placeholder": "Type your message here...",
|
||||
"input.send": "Send",
|
||||
"input.settings": "Settings",
|
||||
"input.topics": " Topics ",
|
||||
"input.translate": "Translate to English",
|
||||
"input.upload": "Upload image or document file",
|
||||
"message.new.branch": "New Branch",
|
||||
"message.new.branch.created": "New Branch Created",
|
||||
"message.new.context": "New Context",
|
||||
"save": "Save",
|
||||
"settings.code_collapsible": "Code block collapsible",
|
||||
"settings.context_count": "Context",
|
||||
"settings.context_count.tip": "The number of previous messages to keep in the context.",
|
||||
"settings.max": "Max",
|
||||
"settings.max_tokens": "Enable max tokens limit",
|
||||
"settings.max_tokens.tip": "The maximum number of tokens the model can generate. Normal chat suggests 500-800. Short text generation suggests 800-2000. Code generation suggests 2000-3600. Long text generation suggests above 4000.",
|
||||
"settings.reset": "Reset",
|
||||
"settings.set_as_default": "Apply to default assistant",
|
||||
"settings.show_line_numbers": "Show line numbers in code",
|
||||
"settings.temperature": "Temperature",
|
||||
"settings.temperature.tip": "Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.",
|
||||
"suggestions.title": "Suggested Questions",
|
||||
"topics.auto_rename": "Auto Rename",
|
||||
"topics.clear.title": "Clear Messages",
|
||||
"topics.edit.placeholder": "Enter new name",
|
||||
"topics.edit.title": "Edit Name",
|
||||
"topics.export.image": "Export as image",
|
||||
"topics.export.md": "Export as markdown",
|
||||
"topics.export.title": "Export",
|
||||
"topics.export.word": "Export as Word",
|
||||
"topics.list": "Topic List",
|
||||
"topics.move_to": "Move to",
|
||||
"topics.title": "Topics",
|
||||
"translate": "Translate"
|
||||
},
|
||||
"common": {
|
||||
"and": "and",
|
||||
"assistant": "Assistant",
|
||||
"avatar": "Avatar",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"chat": "Chat",
|
||||
"close": "Close",
|
||||
"copy": "Copy",
|
||||
"cut": "Cut",
|
||||
"default": "Default",
|
||||
"delete": "Delete",
|
||||
"description": "Description",
|
||||
"docs": "Docs",
|
||||
"download": "Download",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Edit",
|
||||
"footnotes": "References",
|
||||
"language": "Language",
|
||||
"model": "Model",
|
||||
"models": "Models",
|
||||
"name": "Name",
|
||||
"paste": "Paste",
|
||||
"prompt": "Prompt",
|
||||
"provider": "Provider",
|
||||
"regenerate": "Regenerate",
|
||||
"rename": "Rename",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
"search": "Search",
|
||||
"select": "Select",
|
||||
"topics": "Topics",
|
||||
"warning": "Warning",
|
||||
"you": "You",
|
||||
"clear": "Clear",
|
||||
"add": "Add"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "Backup file format error",
|
||||
"chat.response": "Something went wrong. Please check if you have set your API key in the Settings > Providers",
|
||||
"no_api_key": "API key is not configured",
|
||||
"provider_disabled": "Model provider is not enabled",
|
||||
"render": {
|
||||
"title": "Render Error",
|
||||
"description": "Failed to render formula. Please check if the formula format is correct"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"assistant": "Assistant",
|
||||
"attached_files": "Attached Files",
|
||||
"conversation_details": "Conversation Details",
|
||||
"conversation_history": "Conversation History",
|
||||
"created": "Created",
|
||||
"last_updated": "Last Updated",
|
||||
"messages": "Messages",
|
||||
"user": "User"
|
||||
},
|
||||
"files": {
|
||||
"actions": "Actions",
|
||||
"all": "All Files",
|
||||
"count": "Count",
|
||||
"created_at": "Created At",
|
||||
"document": "Document",
|
||||
"file": "File",
|
||||
"image": "Image",
|
||||
"name": "Name",
|
||||
"open": "Open",
|
||||
"size": "Size",
|
||||
"text": "Text",
|
||||
"title": "Files"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Continue Chatting",
|
||||
"locate.message": "Locate the message",
|
||||
"search.messages": "Search All Messages",
|
||||
"search.placeholder": "Search topics or messages...",
|
||||
"search.topics.empty": "No topics found, press Enter to search all messages",
|
||||
"title": "Topics Search"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "Arabic",
|
||||
"chinese": "Chinese",
|
||||
"chinese-traditional": "Traditional Chinese",
|
||||
"english": "English",
|
||||
"french": "French",
|
||||
"italian": "Italian",
|
||||
"japanese": "Japanese",
|
||||
"korean": "Korean",
|
||||
"portuguese": "Portuguese",
|
||||
"russian": "Russian",
|
||||
"spanish": "Spanish"
|
||||
},
|
||||
"mermaid": {
|
||||
"download": {
|
||||
"png": "Download PNG",
|
||||
"svg": "Download SVG"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "Preview",
|
||||
"source": "Source"
|
||||
},
|
||||
"title": "Mermaid Diagram"
|
||||
},
|
||||
"message": {
|
||||
"api.connection.failed": "Connection failed",
|
||||
"api.connection.success": "Connection successful",
|
||||
"assistant.added.content": "Assistant added successfully",
|
||||
"backup.failed": "Backup failed",
|
||||
"backup.success": "Backup successful",
|
||||
"chat.completion.paused": "Chat completion paused",
|
||||
"copied": "Copied!",
|
||||
"error.enter.api.host": "Please enter your API host first",
|
||||
"error.enter.api.key": "Please enter your API key first",
|
||||
"error.enter.model": "Please select a model first",
|
||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||
"message.code_style": "Code style",
|
||||
"message.delete.content": "Are you sure you want to delete this message?",
|
||||
"message.delete.title": "Delete Message",
|
||||
"message.style": "Message style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain",
|
||||
"reset.confirm.content": "Are you sure you want to clear all data?",
|
||||
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
||||
"reset.double.confirm.title": "DATA LOST !!!",
|
||||
"restore.success": "Restored successfully",
|
||||
"save.success.title": "Saved successfully",
|
||||
"switch.disabled": "Switching is disabled while the assistant is generating",
|
||||
"topic.added": "New topic added",
|
||||
"upgrade.success.button": "Restart",
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
"upgrade.success.title": "Upgrade successfully",
|
||||
"regenerate.confirm": "Regenerating will replace current message"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "MinApp"
|
||||
},
|
||||
"history": {
|
||||
"title": "Topics Search",
|
||||
"search.placeholder": "Search topics or messages...",
|
||||
"continue_chat": "Continue Chatting",
|
||||
"search.topics.empty": "No topics found, press Enter to search all messages",
|
||||
"search.messages": "Search All Messages",
|
||||
"locate.message": "Locate the message"
|
||||
"model": {
|
||||
"pinned": "Pinned",
|
||||
"search": "Search models...",
|
||||
"stream_output": "Stream output",
|
||||
"type": {
|
||||
"select": "Select Model Types",
|
||||
"text": "Text",
|
||||
"vision": "Vision"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
"keep_alive_time.placeholder": "Minutes",
|
||||
"keep_alive_time.title": "Keep Alive Time",
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"button.delete.image": "Delete Image",
|
||||
"button.delete.image.confirm": "Are you sure you want to delete this image?",
|
||||
"button.new.image": "New Image",
|
||||
"guidance_scale": "Guidance Scale",
|
||||
"guidance_scale_tip": "Classifier Free Guidance. How close you want the model to stick to your prompt when looking for a related image to show you",
|
||||
"image.size": "Image Size",
|
||||
"inference_steps": "Inference Steps",
|
||||
"inference_steps_tip": "The number of inference steps to perform. More steps produce higher quality but take longer",
|
||||
"negative_prompt": "Negative Prompt",
|
||||
"negative_prompt_tip": "Describe what you don't want included in the image",
|
||||
"number_images": "Number Images",
|
||||
"number_images_tip": "Number of images to generate (1-4)",
|
||||
"prompt_placeholder": "Describe the image you want to create, e.g. A serene lake at sunset with mountains in the background",
|
||||
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
|
||||
"seed": "Seed",
|
||||
"seed_tip": "The same seed and prompt can produce similar images",
|
||||
"title": "Images"
|
||||
},
|
||||
"provider": {
|
||||
"jina": "Jina",
|
||||
"mistral": "Mistral",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"grok": "Grok",
|
||||
"nvidia": "Nvidia",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"zhinao": "360AI",
|
||||
"fireworks": "Fireworks",
|
||||
"together": "Together",
|
||||
"openai": "OpenAI",
|
||||
"gemini": "Gemini",
|
||||
"deepseek": "DeepSeek",
|
||||
"moonshot": "Moonshot",
|
||||
"silicon": "SiliconFlow",
|
||||
"openrouter": "OpenRouter",
|
||||
"yi": "Yi",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"groq": "Groq",
|
||||
"ollama": "Ollama",
|
||||
"aihubmix": "AiHubMix",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Baichuan",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"anthropic": "Anthropic",
|
||||
"aihubmix": "AiHubMix",
|
||||
"stepfun": "StepFun",
|
||||
"deepseek": "DeepSeek",
|
||||
"doubao": "Doubao",
|
||||
"minimax": "MiniMax",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"fireworks": "Fireworks",
|
||||
"gemini": "Gemini",
|
||||
"github": "GitHub Models",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"jina": "Jina",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"moonshot": "Moonshot",
|
||||
"nvidia": "Nvidia",
|
||||
"ocoolai": "ocoolAI",
|
||||
"azure-openai": "Azure OpenAI"
|
||||
"ollama": "Ollama",
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter",
|
||||
"silicon": "SiliconFlow",
|
||||
"stepfun": "StepFun",
|
||||
"together": "Together",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"general": "General Settings",
|
||||
"data": "Data Settings",
|
||||
"provider": "Model Provider",
|
||||
"model": "Default Model",
|
||||
"assistant": "Default Assistant",
|
||||
"about": "About & Feedback",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.title": "Message Settings",
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.use_serif_font": "Use serif font",
|
||||
"messages.input.title": "Input Settings",
|
||||
"messages.input.show_estimated_tokens": "Show estimated input tokens",
|
||||
"messages.input.send_shortcuts": "Send shortcuts",
|
||||
"messages.input.paste_long_text_as_file": "Paste long text as file",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input msg",
|
||||
"messages.math_engine": "Math render engine",
|
||||
"about.checkUpdate": "Check Update",
|
||||
"about.checkingUpdate": "Checking for updates...",
|
||||
"about.contact.button": "Email",
|
||||
"about.contact.title": "Contact",
|
||||
"about.description": "A powerful AI assistant for producer",
|
||||
"about.downloading": "Downloading...",
|
||||
"about.feedback.button": "Feedback",
|
||||
"about.feedback.title": "Feedback",
|
||||
"about.license.button": "License",
|
||||
"about.license.title": "License",
|
||||
"about.releases.button": "Releases",
|
||||
"about.releases.title": "Release Notes",
|
||||
"about.title": "About",
|
||||
"about.updateAvailable": "Found new version {{version}}",
|
||||
"about.updateError": "Update error",
|
||||
"about.updateNotAvailable": "You are using the latest version",
|
||||
"about.website.button": "Website",
|
||||
"about.website.title": "Official Website",
|
||||
"advanced.auto_switch_to_topics": "Auto switch to topic",
|
||||
"advanced.title": "Advanced Settings",
|
||||
"assistant": "Default Assistant",
|
||||
"assistant.model_params": "Model Parameters",
|
||||
"assistant.title": "Default Assistant",
|
||||
"data": {
|
||||
"app_data": "App Data",
|
||||
"app_logs": "App Logs",
|
||||
"clear_cache": {
|
||||
"button": "Clear Cache",
|
||||
"confirm": "Clearing the cache will delete application cache data, including minapp data. This action is irreversible, continue?",
|
||||
"error": "Error clearing cache",
|
||||
"success": "Cache cleared",
|
||||
"title": "Clear Cache"
|
||||
},
|
||||
"data.title": "Data Directory",
|
||||
"title": "Data Settings",
|
||||
"webdav.backup.button": "Backup to WebDAV",
|
||||
"webdav.host": "WebDAV Host",
|
||||
"webdav.host.placeholder": "http://localhost:8080",
|
||||
"webdav.password": "WebDAV Password",
|
||||
"webdav.path": "WebDAV Path",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.restore.button": "Restore from WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV User"
|
||||
},
|
||||
"display.title": "Display Settings",
|
||||
"font_size.title": "Message font size",
|
||||
"general": "General Settings",
|
||||
"general.backup.button": "Backup",
|
||||
"general.backup.title": "Data Backup and Recovery",
|
||||
"general.manually_check_update.title": "Turn off update checking",
|
||||
"general.reset.button": "Reset",
|
||||
"general.reset.title": "Data Reset",
|
||||
"general.restore.button": "Restore",
|
||||
"general.title": "General Settings",
|
||||
"general.user_name": "User Name",
|
||||
"general.user_name.placeholder": "Enter your name",
|
||||
"general.backup.title": "Data Backup and Recovery",
|
||||
"general.backup.button": "Backup",
|
||||
"general.restore.button": "Restore",
|
||||
"general.view_webdav_settings": "View WebDAV settings",
|
||||
"general.reset.title": "Data Reset",
|
||||
"general.reset.button": "Reset",
|
||||
"general.manually_check_update.title": "Turn off update checking",
|
||||
"data.webdav.title": "WebDAV",
|
||||
"data.webdav.host": "WebDAV Host",
|
||||
"data.webdav.host.placeholder": "http://localhost:8080",
|
||||
"data.webdav.user": "WebDAV User",
|
||||
"data.webdav.password": "WebDAV Password",
|
||||
"data.webdav.path": "WebDAV Path",
|
||||
"data.webdav.path.placeholder": "/backup",
|
||||
"data.webdav.backup.button": "Backup to WebDAV",
|
||||
"data.webdav.restore.button": "Restore from WebDAV",
|
||||
"advanced.title": "Advanced Settings",
|
||||
"advanced.click_assistant_switch_to_topics": "Auto switch to topic",
|
||||
"provider.api_key": "API Key",
|
||||
"provider.check": "Check",
|
||||
"provider.get_api_key": "Get API Key",
|
||||
"provider.api_host": "API Host",
|
||||
"provider.api_version": "API Version",
|
||||
"provider.docs_check": "Check",
|
||||
"provider.docs_more_details": "for more details",
|
||||
"provider.search_placeholder": "Search model id or name",
|
||||
"provider.api.url.reset": "Reset",
|
||||
"provider.api.url.preview": "Preview: {{url}}",
|
||||
"provider.api.url.tip": "Ending with / ignores v1, ending with # forces use of input address",
|
||||
"models.default_assistant_model": "Default Assistant Model",
|
||||
"models.topic_naming_model": "Topic Naming Model",
|
||||
"models.translate_model": "Translate Model",
|
||||
"input.auto_translate_with_space": "Quickly translate with 3 spaces",
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.input.paste_long_text_as_file": "Paste long text as file",
|
||||
"messages.input.send_shortcuts": "Send shortcuts",
|
||||
"messages.input.show_estimated_tokens": "Show estimated tokens",
|
||||
"messages.input.title": "Input Settings",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input msg",
|
||||
"messages.math_engine": "Math render engine",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.title": "Message Settings",
|
||||
"messages.use_serif_font": "Use serif font",
|
||||
"model": "Default Model",
|
||||
"models.add.add_model": "Add Model",
|
||||
"models.add.model_id.placeholder": "Required e.g. gpt-3.5-turbo",
|
||||
"models.add.group_name": "Group Name",
|
||||
"models.add.group_name.placeholder": "Optional e.g. ChatGPT",
|
||||
"models.add.group_name.tooltip": "Optional e.g. ChatGPT",
|
||||
"models.add.model_id": "Model ID",
|
||||
"models.add.model_id.placeholder": "Required e.g. gpt-3.5-turbo",
|
||||
"models.add.model_id.tooltip": "Example: gpt-3.5-turbo",
|
||||
"models.add.model_name": "Model Name",
|
||||
"models.add.model_name.placeholder": "Optional e.g. GPT-4",
|
||||
"models.add.group_name": "Group Name",
|
||||
"models.add.group_name.tooltip": "Optional e.g. ChatGPT",
|
||||
"models.add.group_name.placeholder": "Optional e.g. ChatGPT",
|
||||
"models.default_assistant_model": "Default Assistant Model",
|
||||
"models.default_assistant_model_description": "Model used when creating a new assistant, if the assistant is not set, this model will be used",
|
||||
"models.empty": "No models found",
|
||||
"assistant.title": "Default Assistant",
|
||||
"assistant.model_params": "Model Parameters",
|
||||
"about.description": "A powerful AI assistant for producer",
|
||||
"about.updateNotAvailable": "You are using the latest version",
|
||||
"about.checkingUpdate": "Checking for updates...",
|
||||
"about.updateError": "Update error",
|
||||
"about.checkUpdate": "Check Update",
|
||||
"about.downloading": "Downloading...",
|
||||
"provider.delete.title": "Delete Provider",
|
||||
"provider.delete.content": "Are you sure you want to delete this provider?",
|
||||
"provider.edit.name": "Provider Name",
|
||||
"provider.edit.name.placeholder": "Example: OpenAI",
|
||||
"about.title": "About",
|
||||
"about.releases.title": "Release Notes",
|
||||
"about.releases.button": "Releases",
|
||||
"about.website.title": "Official Website",
|
||||
"about.website.button": "Website",
|
||||
"about.feedback.title": "Feedback",
|
||||
"about.feedback.button": "Feedback",
|
||||
"about.contact.title": "Contact",
|
||||
"about.license.title": "License",
|
||||
"about.license.button": "License",
|
||||
"about.contact.button": "Email",
|
||||
"models.topic_naming_model": "Topic Naming Model",
|
||||
"models.topic_naming_model_description": "Model used when automatically naming a new topic",
|
||||
"models.translate_model": "Translate Model",
|
||||
"models.translate_model_description": "Model used for translation service",
|
||||
"models.translate_model_prompt_message": "Please enter the translate model prompt",
|
||||
"models.translate_model_prompt_title": "Translate Model Prompt",
|
||||
"models.topic_naming_model_setting_title": "Topic Naming Model Settings",
|
||||
"models.enable_topic_naming": "Topic Auto Naming",
|
||||
"provider": {
|
||||
"add.name": "Provider Name",
|
||||
"add.name.placeholder": "Example: OpenAI",
|
||||
"add.title": "Add Provider",
|
||||
"add.type": "Provider Type",
|
||||
"api.url.preview": "Preview: {{url}}",
|
||||
"api.url.reset": "Reset",
|
||||
"api.url.tip": "Ending with / ignores v1, ending with # forces use of input address",
|
||||
"api_host": "API Host",
|
||||
"api_key": "API Key",
|
||||
"api_key.tip": "Multiple keys separated by commas",
|
||||
"api_version": "API Version",
|
||||
"check": "Check",
|
||||
"check_all_keys": "Check All Keys",
|
||||
"check_multiple_keys": "Check Multiple API Keys",
|
||||
"delete.content": "Are you sure you want to delete this provider?",
|
||||
"delete.title": "Delete Provider",
|
||||
"docs_check": "Check",
|
||||
"docs_more_details": "for more details",
|
||||
"get_api_key": "Get API Key",
|
||||
"no_models": "Please add models first before checking the API connection",
|
||||
"not_checked": "Not Checked",
|
||||
"remove_duplicate_keys": "Remove Duplicate Keys",
|
||||
"remove_invalid_keys": "Remove Invalid Keys",
|
||||
"search_placeholder": "Search model id or name",
|
||||
"title": "Model Provider"
|
||||
},
|
||||
"provider.api.url.preview": "Preview: {{url}}",
|
||||
"provider.api.url.reset": "Reset",
|
||||
"provider.api.url.tip": "Ending with / ignores v1, ending with # forces use of input address",
|
||||
"provider.api_host": "API Host",
|
||||
"provider.api_key": "API Key",
|
||||
"provider.api_key.tip": "Multiple keys separated by commas",
|
||||
"provider.api_version": "API Version",
|
||||
"provider.check": "Check",
|
||||
"provider.docs_check": "Check",
|
||||
"provider.docs_more_details": "for more details",
|
||||
"provider.get_api_key": "Get API Key",
|
||||
"provider.search_placeholder": "Search model id or name",
|
||||
"proxy": {
|
||||
"mode": {
|
||||
"custom": "Custom Proxy",
|
||||
"none": "No Proxy",
|
||||
"system": "System Proxy",
|
||||
"title": "Proxy Mode"
|
||||
},
|
||||
"title": "Proxy Settings"
|
||||
},
|
||||
"proxy.title": "Proxy Address",
|
||||
"theme.title": "Theme",
|
||||
"theme.dark": "Dark",
|
||||
"theme.light": "Light",
|
||||
"theme.auto": "Auto",
|
||||
"theme.window.style.title": "Window Style",
|
||||
"theme.window.style.transparent": "Transparent Window",
|
||||
"theme.window.style.opaque": "Opaque Window",
|
||||
"font_size.title": "Message Font Size",
|
||||
"topic.position": "Topic Position",
|
||||
"topic.position.left": "Left",
|
||||
"topic.position.right": "Right",
|
||||
"topic.show.time": "Show Topic Time",
|
||||
"shortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"action": "Action",
|
||||
"key": "Key",
|
||||
"new_topic": "New Topic",
|
||||
"title": "Keyboard Shortcuts",
|
||||
"zoom_in": "Zoom In",
|
||||
"zoom_out": "Zoom Out",
|
||||
"zoom_reset": "Reset Zoom"
|
||||
}
|
||||
"zoom_reset": "Reset Zoom",
|
||||
"show_app": "Show App",
|
||||
"reset_defaults": "Reset Defaults",
|
||||
"reset_defaults_confirm": "Are you sure you want to reset all shortcuts?",
|
||||
"press_shortcut": "Press Shortcut",
|
||||
"alt_warning": "Mac does not support Option + letters as shortcuts",
|
||||
"reset_to_default": "Reset to Default",
|
||||
"clear_shortcut": "Clear Shortcut"
|
||||
},
|
||||
"theme.auto": "Auto",
|
||||
"theme.dark": "Dark",
|
||||
"theme.light": "Light",
|
||||
"theme.title": "Theme",
|
||||
"theme.window.style.opaque": "Opaque Window",
|
||||
"theme.window.style.title": "Window Style",
|
||||
"theme.window.style.transparent": "Transparent Window",
|
||||
"title": "Settings",
|
||||
"topic.position": "Topic position",
|
||||
"topic.position.left": "Left",
|
||||
"topic.position.right": "Right",
|
||||
"topic.show.time": "Show topic time",
|
||||
"tray.title": "Enable System Tray Icon"
|
||||
},
|
||||
"translate": {
|
||||
"title": "Translation",
|
||||
"any.language": "Any language",
|
||||
"button.translate": "Translate",
|
||||
"confirm": {
|
||||
"content": "Translation will replace the original text, continue?",
|
||||
"title": "Translation Confirmation"
|
||||
},
|
||||
"error.not_configured": "Translation model is not configured",
|
||||
"error.failed": "Translation failed",
|
||||
"input.placeholder": "Enter text to translate",
|
||||
"output.placeholder": "Translation",
|
||||
"confirm": "Original text has been copied to clipboard. Do you want to replace it with the translated text?"
|
||||
"processing": "Translation in progress...",
|
||||
"title": "Translation",
|
||||
"close": "Close"
|
||||
},
|
||||
"languages": {
|
||||
"english": "English",
|
||||
"chinese": "Chinese",
|
||||
"chinese-traditional": "Traditional Chinese",
|
||||
"japanese": "Japanese",
|
||||
"korean": "Korean",
|
||||
"russian": "Russian",
|
||||
"spanish": "Spanish",
|
||||
"french": "French",
|
||||
"italian": "Italian",
|
||||
"portuguese": "Portuguese",
|
||||
"arabic": "Arabic"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"keep_alive_time.title": "Keep Alive Time",
|
||||
"keep_alive_time.placeholder": "Minutes",
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes."
|
||||
},
|
||||
"error": {
|
||||
"chat.response": "Something went wrong. Please check if you have set your API key in the Settings > Providers",
|
||||
"backup.file_format": "Backup file format error",
|
||||
"provider_disabled": "Model provider is not enabled",
|
||||
"no_api_key": "API key is not configured"
|
||||
"tray": {
|
||||
"quit": "Quit",
|
||||
"show_window": "Show Window"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"export": {
|
||||
"attached_files": "Attached Files",
|
||||
"user": "User",
|
||||
"assistant": "Assistant",
|
||||
"created": "Created",
|
||||
"last_updated": "Last Updated",
|
||||
"messages": "Messages",
|
||||
"conversation_details": "Conversation Details",
|
||||
"conversation_history": "Conversation History"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
511
src/renderer/src/i18n/locales/ru-ru.json
Normal file
511
src/renderer/src/i18n/locales/ru-ru.json
Normal file
@ -0,0 +1,511 @@
|
||||
{
|
||||
"translation": {
|
||||
"agents": {
|
||||
"add.button": "Добавить к ассистенту",
|
||||
"add.name": "Имя",
|
||||
"add.name.placeholder": "Введите имя",
|
||||
"add.prompt": "Промпт",
|
||||
"add.prompt.placeholder": "Введите промпт",
|
||||
"add.title": "Создать агента",
|
||||
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
|
||||
"edit.message.add.title": "Добавить",
|
||||
"edit.message.assistant.placeholder": "Введите сообщение ассистента",
|
||||
"edit.message.assistant.title": "Ассистент",
|
||||
"edit.message.empty.content": "Содержание вводимого сообщения не может быть пустым",
|
||||
"edit.message.group.title": "Группа сообщений",
|
||||
"edit.message.title": "Предустановленные сообщения",
|
||||
"edit.message.user.placeholder": "Введите сообщение пользователя",
|
||||
"edit.message.user.title": "Пользователь",
|
||||
"edit.model.select.title": "Выбрать модель",
|
||||
"edit.settings.hide_preset_messages": "Скрыть предустановленные сообщения",
|
||||
"edit.title": "Редактировать агента",
|
||||
"manage.title": "Редактировать агентов",
|
||||
"my_agents": "Мои агенты",
|
||||
"search.no_results": "Результаты не найдены",
|
||||
"sorting.title": "Сортировка",
|
||||
"tag.agent": "Агент",
|
||||
"tag.default": "По умолчанию",
|
||||
"tag.new": "Новый",
|
||||
"tag.system": "Система",
|
||||
"title": "Агенты"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Ассистент",
|
||||
"clear.content": "Очистка топика удалит все топики и файлы в ассистенте. Вы уверены, что хотите продолжить?",
|
||||
"clear.title": "Очистить топики",
|
||||
"copy.title": "Копировать ассистента",
|
||||
"delete.content": "Удаление ассистента удалит все топики и файлы под ассистентом. Вы уверены, что хотите удалить его?",
|
||||
"delete.title": "Удалить ассистента",
|
||||
"edit.title": "Редактировать ассистента",
|
||||
"save.success": "Успешно сохранено",
|
||||
"save.title": "Сохранить в агента",
|
||||
"search": "Поиск ассистентов...",
|
||||
"settings.auto_reset_model": "Автосброс модели",
|
||||
"settings.auto_reset_model.tip": "Автоматически сбрасывать модель при создании нового топика.",
|
||||
"settings.default_model": "Модель по умолчанию",
|
||||
"settings.model": "Настройки модели",
|
||||
"settings.preset_messages": "Предустановленные сообщения",
|
||||
"settings.prompt": "Настройки промптов",
|
||||
"title": "Ассистенты"
|
||||
},
|
||||
"button": {
|
||||
"add": "Добавить",
|
||||
"added": "Добавлено",
|
||||
"collapse": "Свернуть",
|
||||
"manage": "Редактировать",
|
||||
"select_model": "Выбрать модель",
|
||||
"show.all": "Показать все"
|
||||
},
|
||||
"chat": {
|
||||
"add.assistant.title": "Добавить ассистента",
|
||||
"artifacts.button.download": "Скачать",
|
||||
"artifacts.button.preview": "Предпросмотр",
|
||||
"assistant.search.placeholder": "Поиск",
|
||||
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
|
||||
"default.name": "⭐️ Ассистент по умолчанию",
|
||||
"default.topic.name": "Топик по умолчанию",
|
||||
"input.clear": "Очистить",
|
||||
"input.clear.content": "Хотите очистить все сообщения текущего топика?",
|
||||
"input.clear.title": "Очистить все сообщения?",
|
||||
"input.collapse": "Свернуть",
|
||||
"input.context_count.tip": "Количество контекстов",
|
||||
"input.estimated_tokens.tip": "Затраты токенов",
|
||||
"input.expand": "Развернуть",
|
||||
"input.new.context": "Очистить контекст",
|
||||
"input.new_topic": "Новый топик {{Command}}+N",
|
||||
"input.pause": "Остановить",
|
||||
"input.placeholder": "Введите ваше сообщение здесь...",
|
||||
"input.send": "Отправить",
|
||||
"input.settings": "Настройки",
|
||||
"input.topics": " Топики ",
|
||||
"input.translate": "Перевести на английский",
|
||||
"input.upload": "Загрузить изображение или документ",
|
||||
"message.new.branch": "Новая ветка",
|
||||
"message.new.branch.created": "Новая ветка создана",
|
||||
"message.new.context": "Новый контекст",
|
||||
"save": "Сохранить",
|
||||
"settings.code_collapsible": "Блок кода свернут",
|
||||
"settings.context_count": "Контекст",
|
||||
"settings.context_count.tip": "Количество предыдущих сообщений, которые нужно сохранить в контексте.",
|
||||
"settings.max": "Максимум",
|
||||
"settings.max_tokens": "Включить лимит максимальных токенов",
|
||||
"settings.max_tokens.tip": "Максимальное количество токенов, которые может сгенерировать модель. Обычный чат предполагает 500-800. Генерация короткого текста предполагает 800-2000. Генерация кода предполагает 2000-3600. Генерация длинного текста предполагает выше 4000.",
|
||||
"settings.reset": "Сбросить",
|
||||
"settings.set_as_default": "Применить к ассистенту по умолчанию",
|
||||
"settings.show_line_numbers": "Показать номера строк в коде",
|
||||
"settings.temperature": "Температура",
|
||||
"settings.temperature.tip": "Меньшие значения делают модель более креативной и непредсказуемой, в то время как большие значения делают её более детерминированной и точной.",
|
||||
"suggestions.title": "Предложенные вопросы",
|
||||
"topics.auto_rename": "Автопереименование",
|
||||
"topics.clear.title": "Очистить сообщения",
|
||||
"topics.edit.placeholder": "Введите новый заголовок",
|
||||
"topics.edit.title": "Редактировать заголовок",
|
||||
"topics.export.image": "Экспорт как изображение",
|
||||
"topics.export.md": "Экспорт как markdown",
|
||||
"topics.export.title": "Экспорт",
|
||||
"topics.export.word": "Экспорт как Word",
|
||||
"topics.list": "Список топиков",
|
||||
"topics.move_to": "Переместить в",
|
||||
"topics.title": "Топики",
|
||||
"translate": "Перевести"
|
||||
},
|
||||
"common": {
|
||||
"and": "и",
|
||||
"assistant": "Ассистент",
|
||||
"avatar": "Аватар",
|
||||
"back": "Назад",
|
||||
"cancel": "Отмена",
|
||||
"chat": "Чат",
|
||||
"close": "Закрыть",
|
||||
"copy": "Копировать",
|
||||
"cut": "Вырезать",
|
||||
"default": "По умолчанию",
|
||||
"delete": "Удалить",
|
||||
"description": "Описание",
|
||||
"docs": "Документы",
|
||||
"download": "Скачать",
|
||||
"duplicate": "Дублировать",
|
||||
"edit": "Редактировать",
|
||||
"footnotes": "Сноски",
|
||||
"language": "Язык",
|
||||
"model": "Модель",
|
||||
"models": "Модели",
|
||||
"name": "Имя",
|
||||
"paste": "Вставить",
|
||||
"prompt": "Промпт",
|
||||
"provider": "Провайдер",
|
||||
"regenerate": "Пересоздать",
|
||||
"rename": "Переименовать",
|
||||
"reset": "Сбросить",
|
||||
"save": "Сохранить",
|
||||
"search": "Поиск",
|
||||
"select": "Выбрать",
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
"you": "Вы",
|
||||
"clear": "Очистить",
|
||||
"add": "Добавить"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "Ошибка формата файла резервной копии",
|
||||
"chat.response": "Что-то пошло не так. Пожалуйста, проверьте, установлен ли ваш ключ API в Настройки > Провайдеры",
|
||||
"no_api_key": "Ключ API не настроен",
|
||||
"provider_disabled": "Провайдер моделей не включен",
|
||||
"render": {
|
||||
"title": "Ошибка рендеринга",
|
||||
"description": "Не удалось рендерить формулу. Пожалуйста, проверьте, правильно ли формат формулы"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"assistant": "Ассистент",
|
||||
"attached_files": "Прикрепленные файлы",
|
||||
"conversation_details": "Детали разговора",
|
||||
"conversation_history": "История разговора",
|
||||
"created": "Создано",
|
||||
"last_updated": "Последнее обновление",
|
||||
"messages": "Сообщения",
|
||||
"user": "Пользователь"
|
||||
},
|
||||
"files": {
|
||||
"actions": "Действия",
|
||||
"all": "Все файлы",
|
||||
"count": "Количество",
|
||||
"created_at": "Дата создания",
|
||||
"document": "Документ",
|
||||
"file": "Файл",
|
||||
"image": "Изображение",
|
||||
"name": "Имя",
|
||||
"open": "Открыть",
|
||||
"size": "Размер",
|
||||
"text": "Текст",
|
||||
"title": "Файлы"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Продолжить чат",
|
||||
"locate.message": "Найти сообщение",
|
||||
"search.messages": "Поиск всех сообщений",
|
||||
"search.placeholder": "Поиск топиков или сообщений...",
|
||||
"search.topics.empty": "Топики не найдены, нажмите Enter для поиска всех сообщений",
|
||||
"title": "Поиск топиков"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "Арабский",
|
||||
"chinese": "Китайский",
|
||||
"chinese-traditional": "Китайский традиционный",
|
||||
"english": "Английский",
|
||||
"french": "Французский",
|
||||
"italian": "Итальянский",
|
||||
"japanese": "Японский",
|
||||
"korean": "Корейский",
|
||||
"portuguese": "Португальский",
|
||||
"russian": "Русский",
|
||||
"spanish": "Испанский"
|
||||
},
|
||||
"mermaid": {
|
||||
"download": {
|
||||
"png": "Скачать PNG",
|
||||
"svg": "Скачать SVG"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "Предпросмотр",
|
||||
"source": "Исходный код"
|
||||
},
|
||||
"title": "Диаграмма Mermaid"
|
||||
},
|
||||
"message": {
|
||||
"api.connection.failed": "Соединение не удалось",
|
||||
"api.connection.success": "Соединение успешно",
|
||||
"assistant.added.content": "Ассистент успешно добавлен",
|
||||
"backup.failed": "Создание резервной копии не удалось",
|
||||
"backup.success": "Резервная копия успешно создана",
|
||||
"chat.completion.paused": "Завершение чата приостановлено",
|
||||
"copied": "Скопировано!",
|
||||
"error.enter.api.host": "Пожалуйста, введите ваш API хост",
|
||||
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
|
||||
"error.enter.model": "Пожалуйста, выберите модель",
|
||||
"error.invalid.proxy.url": "Неверный URL прокси",
|
||||
"error.invalid.webdav": "Неверные настройки WebDAV",
|
||||
"message.code_style": "Стиль кода",
|
||||
"message.delete.content": "Вы уверены, что хотите удалить это сообщение?",
|
||||
"message.delete.title": "Удалить сообщение",
|
||||
"message.style": "Стиль сообщения",
|
||||
"message.style.bubble": "Пузырь",
|
||||
"message.style.plain": "Простой",
|
||||
"reset.confirm.content": "Вы уверены, что хотите очистить все данные?",
|
||||
"reset.double.confirm.content": "Все данные будут утеряны, хотите продолжить?",
|
||||
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
|
||||
"restore.success": "Успешно восстановлено",
|
||||
"save.success.title": "Успешно сохранено",
|
||||
"switch.disabled": "Переключение отключено, пока ассистент генерирует",
|
||||
"topic.added": "Новый топик добавлен",
|
||||
"upgrade.success.button": "Перезапустить",
|
||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||
"upgrade.success.title": "Обновление успешно",
|
||||
"regenerate.confirm": "Перегенерация заменит текущее сообщение"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "Встроенные приложения"
|
||||
},
|
||||
"model": {
|
||||
"pinned": "Закреплено",
|
||||
"search": "Поиск моделей...",
|
||||
"stream_output": "Потоковый вывод",
|
||||
"type": {
|
||||
"select": "Выберите тип модели",
|
||||
"text": "Текст",
|
||||
"vision": "Изображение"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
"keep_alive_time.placeholder": "Минуты",
|
||||
"keep_alive_time.title": "Время жизни модели",
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"button.delete.image": "Удалить изображение",
|
||||
"button.delete.image.confirm": "Вы уверены, что хотите удалить это изображение?",
|
||||
"button.new.image": "Новое изображение",
|
||||
"guidance_scale": "Масштаб руководства",
|
||||
"guidance_scale_tip": "Без классификатора руководства. Насколько близко вы хотите, чтобы модель придерживалась вашего промпта при поиске связанного изображения для показа вам",
|
||||
"image.size": "Размер изображения",
|
||||
"inference_steps": "Шаги вывода",
|
||||
"inference_steps_tip": "Количество шагов вывода для выполнения. Больше шагов производят более высокое качество, но занимают больше времени",
|
||||
"negative_prompt": "Негативный промпт",
|
||||
"negative_prompt_tip": "Опишите, что вы не хотите включать в изображение",
|
||||
"number_images": "Количество изображений",
|
||||
"number_images_tip": "Количество изображений для генерации (1-4)",
|
||||
"prompt_placeholder": "Опишите изображение, которое вы хотите создать, например, Спокойное озеро на закате с горами на заднем плане",
|
||||
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
|
||||
"seed": "Ключ генерации",
|
||||
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
|
||||
"title": "Изображения"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Baichuan",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
"doubao": "Doubao",
|
||||
"fireworks": "Fireworks",
|
||||
"gemini": "Gemini",
|
||||
"github": "GitHub Models",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"jina": "Jina",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"moonshot": "Moonshot",
|
||||
"nvidia": "Nvidia",
|
||||
"ocoolai": "ocoolAI",
|
||||
"ollama": "Ollama",
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter",
|
||||
"silicon": "SiliconFlow",
|
||||
"stepfun": "StepFun",
|
||||
"together": "Together",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI"
|
||||
},
|
||||
"settings": {
|
||||
"about": "О программе и обратная связь",
|
||||
"about.checkUpdate": "Проверить обновления",
|
||||
"about.checkingUpdate": "Проверка обновлений...",
|
||||
"about.contact.button": "Электронная почта",
|
||||
"about.contact.title": "Контакты",
|
||||
"about.description": "Мощный AI-ассистент для созидания",
|
||||
"about.downloading": "Загрузка...",
|
||||
"about.feedback.button": "Обратная связь",
|
||||
"about.feedback.title": "Обратная связь",
|
||||
"about.license.button": "Лицензия",
|
||||
"about.license.title": "Лицензия",
|
||||
"about.releases.button": "Релизы",
|
||||
"about.releases.title": "Заметки о релизах",
|
||||
"about.title": "О программе",
|
||||
"about.updateAvailable": "Найдено новое обновление {{version}}",
|
||||
"about.updateError": "Ошибка обновления",
|
||||
"about.updateNotAvailable": "Вы используете последнюю версию",
|
||||
"about.website.button": "Сайт",
|
||||
"about.website.title": "Официальный сайт",
|
||||
"advanced.auto_switch_to_topics": "Автоматически переключаться на топик",
|
||||
"advanced.title": "Расширенные настройки",
|
||||
"assistant": "Ассистент по умолчанию",
|
||||
"assistant.model_params": "Параметры модели",
|
||||
"assistant.title": "Ассистент по умолчанию",
|
||||
"data": {
|
||||
"app_data": "Данные приложения",
|
||||
"app_logs": "Логи приложения",
|
||||
"clear_cache": {
|
||||
"button": "Очистка кэша",
|
||||
"confirm": "Очистка кэша удалит данные приложения. Это действие необратимо, продолжить?",
|
||||
"error": "Ошибка при очистке кэша",
|
||||
"success": "Кэш очищен",
|
||||
"title": "Очистка кэша"
|
||||
},
|
||||
"data.title": "Каталог данных",
|
||||
"title": "Настройки данных",
|
||||
"webdav.backup.button": "Резервное копирование на WebDAV",
|
||||
"webdav.host": "Хост WebDAV",
|
||||
"webdav.host.placeholder": "http://localhost:8080",
|
||||
"webdav.password": "Пароль WebDAV",
|
||||
"webdav.path": "Путь WebDAV",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.restore.button": "Восстановление с WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "Пользователь WebDAV"
|
||||
},
|
||||
"display.title": "Настройки отображения",
|
||||
"font_size.title": "Размер шрифта сообщений",
|
||||
"general": "Общие настройки",
|
||||
"general.backup.button": "Резервное копирование",
|
||||
"general.backup.title": "Резервное копирование и восстановление данных",
|
||||
"general.manually_check_update.title": "Отключить проверку обновлений",
|
||||
"general.reset.button": "Сброс",
|
||||
"general.reset.title": "Сброс данных",
|
||||
"general.restore.button": "Восстановление",
|
||||
"general.title": "Общие настройки",
|
||||
"general.user_name": "Имя пользователя",
|
||||
"general.user_name.placeholder": "Введите ваше имя",
|
||||
"general.view_webdav_settings": "Просмотр настроек WebDAV",
|
||||
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.input.paste_long_text_as_file": "Вставлять длинный текст как файл",
|
||||
"messages.input.send_shortcuts": "Горячие клавиши для отправки",
|
||||
"messages.input.show_estimated_tokens": "Показывать затраты токенов",
|
||||
"messages.input.title": "Настройки ввода",
|
||||
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
|
||||
"messages.math_engine": "Математический движок",
|
||||
"messages.model.title": "Настройки модели",
|
||||
"messages.title": "Настройки сообщений",
|
||||
"messages.use_serif_font": "Использовать serif шрифт",
|
||||
"model": "Модель по умолчанию",
|
||||
"models.add.add_model": "Добавить модель",
|
||||
"models.add.group_name": "Имя группы",
|
||||
"models.add.group_name.placeholder": "Необязательно, например, ChatGPT",
|
||||
"models.add.group_name.tooltip": "Необязательно, например, ChatGPT",
|
||||
"models.add.model_id": "ID модели",
|
||||
"models.add.model_id.placeholder": "Обязательно, например, gpt-3.5-turbo",
|
||||
"models.add.model_id.tooltip": "Пример: gpt-3.5-turbo",
|
||||
"models.add.model_name": "Имя модели",
|
||||
"models.add.model_name.placeholder": "Необязательно, например, GPT-4",
|
||||
"models.default_assistant_model": "Модель ассистента по умолчанию",
|
||||
"models.default_assistant_model_description": "Модель, используемая при создании нового ассистента, если ассистент не имеет настроенной модели, будет использоваться эта модель",
|
||||
"models.empty": "Модели не найдены",
|
||||
"models.topic_naming_model": "Модель именования топика",
|
||||
"models.topic_naming_model_description": "Модель, используемая при автоматическом именовании нового топика",
|
||||
"models.translate_model": "Модель перевода",
|
||||
"models.translate_model_description": "Модель, используемая для сервиса перевода",
|
||||
"models.translate_model_prompt_message": "Введите модель перевода",
|
||||
"models.translate_model_prompt_title": "Модель перевода",
|
||||
"models.topic_naming_model_setting_title": "Настройки модели именования топика",
|
||||
"models.enable_topic_naming": "Автоматическое переименование топика",
|
||||
"provider": {
|
||||
"add.name": "Имя провайдера",
|
||||
"add.name.placeholder": "Пример: OpenAI",
|
||||
"add.title": "Добавить провайдер",
|
||||
"add.type": "Тип провайдера",
|
||||
"api.url.preview": "Предпросмотр: {{url}}",
|
||||
"api.url.reset": "Сброс",
|
||||
"api.url.tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес",
|
||||
"api_host": "Хост API",
|
||||
"api_key": "Ключ API",
|
||||
"api_key.tip": "Несколько ключей, разделенных запятыми",
|
||||
"api_version": "Версия API",
|
||||
"check": "Проверить",
|
||||
"check_all_keys": "Проверить все ключи",
|
||||
"check_multiple_keys": "Проверить несколько ключей API",
|
||||
"delete.content": "Вы уверены, что хотите удалить этот провайдер?",
|
||||
"delete.title": "Удалить провайдер",
|
||||
"docs_check": "Проверить",
|
||||
"docs_more_details": "для получения дополнительной информации",
|
||||
"get_api_key": "Получить ключ API",
|
||||
"no_models": "Пожалуйста, добавьте модели перед проверкой соединения с API",
|
||||
"not_checked": "Не проверено",
|
||||
"remove_duplicate_keys": "Удалить дубликаты ключей",
|
||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||
"search_placeholder": "Поиск по ID или имени модели",
|
||||
"title": "Провайдеры моделей"
|
||||
},
|
||||
"provider.api.url.preview": "Предпросмотр: {{url}}",
|
||||
"provider.api.url.reset": "Сброс",
|
||||
"provider.api.url.tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес",
|
||||
"provider.api_host": "Хост API",
|
||||
"provider.api_key": "Ключ API",
|
||||
"provider.api_key.tip": "Несколько ключей, разделенных запятыми",
|
||||
"provider.api_version": "Версия API",
|
||||
"provider.check": "Проверить",
|
||||
"provider.docs_check": "Проверить",
|
||||
"provider.docs_more_details": "для получения дополнительной информации",
|
||||
"provider.get_api_key": "Получить ключ API",
|
||||
"provider.search_placeholder": "Поиск по ID или имени модели",
|
||||
"proxy": {
|
||||
"mode": {
|
||||
"custom": "Пользовательский прокси",
|
||||
"none": "Не использовать прокси",
|
||||
"system": "Системный прокси",
|
||||
"title": "Режим прокси"
|
||||
},
|
||||
"title": "Настройки прокси"
|
||||
},
|
||||
"proxy.title": "Адрес прокси",
|
||||
"shortcuts": {
|
||||
"action": "Действие",
|
||||
"key": "Клавиша",
|
||||
"new_topic": "Новый топик",
|
||||
"title": "Горячие клавиши",
|
||||
"zoom_in": "Увеличить",
|
||||
"zoom_out": "Уменьшить",
|
||||
"zoom_reset": "Сбросить масштаб",
|
||||
"show_app": "Показать приложение",
|
||||
"reset_defaults": "Сбросить настройки по умолчанию",
|
||||
"reset_defaults_confirm": "Вы уверены, что хотите сбросить все горячие клавиши?",
|
||||
"press_shortcut": "Нажмите сочетание клавиш",
|
||||
"alt_warning": "Mac не поддерживает Option + буквы как горячие клавиши",
|
||||
"reset_to_default": "Сбросить настройки по умолчанию",
|
||||
"clear_shortcut": "Очистить сочетание клавиш"
|
||||
},
|
||||
"theme.auto": "Автоматически",
|
||||
"theme.dark": "Темная",
|
||||
"theme.light": "Светлая",
|
||||
"theme.title": "Тема",
|
||||
"theme.window.style.opaque": "Непрозрачное окно",
|
||||
"theme.window.style.title": "Стиль окна",
|
||||
"theme.window.style.transparent": "Прозрачное окно",
|
||||
"title": "Настройки",
|
||||
"topic.position": "Позиция топиков",
|
||||
"topic.position.left": "Слева",
|
||||
"topic.position.right": "Справа",
|
||||
"topic.show.time": "Показывать время топика",
|
||||
"tray.title": "Включить значок системного трея"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Любой язык",
|
||||
"button.translate": "Перевести",
|
||||
"confirm": {
|
||||
"content": "Перевод заменит исходный текст, продолжить?",
|
||||
"title": "Перевод подтверждение"
|
||||
},
|
||||
"error.not_configured": "Модель перевода не настроена",
|
||||
"error.failed": "Перевод не удалось",
|
||||
"input.placeholder": "Введите текст для перевода",
|
||||
"output.placeholder": "Перевод",
|
||||
"processing": "Перевод в процессе...",
|
||||
"title": "Перевод",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Выйти",
|
||||
"show_window": "Показать окно"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Граф знаний",
|
||||
"visualization": "Визуализация"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,407 +1,499 @@
|
||||
{
|
||||
"translation": {
|
||||
"common": {
|
||||
"avatar": "头像",
|
||||
"language": "语言",
|
||||
"model": "模型",
|
||||
"models": "模型",
|
||||
"topics": "话题",
|
||||
"docs": "文档",
|
||||
"and": "和",
|
||||
"assistant": "智能体",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"prompt": "提示词",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"duplicate": "复制",
|
||||
"copy": "复制",
|
||||
"regenerate": "重新生成",
|
||||
"provider": "提供商",
|
||||
"you": "用户",
|
||||
"save": "保存",
|
||||
"footnote": "引用内容",
|
||||
"select": "选择",
|
||||
"search": "搜索",
|
||||
"default": "默认",
|
||||
"warning": "警告",
|
||||
"back": "返回",
|
||||
"chat": "聊天",
|
||||
"close": "关闭",
|
||||
"cancel": "取消",
|
||||
"download": "下载"
|
||||
},
|
||||
"button": {
|
||||
"add": "添加",
|
||||
"added": "已添加",
|
||||
"manage": "管理",
|
||||
"select_model": "选择模型",
|
||||
"show.all": "显示全部",
|
||||
"collapse": "收起"
|
||||
},
|
||||
"message": {
|
||||
"copied": "已复制",
|
||||
"assistant.added.content": "智能体添加成功",
|
||||
"message.delete.title": "删除消息",
|
||||
"message.delete.content": "确定要删除此消息吗?",
|
||||
"error.enter.api.key": "请输入您的 API 密钥",
|
||||
"error.enter.api.host": "请输入您的 API 地址",
|
||||
"error.enter.model": "请选择一个模型",
|
||||
"error.invalid.proxy.url": "无效的代理地址",
|
||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||
"api.connection.failed": "连接失败",
|
||||
"api.connection.success": "连接成功",
|
||||
"chat.completion.paused": "会话已停止",
|
||||
"switch.disabled": "模型回复完成后才能切换",
|
||||
"restore.success": "恢复成功",
|
||||
"backup.success": "备份成功",
|
||||
"backup.failed": "备份失败",
|
||||
"reset.confirm.content": "确定要重置所有数据吗?",
|
||||
"reset.double.confirm.title": "数据丢失!!!",
|
||||
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
||||
"upgrade.success.title": "升级成功",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.button": "重启",
|
||||
"topic.added": "话题添加成功",
|
||||
"save.success.title": "保存成功",
|
||||
"message.style": "消息样式",
|
||||
"message.style.bubble": "气泡",
|
||||
"message.style.plain": "简洁"
|
||||
},
|
||||
"chat": {
|
||||
"save": "保存",
|
||||
"default.name": "⭐️ 默认助手",
|
||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||
"default.topic.name": "默认话题",
|
||||
"topics.title": "话题",
|
||||
"topics.auto_rename": "生成话题名",
|
||||
"topics.edit.title": "编辑话题名",
|
||||
"topics.edit.placeholder": "输入新名称",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.move_to": "移动到",
|
||||
"topics.list": "话题列表",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.image": "导出为图片",
|
||||
"topics.export.md": "导出为 Markdown",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"input.new_topic": "新话题",
|
||||
"input.topics": " 话题 ",
|
||||
"input.clear": "清空消息",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.expand": "展开",
|
||||
"input.collapse": "收起",
|
||||
"input.clear.title": "清空消息",
|
||||
"input.clear.content": "确定要清除当前会话所有消息吗?",
|
||||
"input.placeholder": "在这里输入消息...",
|
||||
"input.send": "发送",
|
||||
"input.pause": "暂停",
|
||||
"input.settings": "设置",
|
||||
"input.upload": "上传图片或文档",
|
||||
"input.context_count.tip": "上下文数",
|
||||
"input.estimated_tokens.tip": "预估 token 数",
|
||||
"settings.temperature": "模型温度",
|
||||
"settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7",
|
||||
"settings.conext_count": "上下文数",
|
||||
"settings.conext_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10",
|
||||
"settings.max_tokens": "开启消息长度限制",
|
||||
"settings.max_tokens.tip": "单次交互所用的最大 Token 数, 会影响返回结果的长度。普通聊天建议 500-800;短文生成建议 800-2000;代码生成建议 2000-3600;长文生成建议切换模型到 4000 左右",
|
||||
"settings.reset": "重置",
|
||||
"settings.set_as_default": "应用到默认助手",
|
||||
"settings.max": "不限",
|
||||
"settings.show_line_numbers": "代码显示行号",
|
||||
"suggestions.title": "建议的问题",
|
||||
"add.assistant.title": "添加助手",
|
||||
"message.new.context": "清除上下文",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已创建",
|
||||
"assistant.search.placeholder": "搜索",
|
||||
"artifacts.button.preview": "预览",
|
||||
"artifacts.button.download": "下载"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "助手",
|
||||
"abbr": "助手",
|
||||
"search": "搜索助手",
|
||||
"settings.prompt": "提示词设置",
|
||||
"settings.model": "模型设置",
|
||||
"settings.preset_messages": "预设消息",
|
||||
"settings.default_model": "默认模型",
|
||||
"settings.auto_reset_model": "自动重置模型",
|
||||
"settings.auto_reset_model.tip": "创建新话题时自动重置模型",
|
||||
"edit.title": "编辑助手",
|
||||
"copy.title": "复制助手",
|
||||
"clear.title": "清空话题",
|
||||
"clear.content": "清空话题会删除助手下所有话题和文件,确定要继续吗?",
|
||||
"save.title": "保存到智能体",
|
||||
"save.success": "保存成功",
|
||||
"delete.title": "删除助手",
|
||||
"delete.content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?"
|
||||
},
|
||||
"model": {
|
||||
"stream_output": "流式输出",
|
||||
"search": "搜索模型..."
|
||||
},
|
||||
"images": {
|
||||
"title": "图片",
|
||||
"image.size": "图片尺寸",
|
||||
"button.new.image": "新建图片",
|
||||
"button.delete.image": "删除图片",
|
||||
"button.delete.image.confirm": "确定要删除此图片吗?",
|
||||
"number_images": "生成数量",
|
||||
"number_images_tip": "一次生成的图片数量 (1-4)",
|
||||
"seed": "随机种子",
|
||||
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||
"inference_steps": "推理步数",
|
||||
"inference_steps_tip": "要执行的推理步数。步数越多,质量越高但耗时越长",
|
||||
"guidance_scale": "引导比例",
|
||||
"guidance_scale_tip": "无分类器指导。控制模型在寻找相关图像时对提示词的遵循程度",
|
||||
"negative_prompt": "反向提示词",
|
||||
"negative_prompt_tip": "描述你不想在图片中出现的内容",
|
||||
"prompt_placeholder": "描述你想创建的图片,例如:一个宁静的湖泊,夕阳西下,远处是群山",
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?"
|
||||
},
|
||||
"files": {
|
||||
"title": "文件",
|
||||
"file": "文件",
|
||||
"name": "文件名",
|
||||
"size": "大小",
|
||||
"count": "文件数",
|
||||
"created_at": "创建时间",
|
||||
"image": "图片",
|
||||
"text": "文本",
|
||||
"document": "文档",
|
||||
"actions": "操作",
|
||||
"open": "打开",
|
||||
"all": "所有文件"
|
||||
},
|
||||
"agents": {
|
||||
"title": "智能体",
|
||||
"my_agents": "我的智能体",
|
||||
"add.title": "创建智能体",
|
||||
"edit.title": "编辑智能体",
|
||||
"add.button": "添加到助手",
|
||||
"add.name": "名称",
|
||||
"add.name.placeholder": "输入名称",
|
||||
"add.prompt": "提示词",
|
||||
"add.prompt.placeholder": "输入提示词",
|
||||
"add.button": "添加到助手",
|
||||
"manage.title": "管理智能体",
|
||||
"add.title": "创建智能体",
|
||||
"delete.popup.content": "确定要删除此智能体吗?",
|
||||
"tag.default": "默认",
|
||||
"tag.system": "系统",
|
||||
"tag.agent": "智能体",
|
||||
"edit.message.title": "预设消息",
|
||||
"edit.message.add.title": "添加",
|
||||
"edit.message.group.title": "消息组",
|
||||
"edit.message.assistant.title": "助手",
|
||||
"edit.message.assistant.placeholder": "输入助手消息",
|
||||
"edit.message.user.title": "用户",
|
||||
"edit.message.user.placeholder": "输入用户消息",
|
||||
"edit.message.assistant.title": "助手",
|
||||
"edit.message.empty.content": "会话输入内容不能为空",
|
||||
"edit.message.group.title": "消息组",
|
||||
"edit.message.title": "预设消息",
|
||||
"edit.message.user.placeholder": "输入用户消息",
|
||||
"edit.message.user.title": "用户",
|
||||
"edit.model.select.title": "选择模型",
|
||||
"edit.settings.hide_preset_messages": "隐藏预设消息",
|
||||
"edit.title": "编辑智能体",
|
||||
"manage.title": "管理智能体",
|
||||
"my_agents": "我的智能体",
|
||||
"search.no_results": "没有找到相关智能体",
|
||||
"sorting.title": "排序"
|
||||
"sorting.title": "排序",
|
||||
"tag.agent": "智能体",
|
||||
"tag.default": "默认",
|
||||
"tag.new": "新建",
|
||||
"tag.system": "系统",
|
||||
"title": "智能体"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear.content": "清空话题会删除助手下所有话题和文件,确定要继续吗?",
|
||||
"clear.title": "清空话题",
|
||||
"copy.title": "复制助手",
|
||||
"delete.content": "删除助手会删除所有该助手下的话题和文件,确定要继续吗?",
|
||||
"delete.title": "删除助手",
|
||||
"edit.title": "编辑助手",
|
||||
"save.success": "保存成功",
|
||||
"save.title": "保存到智能体",
|
||||
"search": "搜索助手",
|
||||
"settings.auto_reset_model": "自动重置模型",
|
||||
"settings.auto_reset_model.tip": "创建新话题时自动重置模型",
|
||||
"settings.default_model": "默认模型",
|
||||
"settings.model": "模型设置",
|
||||
"settings.preset_messages": "预设消息",
|
||||
"settings.prompt": "提示词设置",
|
||||
"title": "助手"
|
||||
},
|
||||
"button": {
|
||||
"add": "添加",
|
||||
"added": "已添加",
|
||||
"collapse": "收起",
|
||||
"manage": "管理",
|
||||
"select_model": "选择模型",
|
||||
"show.all": "显示全部"
|
||||
},
|
||||
"chat": {
|
||||
"add.assistant.title": "添加助手",
|
||||
"artifacts.button.download": "下载",
|
||||
"artifacts.button.preview": "预览",
|
||||
"assistant.search.placeholder": "搜索",
|
||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||
"default.name": "⭐️ 默认助手",
|
||||
"default.topic.name": "默认话题",
|
||||
"input.clear": "清空消息",
|
||||
"input.clear.content": "确定要清除当前会话所有消息吗?",
|
||||
"input.clear.title": "清空消息",
|
||||
"input.collapse": "收起",
|
||||
"input.context_count.tip": "上下文数",
|
||||
"input.estimated_tokens.tip": "预估 token 数",
|
||||
"input.expand": "展开",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.new_topic": "新话题 {{Command}}+N",
|
||||
"input.pause": "暂停",
|
||||
"input.placeholder": "在这里输入消息...",
|
||||
"input.send": "发送",
|
||||
"input.settings": "设置",
|
||||
"input.topics": " 话题 ",
|
||||
"input.translate": "翻译成英文",
|
||||
"input.upload": "上传图片或文档",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已创建",
|
||||
"message.new.context": "清除上下文",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "代码块可折叠",
|
||||
"settings.context_count": "上下文数",
|
||||
"settings.context_count.tip": "要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10",
|
||||
"settings.max": "不限",
|
||||
"settings.max_tokens": "开启消息长度限制",
|
||||
"settings.max_tokens.tip": "单次交互所用的最大 Token 数, 会影响返回结果的长度。普通聊天建议 500-800;短文生成建议 800-2000;代码生成建议 2000-3600;长文生成建议切换模型到 4000 左右",
|
||||
"settings.reset": "重置",
|
||||
"settings.set_as_default": "应用到默认助手",
|
||||
"settings.show_line_numbers": "代码显示行号",
|
||||
"settings.temperature": "模型温度",
|
||||
"settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7",
|
||||
"suggestions.title": "建议的问题",
|
||||
"topics.auto_rename": "生成话题名",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.edit.placeholder": "输入新名称",
|
||||
"topics.edit.title": "编辑话题名",
|
||||
"topics.export.image": "导出为图片",
|
||||
"topics.export.md": "导出为 Markdown",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"topics.list": "话题列表",
|
||||
"topics.move_to": "移动到",
|
||||
"topics.title": "话题",
|
||||
"translate": "翻译"
|
||||
},
|
||||
"common": {
|
||||
"and": "和",
|
||||
"assistant": "智能体",
|
||||
"avatar": "头像",
|
||||
"back": "返回",
|
||||
"cancel": "取消",
|
||||
"chat": "聊天",
|
||||
"close": "关闭",
|
||||
"copy": "复制",
|
||||
"cut": "剪切",
|
||||
"default": "默认",
|
||||
"delete": "删除",
|
||||
"description": "描述",
|
||||
"docs": "文档",
|
||||
"download": "下载",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
"footnote": "引用内容",
|
||||
"language": "语言",
|
||||
"model": "模型",
|
||||
"models": "模型",
|
||||
"name": "名称",
|
||||
"paste": "粘贴",
|
||||
"prompt": "提示词",
|
||||
"provider": "提供商",
|
||||
"regenerate": "重新生成",
|
||||
"rename": "重命名",
|
||||
"reset": "重置",
|
||||
"save": "保存",
|
||||
"search": "搜索",
|
||||
"select": "选择",
|
||||
"topics": "话题",
|
||||
"warning": "警告",
|
||||
"you": "用户",
|
||||
"clear": "清除",
|
||||
"add": "添加"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "备份文件格式错误",
|
||||
"chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥",
|
||||
"no_api_key": "API 密钥未配置",
|
||||
"provider_disabled": "模型提供商未启用",
|
||||
"render": {
|
||||
"title": "渲染错误",
|
||||
"description": "渲染公式失败,请检查公式格式是否正确"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"assistant": "助手",
|
||||
"attached_files": "附件",
|
||||
"conversation_details": "会话详情",
|
||||
"conversation_history": "会话历史",
|
||||
"created": "创建时间",
|
||||
"last_updated": "最后更新",
|
||||
"messages": "消息数",
|
||||
"user": "用户"
|
||||
},
|
||||
"files": {
|
||||
"actions": "操作",
|
||||
"all": "所有文件",
|
||||
"count": "文件数",
|
||||
"created_at": "创建时间",
|
||||
"document": "文档",
|
||||
"file": "文件",
|
||||
"image": "图片",
|
||||
"name": "文件名",
|
||||
"open": "打开",
|
||||
"size": "大小",
|
||||
"text": "文本",
|
||||
"title": "文件"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "继续聊天",
|
||||
"locate.message": "定位到消息",
|
||||
"search.messages": "搜索所有消息",
|
||||
"search.placeholder": "搜索话题或消息...",
|
||||
"search.topics.empty": "没有找到相关话题, 点击回车键搜索所有消息",
|
||||
"title": "话题搜索"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "阿拉伯文",
|
||||
"chinese": "简体中文",
|
||||
"chinese-traditional": "繁体中文",
|
||||
"english": "英文",
|
||||
"french": "法文",
|
||||
"italian": "意大利文",
|
||||
"japanese": "日文",
|
||||
"korean": "韩文",
|
||||
"portuguese": "葡萄牙文",
|
||||
"russian": "俄文",
|
||||
"spanish": "西班牙文"
|
||||
},
|
||||
"mermaid": {
|
||||
"download": {
|
||||
"png": "下载 PNG",
|
||||
"svg": "下载 SVG"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "预览",
|
||||
"source": "源码"
|
||||
},
|
||||
"title": "Mermaid 图表"
|
||||
},
|
||||
"message": {
|
||||
"api.connection.failed": "连接失败",
|
||||
"api.connection.success": "连接成功",
|
||||
"assistant.added.content": "智能体添加成功",
|
||||
"backup.failed": "备份失败",
|
||||
"backup.success": "备份成功",
|
||||
"chat.completion.paused": "会话已停止",
|
||||
"copied": "已复制",
|
||||
"error.enter.api.host": "请输入您的 API 地址",
|
||||
"error.enter.api.key": "请输入您的 API 密钥",
|
||||
"error.enter.model": "请选择一个模型",
|
||||
"error.invalid.proxy.url": "无效的代理地址",
|
||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||
"message.code_style": "代码风格",
|
||||
"message.delete.content": "确定要删除此消息吗?",
|
||||
"message.delete.title": "删除消息",
|
||||
"message.style": "消息样式",
|
||||
"message.style.bubble": "气泡",
|
||||
"message.style.plain": "简洁",
|
||||
"reset.confirm.content": "确定要重置所有数据吗?",
|
||||
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
|
||||
"reset.double.confirm.title": "数据丢失!!!",
|
||||
"restore.success": "恢复成功",
|
||||
"save.success.title": "保存成功",
|
||||
"switch.disabled": "模型回复完成后才能切换",
|
||||
"topic.added": "话题添加成功",
|
||||
"upgrade.success.button": "重启",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.title": "升级成功",
|
||||
"regenerate.confirm": "重新生成会覆盖当前消息"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序"
|
||||
},
|
||||
"history": {
|
||||
"title": "话题搜索",
|
||||
"search.placeholder": "搜索话题或消息...",
|
||||
"continue_chat": "继续聊天",
|
||||
"search.topics.empty": "没有找到相关话题, 点击回车键搜索所有消息",
|
||||
"search.messages": "搜索所有消息",
|
||||
"locate.message": "定位到消息"
|
||||
"model": {
|
||||
"pinned": "已固定",
|
||||
"search": "搜索模型...",
|
||||
"stream_output": "流式输出",
|
||||
"type": {
|
||||
"select": "选择模型类型",
|
||||
"text": "文本",
|
||||
"vision": "图像"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
||||
"keep_alive_time.placeholder": "分钟",
|
||||
"keep_alive_time.title": "保持活跃时间",
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"button.delete.image": "删除图片",
|
||||
"button.delete.image.confirm": "确定要删除此图片吗?",
|
||||
"button.new.image": "新建图片",
|
||||
"guidance_scale": "引导比例",
|
||||
"guidance_scale_tip": "无分类器指导。控制模型在寻找相关图像时对提示词的遵循程度",
|
||||
"image.size": "图片尺寸",
|
||||
"inference_steps": "推理步数",
|
||||
"inference_steps_tip": "要执行的推理步数。步数越多,质量越高但耗时越长",
|
||||
"negative_prompt": "反向提示词",
|
||||
"negative_prompt_tip": "描述你不想在图片中出现的内容",
|
||||
"number_images": "生成数量",
|
||||
"number_images_tip": "一次生成的图片数量 (1-4)",
|
||||
"prompt_placeholder": "描述你想创建的图片,例如:一个宁静的湖泊,夕阳西下,远处是群山",
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
|
||||
"seed": "随机种子",
|
||||
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||
"title": "图片"
|
||||
},
|
||||
"provider": {
|
||||
"jina": "Jina",
|
||||
"mistral": "Mistral",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"grok": "Grok",
|
||||
"nvidia": "英伟达",
|
||||
"hunyuan": "腾讯混元",
|
||||
"zhinao": "360智脑",
|
||||
"fireworks": "Fireworks",
|
||||
"together": "Together",
|
||||
"openai": "OpenAI",
|
||||
"gemini": "Gemini",
|
||||
"deepseek": "深度求索",
|
||||
"moonshot": "月之暗面",
|
||||
"silicon": "硅基流动",
|
||||
"openrouter": "OpenRouter",
|
||||
"yi": "零一万物",
|
||||
"zhipu": "智谱AI",
|
||||
"groq": "Groq",
|
||||
"ollama": "Ollama",
|
||||
"aihubmix": "AiHubMix",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"dashscope": "阿里云百炼",
|
||||
"anthropic": "Anthropic",
|
||||
"aihubmix": "AiHubMix",
|
||||
"stepfun": "阶跃星辰",
|
||||
"deepseek": "深度求索",
|
||||
"doubao": "豆包",
|
||||
"minimax": "MiniMax",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"fireworks": "Fireworks",
|
||||
"gemini": "Gemini",
|
||||
"github": "GitHub Models",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"hunyuan": "腾讯混元",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"jina": "Jina",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"moonshot": "月之暗面",
|
||||
"nvidia": "英伟达",
|
||||
"ocoolai": "ocoolAI",
|
||||
"azure-openai": "Azure OpenAI"
|
||||
"ollama": "Ollama",
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter",
|
||||
"silicon": "硅基流动",
|
||||
"stepfun": "阶跃星辰",
|
||||
"together": "Together",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"general": "常规设置",
|
||||
"data": "数据设置",
|
||||
"provider": "模型服务",
|
||||
"model": "默认模型",
|
||||
"assistant": "默认助手",
|
||||
"about": "关于我们",
|
||||
"messages.model.title": "模型设置",
|
||||
"messages.title": "消息设置",
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.use_serif_font": "使用衬线字体",
|
||||
"messages.input.title": "输入设置",
|
||||
"messages.input.show_estimated_tokens": "状态显示",
|
||||
"messages.input.send_shortcuts": "发送快捷键",
|
||||
"messages.input.paste_long_text_as_file": "长文本粘贴为文件",
|
||||
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||
"messages.math_engine": "数学公式引擎",
|
||||
"about.checkUpdate": "检查更新",
|
||||
"about.checkingUpdate": "正在检查更新...",
|
||||
"about.contact.button": "邮件",
|
||||
"about.contact.title": "邮件联系",
|
||||
"about.description": "一款为创造者而生的 AI 助手",
|
||||
"about.downloading": "正在下载更新...",
|
||||
"about.feedback.button": "反馈",
|
||||
"about.feedback.title": "意见反馈",
|
||||
"about.license.button": "查看",
|
||||
"about.license.title": "许可证",
|
||||
"about.releases.button": "查看",
|
||||
"about.releases.title": "更新日志",
|
||||
"about.title": "关于我们",
|
||||
"about.updateAvailable": "发现新版本 {{version}}",
|
||||
"about.updateError": "更新出错",
|
||||
"about.updateNotAvailable": "你的软件已是最新版本",
|
||||
"about.website.button": "查看",
|
||||
"about.website.title": "官方网站",
|
||||
"advanced.auto_switch_to_topics": "自动切换到话题",
|
||||
"advanced.title": "高级设置",
|
||||
"assistant": "默认助手",
|
||||
"assistant.model_params": "模型参数",
|
||||
"assistant.title": "默认助手",
|
||||
"data": {
|
||||
"app_data": "应用数据",
|
||||
"app_logs": "应用日志",
|
||||
"clear_cache": {
|
||||
"button": "清除缓存",
|
||||
"confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?",
|
||||
"error": "清除缓存失败",
|
||||
"success": "缓存清除成功",
|
||||
"title": "清除缓存"
|
||||
},
|
||||
"data.title": "数据目录",
|
||||
"title": "数据设置",
|
||||
"webdav.backup.button": "备份到 WebDAV",
|
||||
"webdav.host": "WebDAV 地址",
|
||||
"webdav.host.placeholder": "http://localhost:8080",
|
||||
"webdav.password": "WebDAV 密码",
|
||||
"webdav.path": "WebDAV 路径",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.restore.button": "从 WebDAV 恢复",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 用户名"
|
||||
},
|
||||
"display.title": "显示设置",
|
||||
"font_size.title": "消息字体大小",
|
||||
"general": "常规设置",
|
||||
"general.backup.button": "备份",
|
||||
"general.backup.title": "数据备份与恢复",
|
||||
"general.manually_check_update.title": "关闭更新检测",
|
||||
"general.reset.button": "重置",
|
||||
"general.reset.title": "重置数据",
|
||||
"general.restore.button": "恢复",
|
||||
"general.title": "常规设置",
|
||||
"general.user_name": "用户名",
|
||||
"general.user_name.placeholder": "请输入用户名",
|
||||
"general.backup.title": "数据备份与恢复",
|
||||
"general.backup.button": "备份",
|
||||
"general.restore.button": "恢复",
|
||||
"general.reset.title": "重置数据",
|
||||
"general.reset.button": "重置",
|
||||
"general.view_webdav_settings": "查看 WebDAV 设置",
|
||||
"general.manually_check_update.title": "关闭更新检测",
|
||||
"data.webdav.title": "WebDAV",
|
||||
"data.webdav.host": "WebDAV 地址",
|
||||
"data.webdav.host.placeholder": "http://localhost:8080",
|
||||
"data.webdav.user": "WebDAV 用户名",
|
||||
"data.webdav.password": "WebDAV 密码",
|
||||
"data.webdav.path": "WebDAV 路径",
|
||||
"data.webdav.path.placeholder": "/backup",
|
||||
"data.webdav.backup.button": "备份到 WebDAV",
|
||||
"data.webdav.restore.button": "从 WebDAV 恢复",
|
||||
"advanced.title": "高级设置",
|
||||
"advanced.click_assistant_switch_to_topics": "点击助手切换到话题",
|
||||
"provider.api_key": "API 密钥",
|
||||
"provider.check": "检查",
|
||||
"provider.get_api_key": "点击这里获取密钥",
|
||||
"provider.api_host": "API 地址",
|
||||
"provider.api_version": "API 版本",
|
||||
"provider.docs_check": "查看",
|
||||
"provider.docs_more_details": "获取更多详情",
|
||||
"provider.search_placeholder": "搜索模型 ID 或名称",
|
||||
"provider.api.url.reset": "重置",
|
||||
"provider.api.url.preview": "预览: {{url}}",
|
||||
"provider.api.url.tip": "/结尾忽略v1版本,#结尾制使用输入地址",
|
||||
"models.default_assistant_model": "默认助手模型",
|
||||
"models.topic_naming_model": "话题命名模型",
|
||||
"models.translate_model": "翻译模型",
|
||||
"input.auto_translate_with_space": "快速敲击3次空格翻译",
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.input.paste_long_text_as_file": "长文本粘贴为文件",
|
||||
"messages.input.send_shortcuts": "发送快捷键",
|
||||
"messages.input.show_estimated_tokens": "显示预估 Token 数",
|
||||
"messages.input.title": "输入设置",
|
||||
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||
"messages.math_engine": "数学公式引擎",
|
||||
"messages.model.title": "模型设置",
|
||||
"messages.title": "消息设置",
|
||||
"messages.use_serif_font": "使用衬线字体",
|
||||
"model": "默认模型",
|
||||
"models.add.add_model": "添加模型",
|
||||
"models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo",
|
||||
"models.add.group_name": "分组名称",
|
||||
"models.add.group_name.placeholder": "例如 ChatGPT",
|
||||
"models.add.group_name.tooltip": "例如 ChatGPT",
|
||||
"models.add.model_id": "模型 ID",
|
||||
"models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo",
|
||||
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
|
||||
"models.add.model_name": "模型名称",
|
||||
"models.add.model_name.placeholder": "例如 GPT-3.5",
|
||||
"models.add.group_name": "分组名称",
|
||||
"models.add.group_name.tooltip": "例如 ChatGPT",
|
||||
"models.add.group_name.placeholder": "例如 ChatGPT",
|
||||
"models.default_assistant_model": "默认助手模型",
|
||||
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
|
||||
"models.empty": "没有模型",
|
||||
"assistant.title": "默认助手",
|
||||
"assistant.model_params": "模型参数",
|
||||
"about.description": "一款为创造者而生的 AI 助手",
|
||||
"about.updateNotAvailable": "你的软件已是最新版本",
|
||||
"about.checkingUpdate": "正在检查更新...",
|
||||
"about.updateError": "更新出错",
|
||||
"about.checkUpdate": "检查更新",
|
||||
"about.downloading": "正在下载更新...",
|
||||
"provider.delete.title": "删除提供商",
|
||||
"provider.delete.content": "确定要删除此模型提供商吗?",
|
||||
"provider.edit.name": "模型提供商名称",
|
||||
"provider.edit.name.placeholder": "例如 OpenAI",
|
||||
"about.title": "关于我们",
|
||||
"about.releases.title": "更新日志",
|
||||
"about.releases.button": "查看",
|
||||
"about.website.title": "官方网站",
|
||||
"about.website.button": "查看",
|
||||
"about.feedback.title": "意见反馈",
|
||||
"about.feedback.button": "反馈",
|
||||
"about.contact.title": "邮件联系",
|
||||
"about.license.title": "许可证",
|
||||
"about.license.button": "查看",
|
||||
"about.contact.button": "邮件",
|
||||
"models.topic_naming_model": "话题命名模型",
|
||||
"models.topic_naming_model_description": "自动命名新话题时使用的模型",
|
||||
"models.translate_model": "翻译模型",
|
||||
"models.translate_model_description": "翻译服务使用的模型",
|
||||
"models.translate_model_prompt_message": "请输入翻译模型提示词",
|
||||
"models.translate_model_prompt_title": "翻译模型提示词",
|
||||
"models.topic_naming_model_setting_title": "话题命名模型设置",
|
||||
"models.enable_topic_naming": "话题自动重命名",
|
||||
"provider": {
|
||||
"add.name": "提供商名称",
|
||||
"add.name.placeholder": "例如 OpenAI",
|
||||
"add.title": "添加提供商",
|
||||
"add.type": "提供商类型",
|
||||
"api.url.preview": "预览: {{url}}",
|
||||
"api.url.reset": "重置",
|
||||
"api.url.tip": "/结尾忽略v1版本,#结尾制使用输入地址",
|
||||
"api_host": "API 地址",
|
||||
"api_key": "API 密钥",
|
||||
"api_key.tip": "多个密钥使用逗号分隔",
|
||||
"api_version": "API 版本",
|
||||
"check": "检查",
|
||||
"check_all_keys": "检查所有密钥",
|
||||
"check_multiple_keys": "检查多个 API 密钥",
|
||||
"delete.content": "确定要删除此模型提供商吗?",
|
||||
"delete.title": "删除提供商",
|
||||
"docs_check": "查看",
|
||||
"docs_more_details": "获取更多详情",
|
||||
"get_api_key": "点击这里获取密钥",
|
||||
"no_models": "请先添加模型再检查 API 连接",
|
||||
"not_checked": "未检查",
|
||||
"remove_duplicate_keys": "移除重复密钥",
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
"search_placeholder": "搜索模型 ID 或名称",
|
||||
"title": "模型服务"
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
"custom": "自定义代理",
|
||||
"none": "不使用代理",
|
||||
"system": "系统代理",
|
||||
"title": "代理模式"
|
||||
},
|
||||
"title": "代理设置"
|
||||
},
|
||||
"proxy.title": "代理地址",
|
||||
"theme.title": "主题",
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
"key": "按键",
|
||||
"new_topic": "新建话题",
|
||||
"title": "快捷方式",
|
||||
"zoom_in": "放大界面",
|
||||
"zoom_out": "缩小界面",
|
||||
"zoom_reset": "重置缩放",
|
||||
"show_app": "显示应用",
|
||||
"reset_defaults": "重置默认快捷键",
|
||||
"reset_defaults_confirm": "确定要重置所有快捷键吗?",
|
||||
"press_shortcut": "按下快捷键",
|
||||
"alt_warning": "Mac 系统不能使用 Option + 字母作为快捷键",
|
||||
"reset_to_default": "重置为默认",
|
||||
"clear_shortcut": "清除快捷键"
|
||||
},
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.dark": "深色主题",
|
||||
"theme.light": "浅色主题",
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.title": "主题",
|
||||
"theme.window.style.opaque": "不透明窗口",
|
||||
"theme.window.style.title": "窗口样式",
|
||||
"theme.window.style.transparent": "透明窗口",
|
||||
"theme.window.style.opaque": "不透明窗口",
|
||||
"font_size.title": "消息字体大小",
|
||||
"title": "设置",
|
||||
"topic.position": "话题位置",
|
||||
"topic.position.left": "左侧",
|
||||
"topic.position.right": "右侧",
|
||||
"topic.show.time": "显示话题时间",
|
||||
"shortcuts": {
|
||||
"title": "快捷方式",
|
||||
"action": "操作",
|
||||
"key": "按键",
|
||||
"new_topic": "新建话题",
|
||||
"zoom_in": "放大界面",
|
||||
"zoom_out": "缩小界面",
|
||||
"zoom_reset": "重置缩放"
|
||||
}
|
||||
"tray.title": "启用系统托盘图标"
|
||||
},
|
||||
"translate": {
|
||||
"title": "翻译",
|
||||
"any.language": "任意语言",
|
||||
"button.translate": "翻译",
|
||||
"confirm": {
|
||||
"content": "翻译后将覆盖原文,是否继续?",
|
||||
"title": "翻译确认"
|
||||
},
|
||||
"error.not_configured": "翻译模型未配置",
|
||||
"error.failed": "翻译失败",
|
||||
"input.placeholder": "输入文本进行翻译",
|
||||
"output.placeholder": "翻译",
|
||||
"confirm": "原文已复制到剪贴板,是否用翻译后的文本替换?"
|
||||
"processing": "翻译中...",
|
||||
"title": "翻译",
|
||||
"close": "关闭"
|
||||
},
|
||||
"languages": {
|
||||
"english": "英文",
|
||||
"chinese": "简体中文",
|
||||
"chinese-traditional": "繁体中文",
|
||||
"japanese": "日文",
|
||||
"korean": "韩文",
|
||||
"russian": "俄文",
|
||||
"spanish": "西班牙文",
|
||||
"french": "法文",
|
||||
"italian": "意大利文",
|
||||
"portuguese": "葡萄牙文",
|
||||
"arabic": "阿拉伯文"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"keep_alive_time.title": "保持活跃时间",
|
||||
"keep_alive_time.placeholder": "分钟",
|
||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)"
|
||||
},
|
||||
"error": {
|
||||
"chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥",
|
||||
"backup.file_format": "备份文件格式错误",
|
||||
"provider_disabled": "模型提供商未启用",
|
||||
"no_api_key": "API 密钥未配置"
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
"show_window": "显示窗口"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知识图谱",
|
||||
"visualization": "可视化"
|
||||
},
|
||||
"export": {
|
||||
"attached_files": "附件",
|
||||
"user": "用户",
|
||||
"assistant": "助手",
|
||||
"created": "创建时间",
|
||||
"last_updated": "最后更新",
|
||||
"messages": "消息数",
|
||||
"conversation_details": "会话详情",
|
||||
"conversation_history": "会话历史"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,407 +1,499 @@
|
||||
{
|
||||
"translation": {
|
||||
"common": {
|
||||
"avatar": "頭像",
|
||||
"language": "語言",
|
||||
"model": "模型",
|
||||
"models": "模型",
|
||||
"topics": "話題",
|
||||
"docs": "文件",
|
||||
"and": "與",
|
||||
"assistant": "智能體",
|
||||
"name": "名稱",
|
||||
"description": "描述",
|
||||
"prompt": "提示詞",
|
||||
"rename": "重新命名",
|
||||
"delete": "刪除",
|
||||
"edit": "編輯",
|
||||
"duplicate": "複製",
|
||||
"copy": "複製",
|
||||
"regenerate": "重新生成",
|
||||
"provider": "提供商",
|
||||
"you": "您",
|
||||
"save": "保存",
|
||||
"footnotes": "引用",
|
||||
"select": "選擇",
|
||||
"search": "搜尋",
|
||||
"default": "預設",
|
||||
"warning": "警告",
|
||||
"back": "返回",
|
||||
"chat": "聊天",
|
||||
"close": "關閉",
|
||||
"cancel": "取消",
|
||||
"download": "下載"
|
||||
},
|
||||
"button": {
|
||||
"add": "添加",
|
||||
"added": "已添加",
|
||||
"manage": "管理",
|
||||
"select_model": "選擇模型",
|
||||
"show.all": "顯示全部",
|
||||
"collapse": "收起"
|
||||
},
|
||||
"message": {
|
||||
"copied": "已複製",
|
||||
"assistant.added.content": "智能體添加成功",
|
||||
"message.delete.title": "刪除訊息",
|
||||
"message.delete.content": "確定要刪除此訊息嗎?",
|
||||
"error.enter.api.key": "請先輸入您的 API 密鑰",
|
||||
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
||||
"error.enter.model": "請先選擇一個模型",
|
||||
"error.invalid.proxy.url": "無效的代理 URL",
|
||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||
"api.connection.failed": "連接失敗",
|
||||
"api.connection.success": "連接成功",
|
||||
"chat.completion.paused": "聊天完成已暫停",
|
||||
"switch.disabled": "助手生成回覆時無法切換",
|
||||
"restore.success": "恢復成功",
|
||||
"backup.success": "備份成功",
|
||||
"backup.failed": "備份失敗",
|
||||
"reset.confirm.content": "確定要清除所有資料嗎?",
|
||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
|
||||
"upgrade.success.title": "升級成功",
|
||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"topic.added": "新話題已添加",
|
||||
"save.success.title": "保存成功",
|
||||
"message.style": "消息樣式",
|
||||
"message.style.bubble": "氣泡",
|
||||
"message.style.plain": "簡潔"
|
||||
},
|
||||
"chat": {
|
||||
"save": "保存",
|
||||
"default.name": "⭐️ 預設助手",
|
||||
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
||||
"default.topic.name": "預設話題",
|
||||
"topics.title": "話題",
|
||||
"topics.auto_rename": "自動重新命名",
|
||||
"topics.edit.title": "編輯名稱",
|
||||
"topics.edit.placeholder": "輸入新名稱",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.move_to": "移動到",
|
||||
"topics.list": "話題列表",
|
||||
"topics.export.title": "匯出",
|
||||
"topics.export.image": "匯出為圖片",
|
||||
"topics.export.md": "匯出為 Markdown",
|
||||
"topics.export.word": "導出為 Word",
|
||||
"input.new_topic": "新話題",
|
||||
"input.topics": " 話題 ",
|
||||
"input.clear": "清除",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.expand": "展開",
|
||||
"input.collapse": "收起",
|
||||
"input.clear.title": "清除所有訊息?",
|
||||
"input.clear.content": "您想要清除當前話題的所有訊息嗎?",
|
||||
"input.placeholder": "在此輸入您的訊息...",
|
||||
"input.send": "發送",
|
||||
"input.pause": "暫停",
|
||||
"input.settings": "設定",
|
||||
"input.upload": "上傳圖片或文檔",
|
||||
"input.context_count.tip": "上下文數量",
|
||||
"input.estimated_tokens.tip": "預估 Token 數",
|
||||
"settings.temperature": "溫度",
|
||||
"settings.temperature.tip": "較低的值使模型更具創造性和不可預測性,較高的值則使其更具確定性和精確性。",
|
||||
"settings.conext_count": "上下文",
|
||||
"settings.conext_count.tip": "在上下文中保留的前幾則訊息。",
|
||||
"settings.max_tokens": "啟用最大 Token 限制",
|
||||
"settings.max_tokens.tip": "模型可以生成的最大 Token 數。普通聊天建議 500-800。短文生成建議 800-2000。代碼生成建議 2000-3600。長文生成建議超過 4000。",
|
||||
"settings.reset": "重置",
|
||||
"settings.set_as_default": "設為預設助手",
|
||||
"settings.max": "最大",
|
||||
"settings.show_line_numbers": "代码顯示行號",
|
||||
"suggestions.title": "建議的問題",
|
||||
"add.assistant.title": "添加助手",
|
||||
"message.new.context": "新上下文",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已建立",
|
||||
"assistant.search.placeholder": "搜尋",
|
||||
"artifacts.button.preview": "預覽",
|
||||
"artifacts.button.download": "下載"
|
||||
},
|
||||
"assistants": {
|
||||
"title": "助手",
|
||||
"abbr": "助",
|
||||
"search": "搜尋助手...",
|
||||
"settings.prompt": "提示詞設定",
|
||||
"settings.model": "模型設定",
|
||||
"settings.preset_messages": "預設訊息",
|
||||
"settings.default_model": "預設模型",
|
||||
"settings.auto_reset_model": "自動重置模型",
|
||||
"settings.auto_reset_model.tip": "每次新的話題時自動重置模型",
|
||||
"edit.title": "編輯助手",
|
||||
"copy.title": "複製助手",
|
||||
"clear.title": "清空話題",
|
||||
"clear.content": "清空話題會刪除助手下所有主題和文件,確定要繼續嗎?",
|
||||
"save.title": "儲存到智能體",
|
||||
"save.success": "儲存成功",
|
||||
"delete.title": "删除助手",
|
||||
"delete.content": "删除助手会删除所有该助手下的话题和文件,确定要繼續吗?"
|
||||
},
|
||||
"model": {
|
||||
"stream_output": "串流輸出",
|
||||
"search": "搜尋模型..."
|
||||
},
|
||||
"images": {
|
||||
"title": "繪圖",
|
||||
"image.size": "影像尺寸",
|
||||
"button.new.image": "新繪圖",
|
||||
"button.delete.image": "刪除繪圖",
|
||||
"button.delete.image.confirm": "確定要刪除此繪圖嗎?",
|
||||
"number_images": "生成數量",
|
||||
"number_images_tip": "一次生成的圖片數量 (1-4)",
|
||||
"seed": "隨機種子",
|
||||
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||
"inference_steps": "推理步數",
|
||||
"inference_steps_tip": "要執行的推理步數。步數越多,質量越高但耗時越長",
|
||||
"guidance_scale": "引導比例",
|
||||
"guidance_scale_tip": "無分類器指導。控制模型在尋找相關圖像時對提示詞的遵循程度",
|
||||
"negative_prompt": "反向提示詞",
|
||||
"negative_prompt_tip": "描述你不想在圖片中出現的內容",
|
||||
"prompt_placeholder": "描述你想創建的圖片,例如:一個寧靜的湖泊,夕陽西下,遠處是群山",
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?"
|
||||
},
|
||||
"files": {
|
||||
"title": "檔案",
|
||||
"file": "檔案",
|
||||
"name": "名稱",
|
||||
"size": "大小",
|
||||
"count": "數量",
|
||||
"created_at": "建立時間",
|
||||
"image": "圖片",
|
||||
"text": "文本",
|
||||
"document": "文檔",
|
||||
"actions": "操作",
|
||||
"open": "打開",
|
||||
"all": "所有檔案"
|
||||
},
|
||||
"agents": {
|
||||
"title": "智能體",
|
||||
"my_agents": "我的智能體",
|
||||
"add.title": "创建智能體",
|
||||
"edit.title": "編輯智能體",
|
||||
"add.button": "添加到助手",
|
||||
"add.name": "名稱",
|
||||
"add.name.placeholder": "輸入名稱",
|
||||
"add.prompt": "提示詞",
|
||||
"add.prompt.placeholder": "輸入提示詞",
|
||||
"add.button": "添加到助手",
|
||||
"manage.title": "管理智能體",
|
||||
"add.title": "创建智能體",
|
||||
"delete.popup.content": "確定要刪除此智能體嗎?",
|
||||
"tag.default": "預設",
|
||||
"tag.system": "系統",
|
||||
"tag.agent": "智能体",
|
||||
"edit.message.title": "預設訊息",
|
||||
"edit.message.add.title": "添加",
|
||||
"edit.message.group.title": "訊息組",
|
||||
"edit.message.assistant.title": "助手",
|
||||
"edit.message.assistant.placeholder": "輸入助手消息",
|
||||
"edit.message.user.title": "用戶",
|
||||
"edit.message.user.placeholder": "輸入用戶消息",
|
||||
"edit.message.assistant.title": "助手",
|
||||
"edit.message.empty.content": "會話輸入內容不能為空",
|
||||
"edit.message.group.title": "訊息組",
|
||||
"edit.message.title": "預設訊息",
|
||||
"edit.message.user.placeholder": "輸入用戶消息",
|
||||
"edit.message.user.title": "用戶",
|
||||
"edit.model.select.title": "選擇模型",
|
||||
"edit.settings.hide_preset_messages": "隱藏預設消息",
|
||||
"edit.title": "編輯智能體",
|
||||
"manage.title": "管理智能體",
|
||||
"my_agents": "我的智能體",
|
||||
"search.no_results": "沒有找到相關智能體",
|
||||
"sorting.title": "排序"
|
||||
"sorting.title": "排序",
|
||||
"tag.agent": "智能体",
|
||||
"tag.default": "預設",
|
||||
"tag.new": "新建",
|
||||
"tag.system": "系統",
|
||||
"title": "智能體"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助",
|
||||
"clear.content": "清空話題會刪除助手下所有主題和文件,確定要繼續嗎?",
|
||||
"clear.title": "清空話題",
|
||||
"copy.title": "複製助手",
|
||||
"delete.content": "删除助手会删除所有该助手下的话题和文件,确定要繼續吗?",
|
||||
"delete.title": "删除助手",
|
||||
"edit.title": "編輯助手",
|
||||
"save.success": "儲存成功",
|
||||
"save.title": "儲存到智能體",
|
||||
"search": "搜尋助手...",
|
||||
"settings.auto_reset_model": "自動重置模型",
|
||||
"settings.auto_reset_model.tip": "每次新的話題時自動重置模型",
|
||||
"settings.default_model": "預設模型",
|
||||
"settings.model": "模型設定",
|
||||
"settings.preset_messages": "預設訊息",
|
||||
"settings.prompt": "提示詞設定",
|
||||
"title": "助手"
|
||||
},
|
||||
"button": {
|
||||
"add": "添加",
|
||||
"added": "已添加",
|
||||
"collapse": "收起",
|
||||
"manage": "管理",
|
||||
"select_model": "選擇模型",
|
||||
"show.all": "顯示全部"
|
||||
},
|
||||
"chat": {
|
||||
"add.assistant.title": "添加助手",
|
||||
"artifacts.button.download": "下載",
|
||||
"artifacts.button.preview": "預覽",
|
||||
"assistant.search.placeholder": "搜尋",
|
||||
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
||||
"default.name": "⭐️ 預設助手",
|
||||
"default.topic.name": "預設話題",
|
||||
"input.clear": "清除",
|
||||
"input.clear.content": "您想要清除當前話題的所有訊息嗎?",
|
||||
"input.clear.title": "清除所有訊息?",
|
||||
"input.collapse": "收起",
|
||||
"input.context_count.tip": "上下文數量",
|
||||
"input.estimated_tokens.tip": "預估 Token 數",
|
||||
"input.expand": "展開",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.new_topic": "新話題 {{Command}}+N",
|
||||
"input.pause": "暫停",
|
||||
"input.placeholder": "在此輸入您的訊息...",
|
||||
"input.send": "發送",
|
||||
"input.settings": "設定",
|
||||
"input.topics": " 話題 ",
|
||||
"input.translate": "翻譯成英文",
|
||||
"input.upload": "上傳圖片或文檔",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已建立",
|
||||
"message.new.context": "新上下文",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "代码块可折叠",
|
||||
"settings.context_count": "上下文",
|
||||
"settings.context_count.tip": "在上下文中保留的前幾則訊息。",
|
||||
"settings.max": "最大",
|
||||
"settings.max_tokens": "啟用最大 Token 限制",
|
||||
"settings.max_tokens.tip": "模型可以生成的最大 Token 數。普通聊天建議 500-800。短文生成建議 800-2000。代碼生成建議 2000-3600。長文生成建議超過 4000。",
|
||||
"settings.reset": "重置",
|
||||
"settings.set_as_default": "設為預設助手",
|
||||
"settings.show_line_numbers": "代码顯示行號",
|
||||
"settings.temperature": "溫度",
|
||||
"settings.temperature.tip": "較低的值使模型更具創造性和不可預測性,較高的值則使其更具確定性和精確性。",
|
||||
"suggestions.title": "建議的問題",
|
||||
"topics.auto_rename": "自動重新命名",
|
||||
"topics.clear.title": "清空消息",
|
||||
"topics.edit.placeholder": "輸入新名稱",
|
||||
"topics.edit.title": "編輯名稱",
|
||||
"topics.export.image": "匯出為圖片",
|
||||
"topics.export.md": "匯出為 Markdown",
|
||||
"topics.export.title": "匯出",
|
||||
"topics.export.word": "導出為 Word",
|
||||
"topics.list": "話題列表",
|
||||
"topics.move_to": "移動到",
|
||||
"topics.title": "話題",
|
||||
"translate": "翻譯"
|
||||
},
|
||||
"common": {
|
||||
"and": "與",
|
||||
"assistant": "智能體",
|
||||
"avatar": "頭像",
|
||||
"back": "返回",
|
||||
"cancel": "取消",
|
||||
"chat": "聊天",
|
||||
"close": "關閉",
|
||||
"copy": "複製",
|
||||
"cut": "剪下",
|
||||
"default": "預設",
|
||||
"delete": "刪除",
|
||||
"description": "描述",
|
||||
"docs": "文件",
|
||||
"download": "下載",
|
||||
"duplicate": "複製",
|
||||
"edit": "編輯",
|
||||
"footnotes": "引用",
|
||||
"language": "語言",
|
||||
"model": "模型",
|
||||
"models": "模型",
|
||||
"name": "名稱",
|
||||
"paste": "貼上",
|
||||
"prompt": "提示詞",
|
||||
"provider": "提供商",
|
||||
"regenerate": "重新生成",
|
||||
"rename": "重新命名",
|
||||
"reset": "重置",
|
||||
"save": "保存",
|
||||
"search": "搜尋",
|
||||
"select": "選擇",
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
"you": "您",
|
||||
"clear": "清除",
|
||||
"add": "添加"
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "備份文件格式錯誤",
|
||||
"chat.response": "出現錯誤。如果尚未配置 API 密鑰,請前往設定 > 模型提供者中配置密鑰",
|
||||
"no_api_key": "API 密鑰未配置",
|
||||
"provider_disabled": "模型提供商未啟用",
|
||||
"render": {
|
||||
"title": "渲染錯誤",
|
||||
"description": "渲染公式失敗,請檢查公式格式是否正確"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"assistant": "助手",
|
||||
"attached_files": "附件",
|
||||
"conversation_details": "會話詳情",
|
||||
"conversation_history": "會話歷史",
|
||||
"created": "創建時間",
|
||||
"last_updated": "最後<E69C80><E5BE8C>新",
|
||||
"messages": "訊息數",
|
||||
"user": "用戶"
|
||||
},
|
||||
"files": {
|
||||
"actions": "操作",
|
||||
"all": "所有檔案",
|
||||
"count": "數量",
|
||||
"created_at": "建立時間",
|
||||
"document": "文檔",
|
||||
"file": "檔案",
|
||||
"image": "圖片",
|
||||
"name": "名稱",
|
||||
"open": "打開",
|
||||
"size": "大小",
|
||||
"text": "文本",
|
||||
"title": "檔案"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "繼續聊天",
|
||||
"locate.message": "定位到訊息",
|
||||
"search.messages": "搜尋所有訊息",
|
||||
"search.placeholder": "搜尋話題或訊息...",
|
||||
"search.topics.empty": "沒有找到相關話題, 點擊回車鍵搜尋所有訊息",
|
||||
"title": "搜尋話題"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "阿拉伯文",
|
||||
"chinese": "簡體中文",
|
||||
"chinese-traditional": "繁體中文",
|
||||
"english": "英文",
|
||||
"french": "法文",
|
||||
"italian": "意大利文",
|
||||
"japanese": "日文",
|
||||
"korean": "韓文",
|
||||
"portuguese": "葡萄牙文",
|
||||
"russian": "俄文",
|
||||
"spanish": "西班牙文"
|
||||
},
|
||||
"mermaid": {
|
||||
"download": {
|
||||
"png": "下載 PNG",
|
||||
"svg": "下載 SVG"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "預覽",
|
||||
"source": "原始碼"
|
||||
},
|
||||
"title": "Mermaid 圖表"
|
||||
},
|
||||
"message": {
|
||||
"api.connection.failed": "連接失敗",
|
||||
"api.connection.success": "連接成功",
|
||||
"assistant.added.content": "智能體添加成功",
|
||||
"backup.failed": "備份失敗",
|
||||
"backup.success": "備份成功",
|
||||
"chat.completion.paused": "聊天完成已暫停",
|
||||
"copied": "已複製",
|
||||
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
||||
"error.enter.api.key": "請先輸入您的 API 密鑰",
|
||||
"error.enter.model": "請先選擇一個模型",
|
||||
"error.invalid.proxy.url": "無效的代理 URL",
|
||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||
"message.code_style": "程式碼風格",
|
||||
"message.delete.content": "確定要刪除此訊息嗎?",
|
||||
"message.delete.title": "刪除訊息",
|
||||
"message.style": "消息樣式",
|
||||
"message.style.bubble": "氣泡",
|
||||
"message.style.plain": "簡潔",
|
||||
"reset.confirm.content": "確定要清除所有資料嗎?",
|
||||
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
|
||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||
"restore.success": "恢復成功",
|
||||
"save.success.title": "保存成功",
|
||||
"switch.disabled": "助手生成回覆時無法切換",
|
||||
"topic.added": "新話題已添加",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||
"upgrade.success.title": "升級成功",
|
||||
"regenerate.confirm": "重新生成會覆蓋當前訊息"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序"
|
||||
},
|
||||
"history": {
|
||||
"title": "搜尋話題",
|
||||
"search.placeholder": "搜尋話題或訊息...",
|
||||
"continue_chat": "繼續聊天",
|
||||
"search.topics.empty": "沒有找到相關話題, 點擊回車鍵搜尋所有訊息",
|
||||
"search.messages": "搜尋所有訊息",
|
||||
"locate.message": "定位到訊息"
|
||||
"model": {
|
||||
"pinned": "已固定",
|
||||
"search": "搜尋模型...",
|
||||
"stream_output": "串流輸出",
|
||||
"type": {
|
||||
"select": "選擇模型類型",
|
||||
"text": "文字",
|
||||
"vision": "圖像"
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||
"keep_alive_time.placeholder": "分鐘",
|
||||
"keep_alive_time.title": "保持活躍時間",
|
||||
"title": "Ollama"
|
||||
},
|
||||
"paintings": {
|
||||
"button.delete.image": "刪除繪圖",
|
||||
"button.delete.image.confirm": "確定要刪除此繪圖嗎?",
|
||||
"button.new.image": "新繪圖",
|
||||
"guidance_scale": "引導比例",
|
||||
"guidance_scale_tip": "無分類器指導。控制模型在尋找相關圖像時對提示詞的遵循程度",
|
||||
"image.size": "影像尺寸",
|
||||
"inference_steps": "推理步數",
|
||||
"inference_steps_tip": "要執行的推理步數。步數越多,質量越高但耗時越長",
|
||||
"negative_prompt": "反向提示詞",
|
||||
"negative_prompt_tip": "描述你不想在圖片中出現的內容",
|
||||
"number_images": "生成數量",
|
||||
"number_images_tip": "一次生成的圖片數量 (1-4)",
|
||||
"prompt_placeholder": "描述你想創建的圖片,例如:一個寧靜的湖泊,夕陽西下,遠處是群山",
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
|
||||
"seed": "隨機種子",
|
||||
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||
"title": "繪圖"
|
||||
},
|
||||
"provider": {
|
||||
"jina": "Jina",
|
||||
"mistral": "Mistral",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"grok": "Grok",
|
||||
"nvidia": "輝達",
|
||||
"zhinao": "360智腦",
|
||||
"hunyuan": "騰訊混元",
|
||||
"fireworks": "Fireworks",
|
||||
"together": "Together",
|
||||
"openai": "OpenAI",
|
||||
"gemini": "Gemini",
|
||||
"deepseek": "深度求索",
|
||||
"moonshot": "月之暗面",
|
||||
"silicon": "SiliconFlow",
|
||||
"openrouter": "OpenRouter",
|
||||
"yi": "零一萬物",
|
||||
"zhipu": "智譜AI",
|
||||
"groq": "Groq",
|
||||
"ollama": "Ollama",
|
||||
"aihubmix": "AiHubMix",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"dashscope": "阿里雲百鍊",
|
||||
"anthropic": "Anthropic",
|
||||
"aihubmix": "AiHubMix",
|
||||
"stepfun": "StepFun",
|
||||
"deepseek": "深度求索",
|
||||
"doubao": "豆包",
|
||||
"minimax": "MiniMax",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"fireworks": "Fireworks",
|
||||
"gemini": "Gemini",
|
||||
"github": "GitHub Models",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"hunyuan": "騰訊混元",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"jina": "Jina",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"moonshot": "月之暗面",
|
||||
"nvidia": "輝達",
|
||||
"ocoolai": "ocoolAI",
|
||||
"azure-openai": "Azure OpenAI"
|
||||
"ollama": "Ollama",
|
||||
"openai": "OpenAI",
|
||||
"openrouter": "OpenRouter",
|
||||
"silicon": "SiliconFlow",
|
||||
"stepfun": "StepFun",
|
||||
"together": "Together",
|
||||
"yi": "零一萬物",
|
||||
"zhinao": "360智腦",
|
||||
"zhipu": "智譜AI"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"general": "一般設定",
|
||||
"data": "數據設定",
|
||||
"provider": "模型提供者",
|
||||
"model": "預設模型",
|
||||
"assistant": "預設助手",
|
||||
"about": "關於與回饋",
|
||||
"messages.model.title": "模型設定",
|
||||
"messages.title": "訊息設定",
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.use_serif_font": "使用襯線字體",
|
||||
"messages.input.title": "輸入設定",
|
||||
"messages.input.show_estimated_tokens": "顯示預估輸入 Token 數",
|
||||
"messages.input.send_shortcuts": "發送快捷鍵",
|
||||
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
|
||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||
"messages.math_render_engine": "數學公式引擎",
|
||||
"about.checkUpdate": "檢查更新",
|
||||
"about.checkingUpdate": "正在檢查更新...",
|
||||
"about.contact.button": "郵件",
|
||||
"about.contact.title": "聯繫方式",
|
||||
"about.description": "一款為創作者而生的強大 AI 助手",
|
||||
"about.downloading": "正在下載...",
|
||||
"about.feedback.button": "回饋",
|
||||
"about.feedback.title": "回饋",
|
||||
"about.license.button": "查看",
|
||||
"about.license.title": "許可證",
|
||||
"about.releases.button": "查看",
|
||||
"about.releases.title": "更新日誌",
|
||||
"about.title": "關於我們",
|
||||
"about.updateAvailable": "發現新版本 {{version}}",
|
||||
"about.updateError": "更新錯誤",
|
||||
"about.updateNotAvailable": "您正在使用最新版本",
|
||||
"about.website.button": "網站",
|
||||
"about.website.title": "官方網站",
|
||||
"advanced.auto_switch_to_topics": "自動切換到話題",
|
||||
"advanced.title": "進階設定",
|
||||
"assistant": "預設助手",
|
||||
"assistant.model_params": "模型參數",
|
||||
"assistant.title": "預設助手",
|
||||
"data": {
|
||||
"clear_cache": {
|
||||
"button": "清除緩存",
|
||||
"confirm": "清除緩存將刪除應用緩存數據,包括小程序數據。此操作不可恢復,是否繼續?",
|
||||
"error": "清除緩存失敗",
|
||||
"success": "緩存清除成功",
|
||||
"title": "清除緩存"
|
||||
},
|
||||
"data.app_data": "應用數據",
|
||||
"data.app_logs": "應用日誌",
|
||||
"data.title": "數據目錄",
|
||||
"title": "數據設定",
|
||||
"webdav.backup.button": "從 WebDAV 備份",
|
||||
"webdav.host": "WebDAV 主機位址",
|
||||
"webdav.host.placeholder": "http://localhost:8080",
|
||||
"webdav.password": "WebDAV 密碼",
|
||||
"webdav.path": "WebDAV Path",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.restore.button": "從 WebDAV 恢復",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 使用者名稱"
|
||||
},
|
||||
"display.title": "顯示設定",
|
||||
"font_size.title": "訊息字體大小",
|
||||
"general": "一般設定",
|
||||
"general.backup.button": "備份",
|
||||
"general.backup.title": "資料備份與復原",
|
||||
"general.manually_check_update.title": "關閉更新檢查",
|
||||
"general.reset.button": "重置",
|
||||
"general.reset.title": "資料重置",
|
||||
"general.restore.button": "復原",
|
||||
"general.title": "一般設定",
|
||||
"general.user_name": "使用者名稱",
|
||||
"general.user_name.placeholder": "輸入您的名稱",
|
||||
"general.backup.title": "資料備份與復原",
|
||||
"general.backup.button": "備份",
|
||||
"general.restore.button": "復原",
|
||||
"general.view_webdav_settings": "查看 WebDAV 設定",
|
||||
"general.reset.title": "資料重置",
|
||||
"general.reset.button": "重置",
|
||||
"general.manually_check_update.title": "關閉更新檢查",
|
||||
"data.webdav.title": "WebDAV",
|
||||
"data.webdav.host": "WebDAV 主機位址",
|
||||
"data.webdav.host.placeholder": "http://localhost:8080",
|
||||
"data.webdav.user": "WebDAV 使用者名稱",
|
||||
"data.webdav.password": "WebDAV 密碼",
|
||||
"data.webdav.path": "WebDAV Path",
|
||||
"data.webdav.path.placeholder": "/backup",
|
||||
"data.webdav.backup.button": "從 WebDAV 備份",
|
||||
"data.webdav.restore.button": "從 WebDAV 恢復",
|
||||
"advanced.title": "進階設定",
|
||||
"advanced.click_assistant_switch_to_topics": "點擊助手切換到話題",
|
||||
"provider.api_key": "API 密鑰",
|
||||
"provider.check": "檢查",
|
||||
"provider.get_api_key": "獲取 API 密鑰",
|
||||
"provider.api_host": "API 主機地址",
|
||||
"provider.api_version": "API 版本",
|
||||
"provider.docs_check": "檢查",
|
||||
"provider.docs_more_details": "查看更多細節",
|
||||
"provider.search_placeholder": "搜尋模型 ID 或名稱",
|
||||
"provider.api.url.reset": "重置",
|
||||
"provider.api.url.preview": "預覽: {{url}}",
|
||||
"provider.api.url.tip": "/結尾忽略v1版本,#結尾強制使用輸入位址",
|
||||
"models.default_assistant_model": "預設助手模型",
|
||||
"models.topic_naming_model": "話題命名模型",
|
||||
"models.translate_model": "翻譯模型",
|
||||
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
|
||||
"messages.input.send_shortcuts": "發送快捷鍵",
|
||||
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
|
||||
"messages.input.title": "輸入設定",
|
||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||
"messages.math_render_engine": "數學公式引擎",
|
||||
"messages.model.title": "模型設定",
|
||||
"messages.title": "訊息設定",
|
||||
"messages.use_serif_font": "使用襯線字體",
|
||||
"model": "預設模型",
|
||||
"models.add.add_model": "添加模型",
|
||||
"models.add.model_id.placeholder": "必填,例如 gpt-3.5-turbo",
|
||||
"models.add.group_name": "群組名稱",
|
||||
"models.add.group_name.placeholder": "可選,例如 ChatGPT",
|
||||
"models.add.group_name.tooltip": "可選,例如 ChatGPT",
|
||||
"models.add.model_id": "模型 ID",
|
||||
"models.add.model_id.placeholder": "必填,例如 gpt-3.5-turbo",
|
||||
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
|
||||
"models.add.model_name": "模型名稱",
|
||||
"models.add.model_name.placeholder": "可選,例如 GPT-4",
|
||||
"models.add.group_name": "群組名稱",
|
||||
"models.add.group_name.tooltip": "可選,例如 ChatGPT",
|
||||
"models.add.group_name.placeholder": "可選,例如 ChatGPT",
|
||||
"models.default_assistant_model": "預設助手模型",
|
||||
"models.default_assistant_model_description": "創建新助手時使用的模型,如果助手未設置模型,則使用此模型",
|
||||
"models.empty": "找不到模型",
|
||||
"assistant.title": "預設助手",
|
||||
"assistant.model_params": "模型參數",
|
||||
"about.description": "一款為創作者而生的強大 AI 助手",
|
||||
"about.updateNotAvailable": "您正在使用最新版本",
|
||||
"about.checkingUpdate": "正在檢查更新...",
|
||||
"about.updateError": "更新錯誤",
|
||||
"about.checkUpdate": "檢查更新",
|
||||
"about.downloading": "正在下載...",
|
||||
"provider.delete.title": "刪除提供者",
|
||||
"provider.delete.content": "確定要刪除此提供者嗎?",
|
||||
"provider.edit.name": "提供者名稱",
|
||||
"provider.edit.name.placeholder": "例如:OpenAI",
|
||||
"about.title": "關於我們",
|
||||
"about.releases.title": "更新日誌",
|
||||
"about.releases.button": "查看",
|
||||
"about.website.title": "官方網站",
|
||||
"about.website.button": "網站",
|
||||
"about.feedback.title": "回饋",
|
||||
"about.feedback.button": "回饋",
|
||||
"about.contact.title": "聯繫方式",
|
||||
"about.license.title": "許可證",
|
||||
"about.license.button": "查看",
|
||||
"about.contact.button": "郵件",
|
||||
"models.topic_naming_model": "話題命名模型",
|
||||
"models.topic_naming_model_description": "自動命名新話題時使用的模型",
|
||||
"models.translate_model": "翻譯模型",
|
||||
"models.translate_model_description": "翻譯服務使用的模型",
|
||||
"models.translate_model_prompt_message": "請輸入翻譯模型提示詞",
|
||||
"models.translate_model_prompt_title": "翻譯模型提示詞",
|
||||
"models.topic_naming_model_setting_title": "話題命名模型設定",
|
||||
"models.enable_topic_naming": "話題自動重命名",
|
||||
"provider": {
|
||||
"add.name": "提供者名稱",
|
||||
"add.name.placeholder": "例如:OpenAI",
|
||||
"add.title": "添加提供者",
|
||||
"add.type": "提供商類型",
|
||||
"api.url.preview": "預覽: {{url}}",
|
||||
"api.url.reset": "重置",
|
||||
"api.url.tip": "/結尾忽略v1版本,#結尾強制使用輸入位址",
|
||||
"api_host": "API 主機地址",
|
||||
"api_key": "API 密鑰",
|
||||
"api_key.tip": "多個密鑰使用逗號分隔",
|
||||
"api_version": "API 版本",
|
||||
"check": "檢查",
|
||||
"check_all_keys": "檢查所有密鑰",
|
||||
"check_multiple_keys": "檢查多個 API 密鑰",
|
||||
"delete.content": "確定要刪除此提供者嗎?",
|
||||
"delete.title": "刪除提供者",
|
||||
"docs_check": "檢查",
|
||||
"docs_more_details": "查看更多細節",
|
||||
"get_api_key": "獲取 API 密鑰",
|
||||
"no_models": "請先添加模型再檢查 API 連接",
|
||||
"not_checked": "未檢查",
|
||||
"remove_duplicate_keys": "移除重複密鑰",
|
||||
"remove_invalid_keys": "刪除無效密鑰",
|
||||
"search_placeholder": "搜尋模型 ID 或名稱",
|
||||
"title": "模型提供者"
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
"custom": "自定義代理",
|
||||
"none": "不使用代理",
|
||||
"system": "系統代理",
|
||||
"title": "代理模式"
|
||||
},
|
||||
"title": "代理設定"
|
||||
},
|
||||
"proxy.title": "代理地址",
|
||||
"theme.title": "主題",
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
"key": "按鍵",
|
||||
"new_topic": "新建話題",
|
||||
"title": "快速方式",
|
||||
"zoom_in": "放大界面",
|
||||
"zoom_out": "縮小界面",
|
||||
"zoom_reset": "重置縮放",
|
||||
"show_app": "顯示應用",
|
||||
"reset_defaults": "重置預設快捷鍵",
|
||||
"reset_defaults_confirm": "確定要重置所有快捷鍵嗎?",
|
||||
"press_shortcut": "按下快捷鍵",
|
||||
"alt_warning": "Mac 不能使用 Option + 字母作為快捷鍵",
|
||||
"reset_to_default": "重置為預設",
|
||||
"clear_shortcut": "清除快捷鍵"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "深色主題",
|
||||
"theme.light": "淺色主題",
|
||||
"theme.auto": "自動",
|
||||
"theme.title": "主題",
|
||||
"theme.window.style.opaque": "不透明視窗",
|
||||
"theme.window.style.title": "視窗樣式",
|
||||
"theme.window.style.transparent": "透明視窗",
|
||||
"theme.window.style.opaque": "不透明視窗",
|
||||
"font_size.title": "訊息字體大小",
|
||||
"title": "設定",
|
||||
"topic.position": "話題位置",
|
||||
"topic.position.left": "左側",
|
||||
"topic.position.right": "右側",
|
||||
"topic.show.time": "顯示話題時間",
|
||||
"shortcuts": {
|
||||
"title": "快速方式",
|
||||
"action": "操作",
|
||||
"key": "按鍵",
|
||||
"new_topic": "新建話題",
|
||||
"zoom_in": "放大界面",
|
||||
"zoom_out": "縮小界面",
|
||||
"zoom_reset": "重置縮放"
|
||||
}
|
||||
"tray.title": "啟用系統托盤圖標"
|
||||
},
|
||||
"translate": {
|
||||
"title": "翻譯",
|
||||
"any.language": "任意語言",
|
||||
"button.translate": "翻譯",
|
||||
"confirm": {
|
||||
"content": "翻譯後將覆蓋原文,是否繼續?",
|
||||
"title": "翻譯確認"
|
||||
},
|
||||
"error.not_configured": "翻譯模型未配置",
|
||||
"error.failed": "翻譯失敗",
|
||||
"input.placeholder": "輸入文字進行翻譯",
|
||||
"output.placeholder": "翻譯",
|
||||
"confirm": "原文已複製到剪貼簿,是否用翻譯後的文字替換?"
|
||||
"processing": "翻譯中...",
|
||||
"title": "翻譯",
|
||||
"close": "關閉"
|
||||
},
|
||||
"languages": {
|
||||
"english": "英文",
|
||||
"chinese": "簡體中文",
|
||||
"chinese-traditional": "繁體中文",
|
||||
"japanese": "日文",
|
||||
"korean": "韓文",
|
||||
"russian": "俄文",
|
||||
"spanish": "西班牙文",
|
||||
"french": "法文",
|
||||
"italian": "意大利文",
|
||||
"portuguese": "葡萄牙文",
|
||||
"arabic": "阿拉伯文"
|
||||
},
|
||||
"ollama": {
|
||||
"title": "Ollama",
|
||||
"keep_alive_time.title": "保持活躍時間",
|
||||
"keep_alive_time.placeholder": "分鐘",
|
||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。"
|
||||
},
|
||||
"error": {
|
||||
"chat.response": "出現錯誤。如果尚未配置 API 密鑰,請前往設定 > 模型提供者中配置密鑰",
|
||||
"backup.file_format": "備份文件格式錯誤",
|
||||
"provider_disabled": "模型提供商未啟用",
|
||||
"no_api_key": "API 密鑰未配置"
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
"show_window": "顯示視窗"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知識圖譜",
|
||||
"visualization": "可視化"
|
||||
},
|
||||
"export": {
|
||||
"attached_files": "附件",
|
||||
"user": "用戶",
|
||||
"assistant": "助手",
|
||||
"created": "創建時間",
|
||||
"last_updated": "最後更新",
|
||||
"messages": "訊息數",
|
||||
"conversation_details": "會話詳情",
|
||||
"conversation_history": "會話歷史"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,6 @@
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import localforage from 'localforage'
|
||||
|
||||
import { APP_NAME } from './config/env'
|
||||
import { ThemeMode } from './types'
|
||||
import { loadScript } from './utils'
|
||||
|
||||
export async function initMermaid(theme: ThemeMode) {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
localforage.config({
|
||||
driver: localforage.INDEXEDDB,
|
||||
name: 'CherryAI',
|
||||
version: 1.0,
|
||||
storeName: 'cherryai',
|
||||
description: `${APP_NAME} Storage`
|
||||
})
|
||||
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ let _agentGroups: Record<string, Agent[]> = {}
|
||||
|
||||
const AgentsPage: FC = () => {
|
||||
const [search, setSearch] = useState('')
|
||||
const [searchInput, setSearchInput] = useState('')
|
||||
|
||||
const agentGroups = useMemo(() => {
|
||||
if (Object.keys(_agentGroups).length === 0) {
|
||||
@ -44,26 +45,35 @@ const AgentsPage: FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const filteredAgentGroups = useMemo(() => {
|
||||
const groups = { 我的: [] }
|
||||
const groups: Record<string, Agent[]> = {
|
||||
我的: [],
|
||||
精选: agentGroups['精选'] || []
|
||||
}
|
||||
|
||||
if (!search.trim()) {
|
||||
Object.entries(agentGroups).forEach(([group, agents]) => {
|
||||
groups[group] = agents
|
||||
if (group !== '精选') {
|
||||
groups[group] = agents
|
||||
}
|
||||
})
|
||||
return groups
|
||||
}
|
||||
|
||||
Object.entries(agentGroups).forEach(([group, agents]) => {
|
||||
const filteredAgents = agents.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
if (filteredAgents.length > 0) {
|
||||
groups[group] = filteredAgents
|
||||
}
|
||||
const uniqueAgents = new Map<string, Agent>()
|
||||
|
||||
Object.entries(agentGroups).forEach(([, agents]) => {
|
||||
agents.forEach((agent) => {
|
||||
if (
|
||||
(agent.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(search.toLowerCase())) &&
|
||||
!uniqueAgents.has(agent.name)
|
||||
) {
|
||||
uniqueAgents.set(agent.name, agent)
|
||||
}
|
||||
})
|
||||
})
|
||||
return groups
|
||||
|
||||
return { 搜索结果: Array.from(uniqueAgents.values()) }
|
||||
}, [agentGroups, search])
|
||||
|
||||
const getAgentName = (agent: Agent) => {
|
||||
@ -111,9 +121,7 @@ const AgentsPage: FC = () => {
|
||||
)
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
let groups = Object.keys(filteredAgentGroups)
|
||||
|
||||
groups = groups.includes('办公') ? [groups[0], '办公', ...groups.slice(1)] : groups
|
||||
const groups = Object.keys(filteredAgentGroups)
|
||||
|
||||
return groups.map((group, i) => {
|
||||
const id = String(i + 1)
|
||||
@ -133,7 +141,10 @@ const AgentsPage: FC = () => {
|
||||
) : (
|
||||
filteredAgentGroups[group]?.map((agent, index) => (
|
||||
<Col span={6} key={group + index}>
|
||||
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
|
||||
<AgentCard
|
||||
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
|
||||
agent={agent as any}
|
||||
/>
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
@ -144,6 +155,14 @@ const AgentsPage: FC = () => {
|
||||
})
|
||||
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search])
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchInput.trim() === '') {
|
||||
setSearch('')
|
||||
} else {
|
||||
setSearch(searchInput)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
@ -156,18 +175,37 @@ const AgentsPage: FC = () => {
|
||||
size="small"
|
||||
variant="filled"
|
||||
allowClear
|
||||
suffix={<SearchOutlined />}
|
||||
value={search}
|
||||
onClear={() => setSearch('')}
|
||||
suffix={<SearchOutlined onClick={handleSearch} />}
|
||||
value={searchInput}
|
||||
maxLength={50}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<div style={{ width: 80 }} />
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<AssistantsContainer>
|
||||
{tabItems.length > 0 ? (
|
||||
<Tabs tabPosition="right" animated items={tabItems} />
|
||||
{Object.values(filteredAgentGroups).flat().length > 0 ? (
|
||||
search.trim() ? (
|
||||
<TabContent>
|
||||
<Row gutter={[20, 20]}>
|
||||
{Object.values(filteredAgentGroups)
|
||||
.flat()
|
||||
.map((agent, index, array) => (
|
||||
<Col span={array.length === 1 ? 12 : 6} key={index}>
|
||||
<AgentCard
|
||||
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
|
||||
agent={agent as any}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</TabContent>
|
||||
) : (
|
||||
<Tabs tabPosition="right" animated items={tabItems} $language={i18n.language} />
|
||||
)
|
||||
) : (
|
||||
<EmptyView>
|
||||
<Empty description={t('agents.search.no_results')} />
|
||||
@ -226,7 +264,7 @@ const EmptyView = styled.div`
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
const Tabs = styled(TabsAntd)`
|
||||
const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row-reverse;
|
||||
@ -234,8 +272,8 @@ const Tabs = styled(TabsAntd)`
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
min-width: 140px;
|
||||
max-width: 140px;
|
||||
min-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
|
||||
max-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
padding: 10px 8px;
|
||||
@ -245,19 +283,28 @@ const Tabs = styled(TabsAntd)`
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
margin: 0 !important;
|
||||
border-radius: 20px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 5px !important;
|
||||
font-size: 13px;
|
||||
justify-content: left;
|
||||
padding: 7px 12px !important;
|
||||
padding: 7px 15px !important;
|
||||
border: 0.5px solid transparent;
|
||||
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
|
||||
.ant-tabs-tab-btn {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-text) !important;
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
}
|
||||
.ant-tabs-tab-active {
|
||||
background-color: var(--color-background-mute);
|
||||
background-color: var(--color-background-soft);
|
||||
border-right: none;
|
||||
border: 0.5px solid var(--color-border);
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
|
||||
@ -3,6 +3,7 @@ export type GroupTranslations = {
|
||||
'en-US': string
|
||||
'zh-CN': string
|
||||
'zh-TW': string
|
||||
'ru-RU': string
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,171 +11,205 @@ export const groupTranslations: GroupTranslations = {
|
||||
我的: {
|
||||
'en-US': 'My Agents',
|
||||
'zh-CN': '我的',
|
||||
'zh-TW': '我的'
|
||||
'zh-TW': '我的',
|
||||
'ru-RU': 'Мои агенты'
|
||||
},
|
||||
职业: {
|
||||
'en-US': 'Career',
|
||||
'zh-CN': '职业',
|
||||
'zh-TW': '職業'
|
||||
'zh-TW': '職業',
|
||||
'ru-RU': 'Карьера'
|
||||
},
|
||||
商业: {
|
||||
'en-US': 'Business',
|
||||
'zh-CN': '商业',
|
||||
'zh-TW': '商業'
|
||||
'zh-TW': '商業',
|
||||
'ru-RU': 'Бизнес'
|
||||
},
|
||||
工具: {
|
||||
'en-US': 'Tools',
|
||||
'zh-CN': '工具',
|
||||
'zh-TW': '工具'
|
||||
'zh-TW': '工具',
|
||||
'ru-RU': 'Инструменты'
|
||||
},
|
||||
语言: {
|
||||
'en-US': 'Language',
|
||||
'zh-CN': '语言',
|
||||
'zh-TW': '語言'
|
||||
'zh-TW': '語言',
|
||||
'ru-RU': 'Язык'
|
||||
},
|
||||
办公: {
|
||||
'en-US': 'Office',
|
||||
'zh-CN': '办公',
|
||||
'zh-TW': '辦公'
|
||||
'zh-TW': '辦公',
|
||||
'ru-RU': 'Офис'
|
||||
},
|
||||
通用: {
|
||||
'en-US': 'General',
|
||||
'zh-CN': '通用',
|
||||
'zh-TW': '通用'
|
||||
'zh-TW': '通用',
|
||||
'ru-RU': 'Общее'
|
||||
},
|
||||
写作: {
|
||||
'en-US': 'Writing',
|
||||
'zh-CN': '写作',
|
||||
'zh-TW': '寫作'
|
||||
'zh-TW': '寫作',
|
||||
'ru-RU': 'Письмо'
|
||||
},
|
||||
Artifacts: {
|
||||
'en-US': 'Artifacts',
|
||||
'zh-CN': 'Artifacts',
|
||||
'zh-TW': 'Artifacts'
|
||||
精选: {
|
||||
'en-US': 'Featured',
|
||||
'zh-CN': '精选',
|
||||
'zh-TW': '精選',
|
||||
'ru-RU': 'Избранное'
|
||||
},
|
||||
编程: {
|
||||
'en-US': 'Programming',
|
||||
'zh-CN': '编程',
|
||||
'zh-TW': '編程'
|
||||
'zh-TW': '編程',
|
||||
'ru-RU': 'Программирование'
|
||||
},
|
||||
情感: {
|
||||
'en-US': 'Emotion',
|
||||
'zh-CN': '情感',
|
||||
'zh-TW': '情感'
|
||||
'zh-TW': '情感',
|
||||
'ru-RU': 'Эмоции'
|
||||
},
|
||||
教育: {
|
||||
'en-US': 'Education',
|
||||
'zh-CN': '教育',
|
||||
'zh-TW': '教育'
|
||||
'zh-TW': '教育',
|
||||
'ru-RU': 'Образование'
|
||||
},
|
||||
创意: {
|
||||
'en-US': 'Creative',
|
||||
'zh-CN': '创意',
|
||||
'zh-TW': '創意'
|
||||
'zh-TW': '創意',
|
||||
'ru-RU': 'Креатив'
|
||||
},
|
||||
学术: {
|
||||
'en-US': 'Academic',
|
||||
'zh-CN': '学术',
|
||||
'zh-TW': '學術'
|
||||
'zh-TW': '學術',
|
||||
'ru-RU': 'Академический'
|
||||
},
|
||||
设计: {
|
||||
'en-US': 'Design',
|
||||
'zh-CN': '设计',
|
||||
'zh-TW': '設計'
|
||||
'zh-TW': '設計',
|
||||
'ru-RU': 'Дизайн'
|
||||
},
|
||||
艺术: {
|
||||
'en-US': 'Art',
|
||||
'zh-CN': '艺术',
|
||||
'zh-TW': '藝術'
|
||||
'zh-TW': '藝術',
|
||||
'ru-RU': 'Искусство'
|
||||
},
|
||||
娱乐: {
|
||||
'en-US': 'Entertainment',
|
||||
'zh-CN': '娱乐',
|
||||
'zh-TW': '娛樂'
|
||||
'zh-TW': '娛樂',
|
||||
'ru-RU': 'Развлечения'
|
||||
},
|
||||
生活: {
|
||||
'en-US': 'Life',
|
||||
'zh-CN': '生活',
|
||||
'zh-TW': '生活'
|
||||
'zh-TW': '生活',
|
||||
'ru-RU': 'Жизнь'
|
||||
},
|
||||
医疗: {
|
||||
'en-US': 'Medical',
|
||||
'zh-CN': '医疗',
|
||||
'zh-TW': '醫療'
|
||||
'zh-TW': '醫療',
|
||||
'ru-RU': 'Медицина'
|
||||
},
|
||||
游戏: {
|
||||
'en-US': 'Games',
|
||||
'zh-CN': '游戏',
|
||||
'zh-TW': '遊戲'
|
||||
'zh-TW': '遊戲',
|
||||
'ru-RU': 'Игры'
|
||||
},
|
||||
翻译: {
|
||||
'en-US': 'Translation',
|
||||
'zh-CN': '翻译',
|
||||
'zh-TW': '翻譯'
|
||||
'zh-TW': '翻譯',
|
||||
'ru-RU': 'Перевод'
|
||||
},
|
||||
音乐: {
|
||||
'en-US': 'Music',
|
||||
'zh-CN': '音乐',
|
||||
'zh-TW': '音樂'
|
||||
'zh-TW': '音樂',
|
||||
'ru-RU': 'Музыка'
|
||||
},
|
||||
点评: {
|
||||
'en-US': 'Review',
|
||||
'zh-CN': '点评',
|
||||
'zh-TW': '點評'
|
||||
'zh-TW': '點評',
|
||||
'ru-RU': 'Обзор'
|
||||
},
|
||||
文案: {
|
||||
'en-US': 'Copywriting',
|
||||
'zh-CN': '文案',
|
||||
'zh-TW': '文案'
|
||||
'zh-TW': '文案',
|
||||
'ru-RU': 'Копирайтинг'
|
||||
},
|
||||
百科: {
|
||||
'en-US': 'Encyclopedia',
|
||||
'zh-CN': '百科',
|
||||
'zh-TW': '百科'
|
||||
'zh-TW': '百科',
|
||||
'ru-RU': 'Энциклопедия'
|
||||
},
|
||||
健康: {
|
||||
'en-US': 'Health',
|
||||
'zh-CN': '健康',
|
||||
'zh-TW': '健康'
|
||||
'zh-TW': '健康',
|
||||
'ru-RU': 'Здоровье'
|
||||
},
|
||||
营销: {
|
||||
'en-US': 'Marketing',
|
||||
'zh-CN': '营销',
|
||||
'zh-TW': '營銷'
|
||||
'zh-TW': '營銷',
|
||||
'ru-RU': 'Маркетинг'
|
||||
},
|
||||
科学: {
|
||||
'en-US': 'Science',
|
||||
'zh-CN': '科学',
|
||||
'zh-TW': '科學'
|
||||
'zh-TW': '科學',
|
||||
'ru-RU': 'Наука'
|
||||
},
|
||||
分析: {
|
||||
'en-US': 'Analysis',
|
||||
'zh-CN': '分析',
|
||||
'zh-TW': '分析'
|
||||
'zh-TW': '分析',
|
||||
'ru-RU': 'Анализ'
|
||||
},
|
||||
法律: {
|
||||
'en-US': 'Legal',
|
||||
'zh-CN': '法律',
|
||||
'zh-TW': '法律'
|
||||
'zh-TW': '法律',
|
||||
'ru-RU': 'Право'
|
||||
},
|
||||
咨询: {
|
||||
'en-US': 'Consulting',
|
||||
'zh-CN': '咨询',
|
||||
'zh-TW': '諮詢'
|
||||
'zh-TW': '諮詢',
|
||||
'ru-RU': 'Консалтинг'
|
||||
},
|
||||
金融: {
|
||||
'en-US': 'Finance',
|
||||
'zh-CN': '金融',
|
||||
'zh-TW': '金融'
|
||||
'zh-TW': '金融',
|
||||
'ru-RU': 'Финансы'
|
||||
},
|
||||
旅游: {
|
||||
'en-US': 'Travel',
|
||||
'zh-CN': '旅游',
|
||||
'zh-TW': '旅遊'
|
||||
'zh-TW': '旅遊',
|
||||
'ru-RU': 'Путешествия'
|
||||
},
|
||||
管理: {
|
||||
'en-US': 'Management',
|
||||
'zh-CN': '管理',
|
||||
'zh-TW': '管理'
|
||||
'zh-TW': '管理',
|
||||
'ru-RU': 'Управление'
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ const Container = styled.div`
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
background-color: var(--color-background);
|
||||
border-radius: 15px;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
@ -94,8 +94,8 @@ const Container = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 15px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
background: var(--color-background-soft);
|
||||
transition: all 0.5s ease;
|
||||
border-bottom: none;
|
||||
@ -133,7 +133,7 @@ const CardInfo = styled.div`
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: all 0.5s ease;
|
||||
padding: 0 15px;
|
||||
padding: 0 8px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
|
||||
@ -38,19 +38,20 @@ const FilesPage: FC = () => {
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'file',
|
||||
key: 'file'
|
||||
key: 'file',
|
||||
width: '300px'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: '100px'
|
||||
width: '80px'
|
||||
},
|
||||
{
|
||||
title: t('files.count'),
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: '100px'
|
||||
width: '60px'
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
@ -219,7 +220,7 @@ const ImageInfo = styled.div`
|
||||
const SideNav = styled.div`
|
||||
width: var(--assistants-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 15px;
|
||||
padding: 7px 12px;
|
||||
|
||||
.ant-menu {
|
||||
border-inline-end: none !important;
|
||||
@ -227,18 +228,22 @@ const SideNav = styled.div`
|
||||
}
|
||||
|
||||
.ant-menu-item {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
margin: 4px 0;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
background-color: var(--color-background-soft) !important;
|
||||
}
|
||||
|
||||
&.ant-menu-item-selected {
|
||||
background-color: var(--color-background-soft);
|
||||
color: var(--color-primary);
|
||||
border: 0.5px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import { Divider, Input } from 'antd'
|
||||
import { Input } from 'antd'
|
||||
import { last } from 'lodash'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -59,44 +58,38 @@ const TopicsPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'flex-start' }}>{t('history.title')} </NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<Header>
|
||||
{stack.length > 1 && (
|
||||
<HeaderLeft>
|
||||
<MenuIcon onClick={goBack}>
|
||||
<ArrowLeftOutlined />
|
||||
</MenuIcon>
|
||||
</HeaderLeft>
|
||||
)}
|
||||
<SearchInput
|
||||
placeholder={t('history.search.placeholder')}
|
||||
type="search"
|
||||
value={search}
|
||||
allowClear
|
||||
onChange={(e) => setSearch(e.target.value.trimStart())}
|
||||
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
|
||||
onPressEnter={onSearch}
|
||||
/>
|
||||
</Header>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<TopicsHistory
|
||||
keywords={search}
|
||||
onClick={onTopicClick as any}
|
||||
onSearch={onSearch}
|
||||
style={{ display: isShow('topics') }}
|
||||
<Header>
|
||||
{stack.length > 1 && (
|
||||
<HeaderLeft>
|
||||
<MenuIcon onClick={goBack}>
|
||||
<ArrowLeftOutlined />
|
||||
</MenuIcon>
|
||||
</HeaderLeft>
|
||||
)}
|
||||
<SearchInput
|
||||
placeholder={t('history.search.placeholder')}
|
||||
type="search"
|
||||
value={search}
|
||||
allowClear
|
||||
onChange={(e) => setSearch(e.target.value.trimStart())}
|
||||
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
|
||||
onPressEnter={onSearch}
|
||||
/>
|
||||
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
||||
<SearchResults
|
||||
keywords={isShow('search') ? search : ''}
|
||||
onMessageClick={onMessageClick}
|
||||
onTopicClick={onTopicClick}
|
||||
style={{ display: isShow('search') }}
|
||||
/>
|
||||
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
||||
</ContentContainer>
|
||||
</Header>
|
||||
<TopicsHistory
|
||||
keywords={search}
|
||||
onClick={onTopicClick as any}
|
||||
onSearch={onSearch}
|
||||
style={{ display: isShow('topics') }}
|
||||
/>
|
||||
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
||||
<SearchResults
|
||||
keywords={isShow('search') ? search : ''}
|
||||
onMessageClick={onMessageClick}
|
||||
onTopicClick={onTopicClick}
|
||||
style={{ display: isShow('search') }}
|
||||
/>
|
||||
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -108,24 +101,18 @@ const Container = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const Header = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 20px;
|
||||
padding-top: 10px;
|
||||
padding: 12px 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: var(--color-background-mute);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 0.5px solid var(--color-frame-border);
|
||||
`
|
||||
|
||||
const HeaderLeft = styled.div`
|
||||
@ -133,7 +120,7 @@ const HeaderLeft = styled.div`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 12px;
|
||||
left: 15px;
|
||||
`
|
||||
|
||||
@ -143,11 +130,11 @@ const MenuIcon = styled.div`
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
border-radius: 50%;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
background-color: var(--color-background);
|
||||
.anticon {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@ import { ArrowRightOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
||||
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
@ -14,7 +14,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
||||
const navigate = useNavigate()
|
||||
const navigate = NavigationService.navigate!
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!message) {
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { ArrowRightOutlined, MessageOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Button, Divider, Empty } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { FC } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { default as MessageItem } from '../../home/Messages/Message'
|
||||
@ -18,8 +20,9 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
}
|
||||
|
||||
const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
||||
const navigate = useNavigate()
|
||||
const navigate = NavigationService.navigate!
|
||||
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
|
||||
const { messageStyle } = useSettings()
|
||||
|
||||
const isEmpty = (topic?.messages || []).length === 0
|
||||
|
||||
@ -27,14 +30,16 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const onContinueChat = (topic: Topic) => {
|
||||
const onContinueChat = async (topic: Topic) => {
|
||||
await isGenerating()
|
||||
SearchPopup.hide()
|
||||
const assistant = getAssistantById(topic.assistantId)
|
||||
navigate('/', { state: { assistant, topic } })
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll}>
|
||||
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll} className={messageStyle}>
|
||||
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
|
||||
{topic?.messages.map((message) => (
|
||||
<div key={message.id} style={{ position: 'relative' }}>
|
||||
|
||||
@ -24,7 +24,7 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Container id="chat" className={messageStyle}>
|
||||
<Main vertical flex={1} justify="space-between">
|
||||
<Main id="chat-main" vertical flex={1} justify="space-between">
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
assistant={assistant}
|
||||
@ -52,35 +52,6 @@ const Container = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
&.bubble {
|
||||
background-color: var(--chat-background);
|
||||
.system-prompt {
|
||||
background-color: var(--chat-background-assistant);
|
||||
}
|
||||
.message-content-container {
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
.message-user {
|
||||
.markdown,
|
||||
.anticon,
|
||||
.iconfont,
|
||||
.message-tokens {
|
||||
color: var(--chat-text-user);
|
||||
}
|
||||
.message-action-button:hover {
|
||||
background-color: var(--color-white-soft);
|
||||
}
|
||||
}
|
||||
#inputbar {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
background: var(--color-background);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Main = styled(Flex)`
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { FC, useState } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Chat from './Chat'
|
||||
@ -14,6 +15,7 @@ let _activeAssistant: Assistant
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
@ -24,6 +26,15 @@ const HomePage: FC = () => {
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
useEffect(() => {
|
||||
NavigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
state?.assistant && setActiveAssistant(state?.assistant)
|
||||
state?.topic && setActiveTopic(state?.topic)
|
||||
}, [state])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { PaperClipOutlined } from '@ant-design/icons'
|
||||
import { documentExts, imageExts, textExts } from '@renderer/config/constant'
|
||||
import { isVisionModel } from '@renderer/config/models'
|
||||
import { FileType, Model } from '@renderer/types'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -7,21 +7,26 @@ import {
|
||||
PauseCircleOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { documentExts, imageExts, textExts } from '@renderer/config/constant'
|
||||
import { PicCenterOutlined } from '@ant-design/icons'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isVisionModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||
import { Assistant, FileType, Message, Topic } from '@renderer/types'
|
||||
import { delay, getFileExtension, uuid } from '@renderer/utils'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
@ -47,7 +52,15 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { addTopic, model, setModel } = useAssistant(assistant.id)
|
||||
const { sendMessageShortcut, fontSize, pasteLongTextAsFile, showInputEstimatedTokens } = useSettings()
|
||||
const {
|
||||
sendMessageShortcut,
|
||||
fontSize,
|
||||
pasteLongTextAsFile,
|
||||
showInputEstimatedTokens,
|
||||
clickAssistantToShowTopic,
|
||||
language,
|
||||
autoTranslateWithSpace
|
||||
} = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
const [contextCount, setContextCount] = useState(0)
|
||||
@ -60,6 +73,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const { searching } = useRuntime()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const dispatch = useAppDispatch()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
@ -107,9 +123,48 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
setExpend(false)
|
||||
}, [assistant.id, assistant.topics, generating, files, text])
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsTranslating(true)
|
||||
const translatedText = await translateText(text, 'english')
|
||||
translatedText && setText(translatedText)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const isEnterPressed = event.keyCode == 13
|
||||
|
||||
if (autoTranslateWithSpace) {
|
||||
if (event.key === ' ') {
|
||||
setSpaceClickCount((prev) => prev + 1)
|
||||
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
|
||||
spaceClickTimer.current = setTimeout(() => {
|
||||
setSpaceClickCount(0)
|
||||
}, 200)
|
||||
|
||||
if (spaceClickCount === 2) {
|
||||
console.log('Triple space detected - trigger translation')
|
||||
setSpaceClickCount(0)
|
||||
setIsTranslating(true)
|
||||
translate()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
return setExpend(false)
|
||||
@ -131,6 +186,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(async () => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||
return
|
||||
}
|
||||
|
||||
const topic = getDefaultTopic(assistant.id)
|
||||
|
||||
await db.topics.add({ id: topic.id, messages: [] })
|
||||
@ -143,7 +203,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
|
||||
addTopic(topic)
|
||||
setActiveTopic(topic)
|
||||
}, [addTopic, assistant, setActiveTopic, setModel])
|
||||
|
||||
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}, [addTopic, assistant, clickAssistantToShowTopic, generating, setActiveTopic, setModel, t])
|
||||
|
||||
const clearTopic = async () => {
|
||||
if (generating) {
|
||||
@ -252,20 +314,18 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Command or Ctrl + N create new topic
|
||||
useEffect(() => {
|
||||
const onKeydown = (e) => {
|
||||
if (!generating) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||
addNewTopic()
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
}
|
||||
const onTranslated = (translatedText: string) => {
|
||||
setText(translatedText)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
}
|
||||
|
||||
useShortcut('new_topic', () => {
|
||||
if (!generating) {
|
||||
addNewTopic()
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
return () => document.removeEventListener('keydown', onKeydown)
|
||||
}, [addNewTopic, generating])
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||
@ -288,6 +348,18 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
textareaRef.current?.focus()
|
||||
}, [assistant])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||
@ -296,7 +368,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder')}
|
||||
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
@ -313,7 +385,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: isMac ? '⌘' : 'Ctrl' })} arrow>
|
||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||
<FormOutlined />
|
||||
</ToolbarButton>
|
||||
@ -342,6 +414,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||
<PicCenterOutlined />
|
||||
</Tooltip>
|
||||
</ToolbarButton>
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
@ -356,6 +433,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
/>
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{!language.startsWith('en') && (
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
)}
|
||||
{generating && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ArrowUpOutlined, MenuOutlined, PicCenterOutlined } from '@ant-design/icons'
|
||||
import { ArrowUpOutlined, MenuOutlined } from '@ant-design/icons'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Divider, Popover, Tooltip } from 'antd'
|
||||
import { Divider, Popover } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -13,7 +13,7 @@ type Props = {
|
||||
ToolbarButton: any
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount, ToolbarButton, ...props }) => {
|
||||
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount }) => {
|
||||
const { t } = useTranslation()
|
||||
const { showInputEstimatedTokens } = useSettings()
|
||||
|
||||
@ -38,21 +38,14 @@ const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCou
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarButton type="text" onClick={props.onClick}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||
<PicCenterOutlined />
|
||||
</Tooltip>
|
||||
</ToolbarButton>
|
||||
<Container>
|
||||
<Popover content={PopoverContent}>
|
||||
<MenuOutlined /> {contextCount}
|
||||
<Divider type="vertical" style={{ marginTop: 0, marginLeft: 5, marginRight: 5 }} />
|
||||
<ArrowUpOutlined />
|
||||
{inputTokenCount} / {estimateTokenCount}
|
||||
</Popover>
|
||||
</Container>
|
||||
</>
|
||||
<Container>
|
||||
<Popover content={PopoverContent}>
|
||||
<MenuOutlined /> {contextCount}
|
||||
<Divider type="vertical" style={{ marginTop: 0, marginLeft: 5, marginRight: 5 }} />
|
||||
<ArrowUpOutlined />
|
||||
{inputTokenCount} / {estimateTokenCount}
|
||||
</Popover>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -32,10 +32,10 @@ const Artifacts: FC<Props> = ({ html }) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Button type="primary" icon={<ExpandOutlined />} onClick={onPreview}>
|
||||
<Button type="primary" icon={<ExpandOutlined />} onClick={onPreview} size="small">
|
||||
{t('chat.artifacts.button.preview')}
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={onDownload}>
|
||||
<Button icon={<DownloadOutlined />} onClick={onDownload} size="small">
|
||||
{t('chat.artifacts.button.download')}
|
||||
</Button>
|
||||
</Container>
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import { CheckOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { initMermaid } from '@renderer/init'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import React, { memo, useState } from 'react'
|
||||
import React, { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Artifacts from './Artifacts'
|
||||
@ -19,38 +15,103 @@ interface CodeBlockProps {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
|
||||
return (
|
||||
<CollapseIconWrapper onClick={onClick}>
|
||||
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
|
||||
</CollapseIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ExpandButton: React.FC<{
|
||||
isExpanded: boolean
|
||||
onClick: () => void
|
||||
showButton: boolean
|
||||
}> = ({ isExpanded, onClick, showButton }) => {
|
||||
if (!showButton) return null
|
||||
|
||||
return (
|
||||
<ExpandButtonWrapper onClick={onClick}>
|
||||
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
|
||||
</ExpandButtonWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const showFooterCopyButton = children && children.length > 500
|
||||
const { codeShowLineNumbers, fontSize } = useSettings()
|
||||
const { theme } = useTheme()
|
||||
const language = match?.[1]
|
||||
const { codeShowLineNumbers, fontSize, codeCollapsible } = useSettings()
|
||||
const language = match?.[1] ?? 'text'
|
||||
const [html, setHtml] = useState<string>('')
|
||||
const { codeToHtml } = useSyntaxHighlighter()
|
||||
const [isExpanded, setIsExpanded] = useState(!codeCollapsible)
|
||||
const [shouldShowExpandButton, setShouldShowExpandButton] = useState(false)
|
||||
const codeContentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const showFooterCopyButton = children && children.length > 500 && !codeCollapsible
|
||||
|
||||
useEffect(() => {
|
||||
const loadHighlightedCode = async () => {
|
||||
const highlightedHtml = await codeToHtml(children, language)
|
||||
setHtml(highlightedHtml)
|
||||
}
|
||||
loadHighlightedCode()
|
||||
}, [children, language, codeToHtml])
|
||||
|
||||
useEffect(() => {
|
||||
if (codeContentRef.current) {
|
||||
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
|
||||
}
|
||||
}, [html])
|
||||
|
||||
useEffect(() => {
|
||||
if (!codeCollapsible) {
|
||||
setIsExpanded(true)
|
||||
setShouldShowExpandButton(false)
|
||||
} else {
|
||||
setIsExpanded(!codeCollapsible)
|
||||
if (codeContentRef.current) {
|
||||
setShouldShowExpandButton(codeContentRef.current.scrollHeight > 350)
|
||||
}
|
||||
}
|
||||
}, [codeCollapsible])
|
||||
|
||||
if (language === 'mermaid') {
|
||||
initMermaid(theme)
|
||||
return <Mermaid chart={children} />
|
||||
}
|
||||
|
||||
return match ? (
|
||||
<div className="code-block">
|
||||
<CodeHeader>
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{codeCollapsible && shouldShowExpandButton && (
|
||||
<CollapseIcon expanded={isExpanded} onClick={() => setIsExpanded(!isExpanded)} />
|
||||
)}
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
</div>
|
||||
<CopyButton text={children} />
|
||||
</CodeHeader>
|
||||
<SyntaxHighlighter
|
||||
language={match[1]}
|
||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||
wrapLongLines={false}
|
||||
showLineNumbers={codeShowLineNumbers}
|
||||
customStyle={{
|
||||
<CodeContent
|
||||
ref={codeContentRef}
|
||||
isShowLineNumbers={codeShowLineNumbers}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{
|
||||
border: '0.5px solid var(--color-code-background)',
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
marginTop: 0,
|
||||
fontSize
|
||||
}}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
fontSize: fontSize - 1,
|
||||
maxHeight: codeCollapsible && !isExpanded ? '350px' : 'none',
|
||||
overflow: codeCollapsible && !isExpanded ? 'auto' : 'visible',
|
||||
position: 'relative'
|
||||
}}
|
||||
/>
|
||||
{codeCollapsible && (
|
||||
<ExpandButton
|
||||
isExpanded={isExpanded}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
showButton={shouldShowExpandButton}
|
||||
/>
|
||||
)}
|
||||
{showFooterCopyButton && (
|
||||
<CodeFooter>
|
||||
<CopyButton text={children} style={{ marginTop: -40, marginRight: 10 }} />
|
||||
@ -81,6 +142,31 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
|
||||
)
|
||||
}
|
||||
|
||||
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
|
||||
.shiki {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.isShowLineNumbers &&
|
||||
`
|
||||
code {
|
||||
counter-reset: step;
|
||||
counter-increment: step 0;
|
||||
}
|
||||
|
||||
code .line::before {
|
||||
content: counter(step);
|
||||
counter-increment: step;
|
||||
width: 1rem;
|
||||
margin-right: 1rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
opacity: 0.35;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const CodeHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -111,6 +197,7 @@ const CodeFooter = styled.div`
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.copy {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
@ -121,4 +208,45 @@ const CodeFooter = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const ExpandButtonWrapper = styled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 25px;
|
||||
margin-top: -25px;
|
||||
|
||||
.button-text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
color: var(--color-text-3);
|
||||
z-index: 1;
|
||||
transition: color 0.2s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover .button-text {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`
|
||||
|
||||
const CollapseIconWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(CodeBlock)
|
||||
|
||||
@ -18,6 +18,9 @@ import CodeBlock from './CodeBlock'
|
||||
import ImagePreview from './ImagePreview'
|
||||
import Link from './Link'
|
||||
|
||||
const ALLOWED_ELEMENTS =
|
||||
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr)/i
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
@ -36,8 +39,8 @@ const Markdown: FC<Props> = ({ message }) => {
|
||||
}, [message.content, message.status, t])
|
||||
|
||||
const rehypePlugins = useMemo(() => {
|
||||
const hasUnsafeElements = /<(input|textarea|select)/i.test(messageContent)
|
||||
return hasUnsafeElements ? [rehypeMath] : [rehypeRaw, rehypeMath]
|
||||
const hasElements = ALLOWED_ELEMENTS.test(messageContent)
|
||||
return hasElements ? [rehypeRaw, rehypeMath] : [rehypeMath]
|
||||
}, [messageContent, rehypeMath])
|
||||
|
||||
if (message.role === 'user' && !renderInputMessageAsMarkdown) {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import MermaidPopup from './MermaidPopup'
|
||||
|
||||
interface Props {
|
||||
chart: string
|
||||
}
|
||||
@ -9,7 +11,15 @@ const Mermaid: React.FC<Props> = ({ chart }) => {
|
||||
window?.mermaid?.contentLoaded()
|
||||
}, [])
|
||||
|
||||
return <div className="mermaid">{chart}</div>
|
||||
const onPreview = () => {
|
||||
MermaidPopup.show({ chart })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mermaid" onClick={onPreview} style={{ cursor: 'pointer' }}>
|
||||
{chart}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Mermaid
|
||||
|
||||
164
src/renderer/src/pages/home/Markdown/MermaidPopup.tsx
Normal file
164
src/renderer/src/pages/home/Markdown/MermaidPopup.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { download } from '@renderer/utils/download'
|
||||
import { Button, Modal, Space, Tabs } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ShowParams {
|
||||
chart: string
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const mermaidId = `mermaid-popup-${Date.now()}`
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const handleDownload = async (format: 'svg' | 'png') => {
|
||||
try {
|
||||
const element = document.getElementById(mermaidId)
|
||||
if (!element) return
|
||||
|
||||
const timestamp = Date.now()
|
||||
|
||||
if (format === 'svg') {
|
||||
const svgElement = element.querySelector('svg')
|
||||
if (!svgElement) return
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement)
|
||||
const blob = new Blob([svgData], { type: 'image/svg+xml' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
download(url, `mermaid-diagram-${timestamp}.svg`)
|
||||
URL.revokeObjectURL(url)
|
||||
} else if (format === 'png') {
|
||||
const svgElement = element.querySelector('svg')
|
||||
if (!svgElement) return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number) || []
|
||||
const width = viewBox[2] || svgElement.clientWidth || svgElement.getBoundingClientRect().width
|
||||
const height = viewBox[3] || svgElement.clientHeight || svgElement.getBoundingClientRect().height
|
||||
|
||||
const svgData = new XMLSerializer().serializeToString(svgElement)
|
||||
const svgBase64 = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgData)))}`
|
||||
|
||||
img.onload = () => {
|
||||
const scale = 3
|
||||
canvas.width = width * scale
|
||||
canvas.height = height * scale
|
||||
|
||||
if (ctx) {
|
||||
ctx.scale(scale, scale)
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
}
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const pngUrl = URL.createObjectURL(blob)
|
||||
download(pngUrl, `mermaid-diagram-${timestamp}.png`)
|
||||
URL.revokeObjectURL(pngUrl)
|
||||
}
|
||||
}, 'image/png')
|
||||
}
|
||||
img.src = svgBase64
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window?.mermaid?.contentLoaded()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('mermaid.title')}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
width={1000}
|
||||
centered
|
||||
footer={[
|
||||
<Space key="download-buttons">
|
||||
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
|
||||
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
|
||||
</Space>
|
||||
]}>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'preview',
|
||||
label: t('mermaid.tabs.preview'),
|
||||
children: (
|
||||
<StyledMermaid id={mermaidId} className="mermaid">
|
||||
{chart}
|
||||
</StyledMermaid>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: t('mermaid.tabs.source'),
|
||||
children: (
|
||||
<pre
|
||||
style={{
|
||||
maxHeight: 'calc(80vh - 200px)',
|
||||
overflowY: 'auto',
|
||||
padding: '16px'
|
||||
}}>
|
||||
{chart}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class MermaidPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('MermaidPopup')
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>,
|
||||
'MermaidPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const StyledMermaid = styled.div`
|
||||
max-height: calc(80vh - 200px);
|
||||
text-align: center;
|
||||
overflow-y: auto;
|
||||
`
|
||||
@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageContent from './MessageContent'
|
||||
import MessageErrorBoundary from './MessageErrorBoundary'
|
||||
import MessageHeader from './MessageHeader'
|
||||
import MessageMenubar from './MessageMenubar'
|
||||
import MessageTokens from './MessageTokens'
|
||||
@ -29,6 +30,9 @@ interface Props {
|
||||
onDeleteMessage?: (message: Message) => void
|
||||
}
|
||||
|
||||
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
|
||||
isBubbleStyle ? (isAssistantMessage ? 'var(--chat-background-assistant)' : 'var(--chat-background-user)') : undefined
|
||||
|
||||
const MessageItem: FC<Props> = ({
|
||||
message: _message,
|
||||
topic,
|
||||
@ -56,37 +60,33 @@ const MessageItem: FC<Props> = ({
|
||||
}, [messageFont])
|
||||
|
||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||
const messageBackground = isBubbleStyle
|
||||
? isAssistantMessage
|
||||
? 'var(--chat-background-assistant)'
|
||||
: 'var(--chat-background-user)'
|
||||
: undefined
|
||||
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
|
||||
|
||||
const onEditMessage = useCallback(
|
||||
(msg: Message) => {
|
||||
setMessage(msg)
|
||||
const messages = onGetMessages?.().map((m) => (m.id === message.id ? message : m))
|
||||
const messages = onGetMessages?.()?.map((m) => (m.id === message.id ? msg : m))
|
||||
messages && onSetMessages?.(messages)
|
||||
topic && db.topics.update(topic.id, { messages })
|
||||
},
|
||||
[message, onGetMessages, onSetMessages, topic]
|
||||
[message.id, onGetMessages, onSetMessages, topic]
|
||||
)
|
||||
|
||||
const messageHighlightHandler = (highlight: boolean = true) => {
|
||||
if (messageContainerRef.current) {
|
||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
if (highlight) {
|
||||
setTimeout(() => {
|
||||
const classList = messageContainerRef.current?.classList
|
||||
classList?.add('message-highlight')
|
||||
setTimeout(() => classList?.remove('message-highlight'), 2500)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, (highlight: boolean = true) => {
|
||||
if (messageContainerRef.current) {
|
||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
if (highlight) {
|
||||
setTimeout(() => {
|
||||
const classList = messageContainerRef.current?.classList
|
||||
classList?.add('message-highlight')
|
||||
setTimeout(() => classList?.remove('message-highlight'), 2500)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [message])
|
||||
|
||||
@ -104,11 +104,16 @@ const MessageItem: FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (topic && onGetMessages && onSetMessages) {
|
||||
if (message.status === 'sending' && index === 0) {
|
||||
if (message.status === 'sending') {
|
||||
const messages = onGetMessages()
|
||||
fetchChatCompletion({
|
||||
message,
|
||||
messages: messages.filter((m) => !m.status.includes('ing')),
|
||||
messages: messages
|
||||
.filter((m) => !m.status.includes('ing'))
|
||||
.slice(
|
||||
0,
|
||||
messages.findIndex((m) => m.id === message.id)
|
||||
),
|
||||
assistant,
|
||||
topic,
|
||||
onResponse: (msg) => {
|
||||
@ -123,7 +128,7 @@ const MessageItem: FC<Props> = ({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [message.status])
|
||||
|
||||
if (hidePresetMessages && message.isPreset) {
|
||||
return null
|
||||
@ -147,11 +152,13 @@ const MessageItem: FC<Props> = ({
|
||||
})}
|
||||
ref={messageContainerRef}
|
||||
style={isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : undefined}>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} />
|
||||
<MessageHeader message={message} assistant={assistant} model={model} key={message.modelId} />
|
||||
<MessageContentContainer
|
||||
className="message-content-container"
|
||||
style={{ fontFamily, fontSize, background: messageBackground }}>
|
||||
<MessageContent message={message} model={model} />
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} model={model} />
|
||||
</MessageErrorBoundary>
|
||||
{showMenubar && (
|
||||
<MessageFooter
|
||||
style={{
|
||||
@ -161,6 +168,7 @@ const MessageItem: FC<Props> = ({
|
||||
<MessageTokens message={message} isLastMessage={isLastMessage} />
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
assistantModel={assistant?.model}
|
||||
model={model}
|
||||
index={index}
|
||||
isLastMessage={isLastMessage}
|
||||
@ -216,7 +224,7 @@ const MessageFooter = styled.div`
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
margin-top: 2px;
|
||||
border-top: 0.5px dashed var(--color-border);
|
||||
border-top: 1px dotted var(--color-border);
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ const MessageAttachments: FC<Props> = ({ message }) => {
|
||||
|
||||
if (message?.files && message.files[0]?.type === FileTypes.IMAGE) {
|
||||
return (
|
||||
<Container>
|
||||
<Container style={{ marginBottom: 8 }}>
|
||||
{message.files?.map((image) => <Image src={FileManager.getFileUrl(image)} key={image.id} width="33%" />)}
|
||||
</Container>
|
||||
)
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { SyncOutlined } from '@ant-design/icons'
|
||||
import { SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { getBriefInfo } from '@renderer/utils'
|
||||
import { Divider } from 'antd'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Markdown from '../Markdown/Markdown'
|
||||
@ -12,6 +15,8 @@ const MessageContent: React.FC<{
|
||||
message: Message
|
||||
model?: Model
|
||||
}> = ({ message, model }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (message.status === 'sending') {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
@ -32,6 +37,18 @@ const MessageContent: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<Markdown message={message} />
|
||||
{message.translatedContent && (
|
||||
<>
|
||||
<Divider style={{ margin: 0, marginBottom: 10 }}>
|
||||
<TranslationOutlined />
|
||||
</Divider>
|
||||
{message.translatedContent === t('translate.processing') ? (
|
||||
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginBottom: 15 }} />
|
||||
) : (
|
||||
<Markdown message={{ ...message, content: message.translatedContent }} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<MessageAttachments message={message} />
|
||||
</>
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user