Compare commits

...

40 Commits

Author SHA1 Message Date
kangfenmao
68d57ba238 chore(version): 0.7.12 2024-10-06 10:12:46 +08:00
kangfenmao
cf98675223 wip 2024-10-05 19:02:56 +08:00
kangfenmao
4cc140e4f2 feat: add topics history 2024-10-05 17:52:18 +08:00
亢奋猫
2da3a3f010 Update README.md 2024-09-30 22:42:06 +08:00
kangfenmao
fa6f7ecab0 chore(version): 0.7.11 2024-09-30 22:33:58 +08:00
kangfenmao
31ab444300 fix: together ai models 2024-09-30 20:45:51 +08:00
kangfenmao
85453f5a3a fix: webdav backup path 2024-09-30 18:15:15 +08:00
kangfenmao
6d92539524 fix: merge migration versions 2024-09-30 13:37:55 +08:00
牡丹凤凰
a605ae6043 新增:模型服务商together (#148)
* 新增:模型服务商together

新增:模型服务商together
修复:providers为null或undefined时会抛出错误。

* 新增服务商:fireworks、360智脑、英伟达

* 新增:若干模型头像

* 谷歌其他系列模型匹配头像

* 1

* version+
2024-09-30 13:30:09 +08:00
kangfenmao
6aaa6bf042 chore(version): 0.7.10 2024-09-29 23:29:28 +08:00
kangfenmao
aa578194c7 fix: add markdown rendering input msg switcher #143 #142 2024-09-29 23:21:31 +08:00
kangfenmao
220600070c fix: paste long text issue 2024-09-29 22:37:33 +08:00
kangfenmao
32cdfbbfb0 feat: add assistant setting popup 2024-09-29 22:31:07 +08:00
kangfenmao
33b83bf242 feat: add webdav settings component and backup user data files #69 2024-09-29 16:44:18 +08:00
dray
2e1b433365 feat: 添加 WebDAV 配置项
为应用程序添加了 WebDAV 配置项,包括主机、用户、密码和路径。这样用户就可以将备份文件定时上传到 WebDAV 服务器,并从 WebDAV 服务器恢复备份文件。

- 添加了新的依赖项 "webdav": "^5.7.1"
- 修改了 package.json 文件
- 修改了 zh-tw.json、zh-cn.json 和 en-us.json 文件
- 修改了 settings.ts 文件
- 修改了 GeneralSettings.tsx 文件

https://github.com/kangfenmao/cherry-studio/issues/129
2024-09-29 09:27:42 +08:00
kangfenmao
2771a842fe fix: context count 2024-09-28 22:01:09 +08:00
牡丹凤凰
4af3d16e61 Merge pull request #137 from 1355873789/develop
add new app(Felo)
2024-09-28 17:58:00 +08:00
1355873789
eb47fb051b add new app(Felo) 2024-09-28 17:58:30 +08:00
牡丹凤凰
0f9655611b Update models.ts
fix: Confusion between Minimax and Hailuo logos
2024-09-28 11:21:01 +08:00
kangfenmao
0c72ccac12 chore(version): 0.7.9 2024-09-28 00:45:05 +08:00
kangfenmao
09f7fcd2b4 fix: about page minapp logo 2024-09-27 22:44:45 +08:00
kangfenmao
b9250df347 feat: backup all files
1. remove window.api.compress window.api.decompress
2024-09-27 22:35:22 +08:00
kangfenmao
ca897db0d2 fix: minimax hailuo logo 2024-09-27 14:14:10 +08:00
牡丹凤凰
af75d4139c fix: correct display for non-vision GPT-4 models (#135)
* Update models.ts

feat: add matching rules for EMBEDDING_REGEX
fix: correct display for non-vision gpt-4 models

* Update models.ts

feat:add matching rules for gpt-4

* Update models.ts

feat:add matching rules for gpt-4

* Update models.ts

feat:add matching rules for gpt-4
2024-09-27 11:47:03 +08:00
kangfenmao
d2e35a888d refactor: MessageContent component 2024-09-27 00:25:45 +08:00
kangfenmao
fb56c3744b docs: add dev docs 2024-09-27 00:13:24 +08:00
drfyup
26942cfd1f doc: add dev docs (#133)
* Create PR_FAQ.md

* Create Code_DSC.md
2024-09-27 00:04:46 +08:00
kangfenmao
1601fc6d81 fix: 在提问时携带图片会卡住软件 #108 2024-09-27 00:01:35 +08:00
kangfenmao
f543a9ff80 fix: Assistant 的 Prompt 过长时会超出组件 #95
close #95
2024-09-26 23:29:04 +08:00
kangfenmao
5299a2a687 feat: check update settings #131
close #131
2024-09-26 23:17:21 +08:00
kangfenmao
fcc627db6f feat: edit message 2024-09-26 22:45:59 +08:00
kangfenmao
1035019fc2 feat: translate settings persist 2024-09-26 19:15:26 +08:00
kangfenmao
9d311a7261 fix: filter unsupported models 2024-09-26 15:12:47 +08:00
kangfenmao
a973c5fb89 fix: remove filter messages 2024-09-26 14:55:09 +08:00
亢奋猫
be081ccf7a docs: new screenshot 2024-09-26 14:10:14 +08:00
kangfenmao
c25db02acf docs: add sponsor 2024-09-26 13:55:30 +08:00
1355873789
01f98235c6 更新智谱清言APP logo 2024-09-26 08:40:07 +08:00
1355873789
00f3b87215 繁体中文支持 2024-09-26 08:40:07 +08:00
1355873789
849958eeec 模型头像相关
修正部分模型头像错误
新增部分模型头像
2024-09-26 08:40:07 +08:00
牡丹凤凰
9655153e01 Update providers.ts 2024-09-26 03:10:44 +08:00
124 changed files with 4507 additions and 1265 deletions

View File

@@ -2,7 +2,7 @@
<a href="https://github.com/kangfenmao/cherry-studio/releases">
<img src="https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505" alt="banner" />
</a>
English | <a href="./docs/README_zh.md">中文</a>
English | <a href="./docs/README.zh.md">中文</a>
</div>
# 🍒 Cherry Studio
@@ -11,11 +11,9 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
# 🌠 Screenshot
![](https://github.com/user-attachments/assets/e24d1e7d-126a-4647-bd98-f470bfe26fde)
![](https://github.com/user-attachments/assets/3f3f0bfa-cb88-4abf-923a-a0859fa3c912)
![](https://github.com/user-attachments/assets/288560c1-d218-437c-87c2-2a5e87b43b93)
![](https://github.com/user-attachments/assets/28585d83-4bf0-4714-b561-8c7bf57cc600)
![](https://github.com/user-attachments/assets/8576863a-f632-4776-bc12-657eeced9da3)
![](https://github.com/user-attachments/assets/790790d7-b462-48dd-bde1-91c1697a4648)
# 🌟 Features
@@ -70,6 +68,10 @@ $ yarn build:linux
<img src="https://contrib.rocks/image?repo=kangfenmao/cherry-studio" />
</a>
# Sponsor
[Buy Me a Coffee](docs/sponsor.md)
# 📃 License
[LICENSE](./LICENSE)

View File

@@ -87,6 +87,10 @@ $ yarn build:linux
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
# 赞助
[微信赞赏码](docs/sponsor.md)
# 📃 许可证
[LICENSE](./LICENSE)

5
docs/Sponsor.md Normal file
View File

@@ -0,0 +1,5 @@
# Sponsor
<div align="center">
<img src="https://github.com/user-attachments/assets/4665f07f-5ecc-4bd8-8727-ae00f35d6d98" alt="Buy Me a Coffee" width="280"/>
</div>

95
docs/dev/FAQ.md Normal file
View File

@@ -0,0 +1,95 @@
# FAQ 文档
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
## 问题1Cherry Studio 支持哪些操作系统?
- **答案**Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
## 问题2Cherry Studio 的主要功能有哪些?
- **答案**Cherry Studio 的主要功能包括:
1. 支持多个 LLM 提供商
2. 允许创建多个助手
3. 支持创建多个主题
4. 允许在同一对话中使用多个模型来回答问题
5. 支持拖放排序
6. 代码高亮
7. Mermaid 图表支持
## 问题3Cherry Studio 的主要目录结构是怎样的?
- **答案**Cherry Studio 的主要目录结构如下:
- `/src`: 主要源代码目录
- `/build`: 构建相关文件
- `/docs`: 文档目录
- `/resources`: 资源文件目录
- `/scripts`: 脚本文件目录
## 问题4如何在 Windows 环境下 fork Cherry Studio 并修改部分功能?
- **答案**:在 Windows 环境下 fork Cherry Studio 并修改部分功能的步骤如下:
1. 在 GitHub 上 fork Cherry Studio 仓库
2. 克隆 fork 的仓库到本地:`git clone https://github.com/your-username/cherry-studio.git`
3. 进入项目目录:`cd cherry-studio`
4. 安装依赖:`yarn install`
5. 修改所需的功能代码
6. 测试修改:`yarn dev`
7. 提交修改:`git add .``git commit -m "描述你的修改"`
8. 推送到你的 fork 仓库:`git push origin main`
## 问题5Cherry Studio 使用了哪些主要技术栈?
- **答案**Cherry Studio 主要使用了以下技术栈:
- TypeScript
- SCSS
- Electron
- Vite
- Sequelize
## 问题6如何贡献代码到 Cherry Studio 项目?
- **答案**:贡献代码到 Cherry Studio 项目的步骤如下:
1. Fork 项目仓库
2. 创建你的特性分支:`git checkout -b feature/AmazingFeature`
3. 提交你的修改:`git commit -m 'Add some AmazingFeature'`
4. 推送到分支:`git push origin feature/AmazingFeature`
5. 打开一个 Pull Request
## 问题7Cherry Studio 的 `/src` 目录主要包含哪些内容?
- **答案**Cherry Studio 的 `/src` 目录主要包含以下内容:
- 主进程代码Electron 主进程)
- 渲染进程代码(用户界面)
- 组件
- 工具函数
- 状态管理
- 样式文件
## 问题8如何在 Cherry Studio 中添加新的 LLM 提供商?
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
1.`/src/services` 或类似目录下创建新的服务文件
2. 实现与新 LLM 提供商 API 的集成
3. 在用户界面中添加新提供商的选项
4. 更新配置和状态管理以支持新提供商
## 问题9Cherry Studio 的构建过程是怎样的?
- **答案**Cherry Studio 的构建过程主要包括:
1. 使用 Vite 构建前端资源
2. 使用 Electron Builder 打包桌面应用
3. 根据不同平台Windows、Mac、Linux生成相应的安装包
## 问题10如何在 Cherry Studio 中实现新的 UI 主题?
- **答案**:在 Cherry Studio 中实现新的 UI 主题的步骤:
1.`/src/styles` 目录下创建新的主题 SCSS 文件
2. 定义新主题的颜色变量和样式
3. 在主样式文件中导入新主题
4. 更新主题切换逻辑以包含新主题
5. 在用户界面中添加新主题的选项
## 问题11Cherry Studio 如何处理多语言支持?
- **答案**Cherry Studio 可能通过以下方式处理多语言支持:
1. 使用 i18n 库进行国际化
2.`/src/locales` 或类似目录下存储不同语言的翻译文件
3. 实现语言切换功能
4. 在组件中使用翻译函数或组件来显示多语言文本
## 问题12如何为 Cherry Studio 编写单元测试?
- **答案**:为 Cherry Studio 编写单元测试的步骤:
1.`/tests` 目录下创建测试文件
2. 使用测试框架(如 Jest编写测试用例
3. 模拟 Electron 环境和其他依赖
4. 运行测试命令:`yarn test`
5. 确保测试覆盖主要功能和组件

95
docs/dev/faq.md Normal file
View File

@@ -0,0 +1,95 @@
# FAQ 文档
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
## 问题1Cherry Studio 支持哪些操作系统?
- **答案**Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
## 问题2Cherry Studio 的主要功能有哪些?
- **答案**Cherry Studio 的主要功能包括:
1. 支持多个 LLM 提供商
2. 允许创建多个助手
3. 支持创建多个主题
4. 允许在同一对话中使用多个模型来回答问题
5. 支持拖放排序
6. 代码高亮
7. Mermaid 图表支持
## 问题3Cherry Studio 的主要目录结构是怎样的?
- **答案**Cherry Studio 的主要目录结构如下:
- `/src`: 主要源代码目录
- `/build`: 构建相关文件
- `/docs`: 文档目录
- `/resources`: 资源文件目录
- `/scripts`: 脚本文件目录
## 问题4如何在 Windows 环境下 fork Cherry Studio 并修改部分功能?
- **答案**:在 Windows 环境下 fork Cherry Studio 并修改部分功能的步骤如下:
1. 在 GitHub 上 fork Cherry Studio 仓库
2. 克隆 fork 的仓库到本地:`git clone https://github.com/your-username/cherry-studio.git`
3. 进入项目目录:`cd cherry-studio`
4. 安装依赖:`yarn install`
5. 修改所需的功能代码
6. 测试修改:`yarn dev`
7. 提交修改:`git add .``git commit -m "描述你的修改"`
8. 推送到你的 fork 仓库:`git push origin main`
## 问题5Cherry Studio 使用了哪些主要技术栈?
- **答案**Cherry Studio 主要使用了以下技术栈:
- TypeScript
- SCSS
- Electron
- Vite
- Sequelize
## 问题6如何贡献代码到 Cherry Studio 项目?
- **答案**:贡献代码到 Cherry Studio 项目的步骤如下:
1. Fork 项目仓库
2. 创建你的特性分支:`git checkout -b feature/AmazingFeature`
3. 提交你的修改:`git commit -m 'Add some AmazingFeature'`
4. 推送到分支:`git push origin feature/AmazingFeature`
5. 打开一个 Pull Request
## 问题7Cherry Studio 的 `/src` 目录主要包含哪些内容?
- **答案**Cherry Studio 的 `/src` 目录主要包含以下内容:
- 主进程代码Electron 主进程)
- 渲染进程代码(用户界面)
- 组件
- 工具函数
- 状态管理
- 样式文件
## 问题8如何在 Cherry Studio 中添加新的 LLM 提供商?
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
1.`/src/services` 或类似目录下创建新的服务文件
2. 实现与新 LLM 提供商 API 的集成
3. 在用户界面中添加新提供商的选项
4. 更新配置和状态管理以支持新提供商
## 问题9Cherry Studio 的构建过程是怎样的?
- **答案**Cherry Studio 的构建过程主要包括:
1. 使用 Vite 构建前端资源
2. 使用 Electron Builder 打包桌面应用
3. 根据不同平台Windows、Mac、Linux生成相应的安装包
## 问题10如何在 Cherry Studio 中实现新的 UI 主题?
- **答案**:在 Cherry Studio 中实现新的 UI 主题的步骤:
1.`/src/styles` 目录下创建新的主题 SCSS 文件
2. 定义新主题的颜色变量和样式
3. 在主样式文件中导入新主题
4. 更新主题切换逻辑以包含新主题
5. 在用户界面中添加新主题的选项
## 问题11Cherry Studio 如何处理多语言支持?
- **答案**Cherry Studio 可能通过以下方式处理多语言支持:
1. 使用 i18n 库进行国际化
2.`/src/locales` 或类似目录下存储不同语言的翻译文件
3. 实现语言切换功能
4. 在组件中使用翻译函数或组件来显示多语言文本
## 问题12如何为 Cherry Studio 编写单元测试?
- **答案**:为 Cherry Studio 编写单元测试的步骤:
1.`/tests` 目录下创建测试文件
2. 使用测试框架(如 Jest编写测试用例
3. 模拟 Electron 环境和其他依赖
4. 运行测试命令:`yarn test`
5. 确保测试覆盖主要功能和组件

72
docs/dev/structure.md Normal file
View File

@@ -0,0 +1,72 @@
## Cherry Studio目录结构和功能
### 1. `/src`: 主要源代码目录
- ** `/main`**: Electron主进程相关代码
- 负责应用的生命周期管理、窗口创建、IPC通信等
- ** `/renderer`**: Electron渲染进程相关代码
- 包含用户界面的实现使用TypeScript和SCSS
- ** `/preload`**: 预加载脚本
- 用于在渲染进程中安全地暴露主进程功能
- ** `/components`**: React组件
- 可复用的UI组件如对话框、输入框等
- ** `/pages`**: 应用的主要页面
- 如聊天界面、设置页面等
- ** `/store`**: 状态管理
- 可能使用Redux或MobX来管理应用状态
- ** `/utils`**: 工具函数
- 包含各种辅助函数和工具类
- ** `/styles`**: 全局样式文件
- 包含SCSS文件定义全局样式和主题
### 2. `/public`: 静态资源目录
- 包含图标、字体等静态文件
### 3. `/electron`: Electron相关配置
- 包含Electron的构建和打包配置
### 4. `/scripts`: 构建和开发脚本
- 包含npm脚本用于开发、构建和部署
### 5. `/types`: TypeScript类型定义
- 包含自定义的类型定义文件
### 6. `/tests`: 测试文件目录
- 包含单元测试和集成测试
### 7. `/docs`: 文档目录
- 包含项目文档、API文档等
### 8. `/config`: 配置文件目录
- 包含各种配置文件如webpack配置、环境变量等
### 9. `/migrations`: 数据库迁移文件
- 由于使用了Sequelize这里可能包含数据库结构的变更记录
### 10. `/models`: 数据模型
- 定义Sequelize的数据模型对应数据库表结构
## 主要功能实现
### 1. LLM提供商集成
- 可能在`/src/utils``/src/services`中实现与不同LLM API的集成
### 2. 多助手和多主题支持
-`/src/store`中管理助手和主题的状态
-`/src/components`中实现相关的UI组件
### 3. 多模型对话
-`/src/pages`的聊天界面中实现
- 可能使用`/src/store`来管理对话状态
### 4. 拖放排序
-`/src/components`中实现相关的可拖拽组件
### 5. 代码高亮
- 可能使用第三方库如Prism.js集成在`/src/components`
### 6. Mermaid图表支持
-`/src/components`中集成Mermaid库
### 7. 数据持久化
- 使用Sequelize在`/models`中定义数据模型
-`/migrations`中管理数据库结构变更

5
docs/sponsor.md Normal file
View File

@@ -0,0 +1,5 @@
# Sponsor
<div align="center">
<img src="https://github.com/user-attachments/assets/4665f07f-5ecc-4bd8-8727-ae00f35d6d98" alt="Buy Me a Coffee" width="280"/>
</div>

View File

@@ -65,15 +65,9 @@ afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
本次更新:
增加流式输出开关
Windows 安装程序支持修改安装位置了
服务商和小程序图标更新
增加 ocoolAI 服务商
小程序增加 HuggingChat
Gemini 模型回复安全级别关闭
修复 macOS 切换窗口透明不生效问题
修复消息回复完成界面会自动滚动到最底部的问题
增加话题历史记录
增加消息搜索功能
近期更新:
全新应用图标
模型图标更新
支持 Linux ARM 架构
增加 WebDAV 备份功能 by @DrayChou
增加使用 Markdown 渲染用户消息开关
增加 Felo 小程序

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.7.8",
"version": "0.7.12",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -32,11 +32,15 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"archiver": "^7.0.1",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3",
"html2canvas": "^1.4.1"
"fs-extra": "^11.2.0",
"html2canvas": "^1.4.1",
"unzipper": "^0.12.3",
"webdav": "4.11.4"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.24.3",
@@ -47,11 +51,13 @@
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
"@types/node": "^18.19.9",
"@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",

View File

@@ -1,13 +1,13 @@
import { FileType } from '@types'
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import FileManager from './services/FileManager'
import { compress, decompress } from './utils/zip'
import { createMinappWindow } from './window'
const fileManager = new FileManager()
const backupManager = new BackupManager()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow)
@@ -29,24 +29,24 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('reload', () => mainWindow.reload())
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
ipcMain.handle('backup:backup', backupManager.backup)
ipcMain.handle('backup:restore', backupManager.restore)
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile)
ipcMain.handle('file:upload', fileManager.uploadFile)
ipcMain.handle('file:clear', fileManager.clear)
ipcMain.handle('file:read', fileManager.readFile)
ipcMain.handle('file:delete', fileManager.deleteFile)
ipcMain.handle('file:get', fileManager.getFile)
ipcMain.handle('file:selectFolder', fileManager.selectFolder)
ipcMain.handle('file:create', fileManager.createTempFile)
ipcMain.handle('file:write', fileManager.writeFile)
ipcMain.handle('file:saveImage', fileManager.saveImage)
ipcMain.handle('file:base64Image', async (_, id) => await fileManager.base64Image(id))
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
ipcMain.handle('file:clear', async () => await fileManager.clear())
ipcMain.handle('file:read', async (_, id: string) => await fileManager.readFile(id))
ipcMain.handle('file:delete', async (_, id: string) => await fileManager.deleteFile(id))
ipcMain.handle('file:get', async (_, filePath: string) => await fileManager.getFile(filePath))
ipcMain.handle('file:create', async (_, fileName: string) => await fileManager.createTempFile(fileName))
ipcMain.handle(
'file:write',
async (_, filePath: string, data: Uint8Array | string) => await fileManager.writeFile(filePath, data)
)
ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('minapp', (_, args) => {
createMinappWindow({

View File

@@ -0,0 +1,110 @@
import { WebDavConfig } from '@types'
import archiver from 'archiver'
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'
class BackupManager {
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
private backupDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup')
constructor() {
this.backup = this.backup.bind(this)
this.restore = this.restore.bind(this)
this.backupToWebdav = this.backupToWebdav.bind(this)
this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
}
async backup(
_: Electron.IpcMainInvokeEvent,
fileName: string,
data: string,
destinationPath: string = this.backupDir
): Promise<string> {
try {
// 创建临时目录
await fs.ensureDir(this.tempDir)
// 将 data 写入临时文件
const tempDataPath = path.join(this.tempDir, 'data.json')
await fs.writeFile(tempDataPath, data)
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
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()
// 清理临时目录
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)
throw error
}
}
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
// 创建临时目录
await fs.ensureDir(this.tempDir)
// 解压备份文件到临时目录
await fs
.createReadStream(backupPath)
.pipe(unzipper.Extract({ path: this.tempDir }))
.promise()
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
// 恢复 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)
// 清理临时目录
await fs.remove(this.tempDir)
Logger.log('Restore completed successfully')
return data
}
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data)
const webdavClient = new WebDav(webdavConfig)
return await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true
})
}
async restoreFromWebdav(_: Electron.IpcMainInvokeEvent, webdavConfig: WebDavConfig) {
const filename = 'cherry-studio.backup.zip'
const webdavClient = new WebDav(webdavConfig)
const retrievedFile = await webdavClient.getFileContents(filename)
const backupedFilePath = path.join(this.backupDir, filename)
await fs.writeFileSync(backupedFilePath, retrievedFile as Buffer)
return await this.restore(_, backupedFilePath)
}
}
export default BackupManager

View File

@@ -17,20 +17,19 @@ import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
class FileManager {
private storageDir: string
private storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
constructor() {
this.storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
this.initStorageDir()
}
private initStorageDir(): void {
private initStorageDir = (): void => {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
}
private async getFileHash(filePath: string): Promise<string> {
private getFileHash = async (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5')
const stream = fs.createReadStream(filePath)
@@ -40,7 +39,7 @@ class FileManager {
})
}
async findDuplicateFile(filePath: string): Promise<FileType | null> {
findDuplicateFile = async (filePath: string): Promise<FileType | null> => {
const stats = fs.statSync(filePath)
const fileSize = stats.size
@@ -76,7 +75,10 @@ class FileManager {
return null
}
async selectFile(options?: OpenDialogOptions): Promise<FileType[] | null> {
public selectFile = async (
_: Electron.IpcMainInvokeEvent,
options?: OpenDialogOptions
): Promise<FileType[] | null> => {
const defaultOptions: OpenDialogOptions = {
properties: ['openFile']
}
@@ -110,7 +112,7 @@ class FileManager {
return Promise.all(fileMetadataPromises)
}
async uploadFile(file: FileType): Promise<FileType> {
public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise<FileType> => {
const duplicateFile = await this.findDuplicateFile(file.path)
if (duplicateFile) {
@@ -141,7 +143,7 @@ class FileManager {
return fileMetadata
}
async getFile(filePath: string): Promise<FileType | null> {
public getFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<FileType | null> => {
if (!fs.existsSync(filePath)) {
return null
}
@@ -165,16 +167,16 @@ class FileManager {
return fileInfo
}
async deleteFile(id: string): Promise<void> {
public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
await fs.promises.unlink(path.join(this.storageDir, id))
}
async readFile(id: string): Promise<string> {
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => {
const filePath = path.join(this.storageDir, id)
return fs.readFileSync(filePath, 'utf8')
}
async createTempFile(fileName: string): Promise<string> {
public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {
const tempDir = path.join(app.getPath('temp'), 'CherryStudio')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
@@ -183,11 +185,18 @@ class FileManager {
return tempFilePath
}
async writeFile(filePath: string, data: Uint8Array | string): Promise<void> {
public writeFile = async (
_: Electron.IpcMainInvokeEvent,
filePath: string,
data: Uint8Array | string
): Promise<void> => {
await fs.promises.writeFile(filePath, data)
}
async base64Image(id: string): Promise<{ mime: string; base64: string; data: string }> {
public base64Image = async (
_: Electron.IpcMainInvokeEvent,
id: string
): Promise<{ mime: string; base64: string; data: string }> => {
const filePath = path.join(this.storageDir, id)
const data = await fs.promises.readFile(filePath)
const base64 = data.toString('base64')
@@ -199,15 +208,15 @@ class FileManager {
}
}
async clear(): Promise<void> {
public clear = async (): Promise<void> => {
await fs.promises.rmdir(this.storageDir, { recursive: true })
await this.initStorageDir()
}
async open(
public open = async (
_: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions
): Promise<{ fileName: string; content: Buffer } | null> {
): Promise<{ fileName: string; filePath: string; content: Buffer } | null> => {
try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '打开文件',
@@ -220,7 +229,7 @@ class FileManager {
const filePath = result.filePaths[0]
const fileName = filePath.split('/').pop() || ''
const content = await readFile(filePath)
return { fileName, content }
return { fileName, filePath, content }
}
return null
@@ -230,12 +239,12 @@ class FileManager {
}
}
async save(
public save = async (
_: Electron.IpcMainInvokeEvent,
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<void> {
): Promise<void> => {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
@@ -251,7 +260,7 @@ class FileManager {
}
}
async saveImage(_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise<void> {
public saveImage = async (_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise<void> => {
try {
const filePath = dialog.showSaveDialogSync({
defaultPath: `${name}.png`,
@@ -266,6 +275,25 @@ class FileManager {
logger.error('[IPC - Error]', 'An error occurred saving the image:', error)
}
}
public selectFolder = async (_: Electron.IpcMainInvokeEvent, options: OpenDialogOptions): Promise<string | null> => {
try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '选择文件夹',
properties: ['openDirectory'],
...options
})
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]
}
return null
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred selecting the folder:', err)
return null
}
}
}
export default FileManager

View File

@@ -0,0 +1,66 @@
import { WebDavConfig } from '@types'
import Logger from 'electron-log'
import Stream from 'stream'
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
export default class WebDav {
public instance: WebDAVClient | undefined
private webdavPath: string
constructor(params: WebDavConfig) {
this.webdavPath = params.webdavPath
this.instance = createClient(params.webdavHost, {
username: params.webdavUser,
password: params.webdavPass
})
this.putFileContents = this.putFileContents.bind(this)
this.getFileContents = this.getFileContents.bind(this)
}
public putFileContents = async (
filename: string,
data: string | BufferLike | Stream.Readable,
options?: PutFileContentsOptions
) => {
if (!this.instance) {
return new Error('WebDAV client not initialized')
}
try {
if (!(await this.instance.exists(this.webdavPath))) {
await this.instance.createDirectory(this.webdavPath, {
recursive: true
})
}
} catch (error) {
Logger.error('[WebDAV] Error creating directory on WebDAV:', error)
throw error
}
const remoteFilePath = `${this.webdavPath}/${filename}`
try {
return await this.instance.putFileContents(remoteFilePath, data, options)
} catch (error) {
Logger.error('[WebDAV] Error putting file contents on WebDAV:', error)
throw error
}
}
public getFileContents = async (filename: string, options?: GetFileContentsOptions) => {
if (!this.instance) {
throw new Error('WebDAV client not initialized')
}
const remoteFilePath = `${this.webdavPath}/${filename}`
try {
return await this.instance.getFileContents(remoteFilePath, options)
} catch (error) {
Logger.error('[WebDAV] Error getting file contents on WebDAV:', error)
throw error
}
}
}

View File

@@ -1,39 +0,0 @@
import util from 'node:util'
import zlib from 'node:zlib'
import logger from 'electron-log'
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
const gzipPromise = util.promisify(zlib.gzip)
const gunzipPromise = util.promisify(zlib.gunzip)
/**
* 压缩字符串
* @param {string} string - 要压缩的 JSON 字符串
* @returns {Promise<Buffer>} 压缩后的 Buffer
*/
export async function compress(str) {
try {
const buffer = Buffer.from(str, 'utf-8')
const compressedBuffer = await gzipPromise(buffer)
return compressedBuffer
} catch (error) {
logger.error('Compression failed:', error)
throw error
}
}
/**
* 解压缩 Buffer 到 JSON 字符串
* @param {Buffer} compressedBuffer - 压缩的 Buffer
* @returns {Promise<string>} 解压缩后的 JSON 字符串
*/
export async function decompress(compressedBuffer) {
try {
const buffer = await gunzipPromise(compressedBuffer)
return buffer.toString('utf-8')
} catch (error) {
logger.error('Decompression failed:', error)
throw error
}
}

View File

@@ -1,6 +1,8 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types'
import type { OpenDialogOptions } from 'electron'
import { Readable } from 'stream'
declare global {
interface Window {
@@ -19,19 +21,26 @@ declare global {
reload: () => void
compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string>
backup: {
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
restore: (backupPath: string) => Promise<string>
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
}
file: {
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
upload: (file: FileType) => Promise<FileType>
delete: (fileId: string) => Promise<void>
read: (fileId: string) => Promise<string>
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
clear: () => Promise<void>
get: (filePath: string) => Promise<FileType | null>
selectFolder: () => Promise<string | null>
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
saveImage: (name: string, data: string) => void
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
}
}
}

View File

@@ -1,4 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload'
import { WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer
@@ -10,23 +11,29 @@ const api = {
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
reload: () => ipcRenderer.invoke('reload'),
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
backup: {
backup: (fileName: string, data: string, destinationPath?: string) =>
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke('backup:backupToWebdav', data, webdavConfig),
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig)
},
file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
read: (fileId: string) => ipcRenderer.invoke('file:read', fileId),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
clear: () => ipcRenderer.invoke('file:clear'),
get: (filePath: string) => ipcRenderer.invoke('file:get', filePath),
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
save: (path: string, content: string, options?: { compress: boolean }) => {
return ipcRenderer.invoke('file:save', path, content, options)
},
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data)
save: (path: string, content: string, options?: { compress: boolean }) =>
ipcRenderer.invoke('file:save', path, content, options),
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId)
}
}

View File

@@ -12,6 +12,7 @@ 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 SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
@@ -31,6 +32,7 @@ function App(): JSX.Element {
<Route path="/agents" element={<AgentsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/messages/*" element={<HistoryPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -41,6 +41,9 @@
}
.segmented-tab {
.ant-segmented-item {
overflow: hidden;
}
.ant-segmented-item-selected {
background-color: var(--color-background-mute);
}

View File

@@ -0,0 +1,187 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Col, Row, Slider, Switch, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
}
const AssistantModelSettings: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const { t } = useTranslation()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings({
temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens,
streamOutput: settings.streamOutput ?? streamOutput
})
}
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
onUpdateAssistantSettings({ maxTokens: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS,
streamOutput: true
}
})
}
useEffect(() => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
setStreamOutput(assistant?.settings?.streamOutput ?? true)
}, [assistant])
return (
<Container>
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={20}
onChange={setConextCount}
onChangeComplete={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<Row align="middle" justify="space-between">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
disabled={!enableMaxTokens}
min={0}
max={32000}
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
</Row>
<SettingRow>
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
<Switch
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
onUpdateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<HStack
justifyContent="flex-end"
style={{ marginTop: 20, padding: '10px 0', borderTop: '0.5px solid var(--color-border)' }}>
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
{t('chat.settings.reset')}
</Button>
</HStack>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding-bottom: 10px;
`
const Label = styled.p`
margin: 0;
margin-right: 5px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 12px;
cursor: pointer;
color: var(--color-text-3);
`
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export default AssistantModelSettings

View File

@@ -0,0 +1,47 @@
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box, VStack } from '../Layout'
const AssistantPromptSettings: React.FC<{ assistant: Assistant }> = (props) => {
const { assistant, updateAssistant } = useAssistant(props.assistant.id)
const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt)
const { t } = useTranslation()
const onUpdate = () => {
const _assistant = { ...assistant, name, prompt }
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
return (
<VStack flex={1}>
<Box mb={8}>{t('common.name')}</Box>
<Input
placeholder={t('common.assistant') + t('common.name')}
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={onUpdate}
/>
<Box mt={8} mb={8}>
{t('common.prompt')}
</Box>
<TextArea
rows={10}
placeholder={t('common.assistant') + t('common.prompt')}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onBlur={onUpdate}
style={{ minHeight: 'calc(80vh - 150px)', maxHeight: 'calc(80vh - 150px)' }}
/>
</VStack>
)
}
export default AssistantPromptSettings

View File

@@ -0,0 +1,154 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { Assistant } from '@renderer/types'
import { Menu, Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { HStack } from '../Layout'
import { TopView } from '../TopView'
import AssistantModelSettings from './AssistantModelSettings'
import AssistantPromptSettings from './AssistantPromptSettings'
interface AssistantSettingPopupShowParams {
assistant: Assistant
}
interface Props extends AssistantSettingPopupShowParams {
resolve: (assistant: Assistant) => void
}
const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [menu, setMenu] = useState('prompt')
const { theme } = useTheme()
const onOk = () => {
setOpen(false)
}
const handleCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(assistant)
}
const items = [
{
key: 'prompt',
label: t('assistants.prompt_settings')
},
{
key: 'model',
label: t('assistants.model_settings')
}
]
return (
<StyledModal
open={open}
onOk={onOk}
onCancel={handleCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
footer={null}
title={assistant.name}
styles={{
content: {
padding: 0,
overflow: 'hidden',
border: '1px solid var(--color-border)',
background: 'var(--color-background)'
},
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0 },
mask: { background: theme === 'light' ? 'rgba(255,255,255, 0.8)' : 'rgba(0,0,0, 0.8)' }
}}
width="70vw"
height="80vh"
centered>
<HStack>
<LeftMenu>
<Menu
style={{ width: 220, padding: 5, background: 'transparent' }}
defaultSelectedKeys={['prompt']}
mode="vertical"
items={items}
onSelect={({ key }) => setMenu(key as string)}
/>
</LeftMenu>
<Settings>
{menu === 'prompt' && <AssistantPromptSettings assistant={assistant} />}
{menu === 'model' && <AssistantModelSettings assistant={assistant} />}
</Settings>
</HStack>
</StyledModal>
)
}
const LeftMenu = styled.div`
background-color: var(--color-background);
height: calc(80vh - 20px);
border-right: 0.5px solid var(--color-border);
`
const Settings = styled.div`
flex: 1;
padding: 10px 20px;
height: calc(80vh - 20px);
overflow-y: scroll;
`
const StyledModal = styled(Modal)`
.ant-modal-title {
font-size: 14px;
}
.ant-modal-close {
top: 4px;
}
.ant-menu-item {
height: 36px;
border-radius: 4px;
color: var(--color-text-2);
display: flex;
align-items: center;
.ant-menu-title-content {
line-height: 36px;
}
}
.ant-menu-item-active {
background-color: var(--color-background-soft) !important;
transition: none;
}
.ant-menu-item-selected {
background-color: var(--color-background-soft);
.ant-menu-title-content {
color: var(--color-text-1);
font-weight: 500;
}
}
`
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AssistantSettingPopup'
)
})
}
}

View File

@@ -1,84 +0,0 @@
import { Assistant } from '@renderer/types'
import { Input, Modal } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface AssistantSettingPopupShowParams {
assistant: Assistant
}
interface Props extends AssistantSettingPopupShowParams {
resolve: (assistant: Assistant) => void
}
const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => {
const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt)
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
}
const handleCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({ ...assistant, name, prompt })
}
return (
<Modal
title={assistant.name}
open={open}
onOk={onOk}
onCancel={handleCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
centered>
<Box mb={8}>{t('common.name')}</Box>
<Input
placeholder={t('common.assistant') + t('common.name')}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Box mt={8} mb={8}>
{t('common.prompt')}
</Box>
<TextArea
rows={10}
placeholder={t('common.assistant') + t('common.prompt')}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
</Modal>
)
}
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AssistantSettingPopup'
)
})
}
}

View File

@@ -0,0 +1,98 @@
import { Modal, ModalProps } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { TextAreaProps } from 'antd/lib/input'
import { TextAreaRef } from 'antd/lib/input/TextArea'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TopView } from '../TopView'
interface ShowParams {
text: string
textareaProps?: TextAreaProps
modalProps?: ModalProps
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [textValue, setTextValue] = useState(text)
const textareaRef = useRef<TextAreaRef>(null)
const onOk = () => {
setOpen(false)
resolve(textValue)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(null)
}
const resizeTextArea = () => {
const textArea = textareaRef.current?.resizableTextArea?.textArea
const maxHeight = innerHeight * 0.6
if (textArea) {
textArea.style.height = 'auto'
textArea.style.height = textArea?.scrollHeight > maxHeight ? maxHeight + 'px' : `${textArea?.scrollHeight}px`
}
}
useEffect(() => {
setTimeout(resizeTextArea, 0)
}, [])
return (
<Modal
title={t('common.edit')}
width="60vw"
style={{ maxHeight: '70vh' }}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
okText={t('common.save')}
{...modalProps}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
centered>
<TextArea
ref={textareaRef}
rows={2}
autoFocus
{...textareaProps}
value={textValue}
onInput={resizeTextArea}
onChange={(e) => setTextValue(e.target.value)}
/>
</Modal>
)
}
export default class TextEditPopup {
static topviewId = 0
static hide() {
TopView.hide('TextEditPopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'TextEditPopup'
)
})
}
}

View File

@@ -1,4 +1,4 @@
import { FolderOutlined, TranslationOutlined } from '@ant-design/icons'
import { FileSearchOutlined, FolderOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env'
import useAvatar from '@renderer/hooks/useAvatar'
@@ -23,6 +23,7 @@ const Sidebar: FC = () => {
const { windowStyle } = useSettings()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const onEditUser = () => UserPopup.show()
@@ -72,6 +73,11 @@ const Sidebar: FC = () => {
<FolderOutlined />
</Icon>
</StyledLink>
<StyledLink onClick={() => to('/messages')}>
<Icon className={isRoutes('/messages')}>
<FileSearchOutlined />
</Icon>
</StyledLink>
</Menus>
</MainMenus>
<Menus onClick={MinApp.onClose}>

View File

@@ -1,5 +1,5 @@
export const DEFAULT_TEMPERATURE = 0.7
export const DEFAULT_CONEXTCOUNT = 6
export const DEFAULT_CONEXTCOUNT = 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"

View File

@@ -1,6 +1,7 @@
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
@@ -10,6 +11,7 @@ import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
@@ -24,7 +26,6 @@ import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import MinApp from '@renderer/components/MinApp'
import { MinAppType } from '@renderer/types'
@@ -63,7 +64,7 @@ const _apps: MinAppType[] = [
},
{
id: 'zhipu',
name: '智谱',
name: '智谱清言',
url: 'https://chatglm.cn/main/alltoolsdetail',
logo: ZhipuProviderLogo
},
@@ -199,6 +200,13 @@ const _apps: MinAppType[] = [
logo: HuggingChatLogo,
url: 'https://huggingface.co/chat/',
bodered: true
},
{
id: 'Felo',
name: 'Felo',
logo: FeloAppLogo,
url: 'https://felo.ai/',
bodered: true
}
]

View File

@@ -1,11 +1,20 @@
import Ai360ModelLogo from '@renderer/assets/images/models/360.png'
import Ai360ModelLogoDark from '@renderer/assets/images/models/360_dark.png'
import AdeptModelLogo from '@renderer/assets/images/models/adept.png'
import AdeptModelLogoDark from '@renderer/assets/images/models/adept_dark.png'
import Ai21ModelLogo from '@renderer/assets/images/models/ai21.png'
import Ai21ModelLogoDark from '@renderer/assets/images/models/ai21_dark.png'
import AimassModelLogo from '@renderer/assets/images/models/aimass.png'
import AimassModelLogoDark from '@renderer/assets/images/models/aimass_dark.png'
import AisingaporeModelLogo from '@renderer/assets/images/models/aisingapore.png'
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.png'
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.png'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
import ChatGLMModelLogoDark from '@renderer/assets/images/models/chatglm_dark.png'
import ChatGptModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
import ClaudeModelLogoDark from '@renderer/assets/images/models/claude_dark.png'
import CodegeexModelLogo from '@renderer/assets/images/models/codegeex.png'
@@ -16,9 +25,11 @@ import CopilotModelLogo from '@renderer/assets/images/models/copilot.png'
import CopilotModelLogoDark from '@renderer/assets/images/models/copilot_dark.png'
import DalleModelLogo from '@renderer/assets/images/models/dalle.png'
import DalleModelLogoDark from '@renderer/assets/images/models/dalle_dark.png'
import DbrxModalLogo from '@renderer/assets/images/models/dbrx.png'
import DbrxModelLogo from '@renderer/assets/images/models/dbrx.png'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import DeepSeekModelLogoDark from '@renderer/assets/images/models/deepseek_dark.png'
import DianxinModelLogo from '@renderer/assets/images/models/dianxin.png'
import DianxinModelLogoDark from '@renderer/assets/images/models/dianxin_dark.png'
import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
import DoubaoModelLogoDark from '@renderer/assets/images/models/doubao_dark.png'
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
@@ -31,26 +42,41 @@ import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
import GeminiModelLogoDark from '@renderer/assets/images/models/gemini_dark.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.png'
import GemmaModelLogoDark from '@renderer/assets/images/models/gemma_dark.png'
import GoogleModelLogo from '@renderer/assets/images/models/google.png'
import GoogleModelLogoDark from '@renderer/assets/images/models/google.png'
import GorkModelLogo from '@renderer/assets/images/models/gork.png'
import GorkModelLogoDark from '@renderer/assets/images/models/gork_dark.png'
import ChatGPT35ModelLogo from '@renderer/assets/images/models/gpt_3.5.png'
import ChatGPT4ModelLogo from '@renderer/assets/images/models/gpt_4.png'
import ChatGptModelLogoDakr from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPT35ModelLogoDark from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPT4ModelLogoDark from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPTo1ModelLogoDark from '@renderer/assets/images/models/gpt_dark.png'
import ChatGPTo1ModelLogo from '@renderer/assets/images/models/gpt_o1.png'
import GrypheModelLogo from '@renderer/assets/images/models/gryphe.png'
import GrypheModelLogoDark from '@renderer/assets/images/models/gryphe_dark.png'
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png'
import HailuoModelLogoDark from '@renderer/assets/images/models/hailuo_dark.png'
import HuggingfaceModelLogo from '@renderer/assets/images/models/huggingface.png'
import HuggingfaceModelLogoDark from '@renderer/assets/images/models/huggingface_dark.png'
import HunyuanModelLogo from '@renderer/assets/images/models/hunyuan.png'
import HunyuanModelLogoDark from '@renderer/assets/images/models/hunyuan_dark.png'
import IbmModelLogo from '@renderer/assets/images/models/ibm.png'
import IbmModelLogoDark from '@renderer/assets/images/models/ibm_dark.png'
import InternlmModelLogo from '@renderer/assets/images/models/internlm.png'
import InternlmModelLogoDark from '@renderer/assets/images/models/internlm_dark.png'
import KeLingModelLogo from '@renderer/assets/images/models/keling.png'
import KeLingModelLogoDark from '@renderer/assets/images/models/keling_dark.png'
import LlamaModelLogo from '@renderer/assets/images/models/llama.png'
import LlamaModelLogoDark from '@renderer/assets/images/models/llama_dark.png'
import LLavaModelLogo from '@renderer/assets/images/models/llava.png'
import LLavaModelLogoDark from '@renderer/assets/images/models/llava_dark.png'
import LumaModelLogo from '@renderer/assets/images/models/luma.png'
import LumaModelLogoDark from '@renderer/assets/images/models/luma_dark.png'
import MagicModelLogo from '@renderer/assets/images/models/magic.png'
import MagicModelLogoDark from '@renderer/assets/images/models/magic_dark.png'
import MediatekModelLogo from '@renderer/assets/images/models/mediatek.png'
import MediatekModelLogoDark from '@renderer/assets/images/models/mediatek_dark.png'
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import MicrosoftModelLogoDark from '@renderer/assets/images/models/microsoft_dark.png'
import MidjourneyModelLogo from '@renderer/assets/images/models/midjourney.png'
@@ -63,23 +89,54 @@ import MistralModelLogo from '@renderer/assets/images/models/mixtral.png'
import MistralModelLogoDark from '@renderer/assets/images/models/mixtral_dark.png'
import MoonshotModelLogo from '@renderer/assets/images/models/moonshot.png'
import MoonshotModelLogoDark from '@renderer/assets/images/models/moonshot_dark.png'
import NousResearchModelLogo from '@renderer/assets/images/models/nousresearch.png'
import NousResearchModelLogoDark from '@renderer/assets/images/models/nousresearch.png'
import NvidiaModelLogo from '@renderer/assets/images/models/nvidia.png'
import NvidiaModelLogoDark from '@renderer/assets/images/models/nvidia_dark.png'
import PalmModelLogo from '@renderer/assets/images/models/palm.png'
import PalmModelLogoDark from '@renderer/assets/images/models/palm_dark.png'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
import QwenModelLogoDark from '@renderer/assets/images/models/qwen_dark.png'
import RakutenaiModelLogo from '@renderer/assets/images/models/rakutenai.png'
import RakutenaiModelLogoDark from '@renderer/assets/images/models/rakutenai_dark.png'
import SparkDeskModelLogo from '@renderer/assets/images/models/sparkdesk.png'
import SparkDeskModelLogoDark from '@renderer/assets/images/models/sparkdesk_dark.png'
import StabilityModelLogo from '@renderer/assets/images/models/stability.png'
import StabilityModelLogoDark from '@renderer/assets/images/models/stability_dark.png'
import StepModelLogo from '@renderer/assets/images/models/step.png'
import StepModelLogoDark from '@renderer/assets/images/models/step_dark.png'
import SunoModelLogo from '@renderer/assets/images/models/suno.png'
import SunoModelLogoDark from '@renderer/assets/images/models/suno_dark.png'
import TeleModelLogo from '@renderer/assets/images/models/tele.png'
import TeleModelLogoDark from '@renderer/assets/images/models/tele_dark.png'
import UpstageModelLogo from '@renderer/assets/images/models/upstage.png'
import UpstageModelLogoDark from '@renderer/assets/images/models/upstage_dark.png'
import ViduModelLogo from '@renderer/assets/images/models/vidu.png'
import ViduModelLogoDark from '@renderer/assets/images/models/vidu_dark.png'
import WenxinModelLogo from '@renderer/assets/images/models/wenxin.png'
import WenxinModelLogoDark from '@renderer/assets/images/models/wenxin_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
import { Model } from '@renderer/types'
import OpenAI from 'openai'
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-turbo|dall|cogview/i
const VISION_REGEX = /llava|moondream|minicpm|gemini-1.5|claude-3|vision|glm-4v|gpt-4|qwen-vl/i
const EMBEDDING_REGEX = /embedding/i
const allowedModels = [
'llava',
'moondream',
'minicpm',
'gemini-1\\.5',
'claude-3',
'vision',
'glm-4v',
'qwen-vl',
'gpt-4(?:-[\\w-]+)',
'gpt-4o(?:-[\\w-]+)?'
]
const excludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
const VISION_REGEX = new RegExp(`\\b(?!(?:${excludedModels.join('|')})\\b)(${allowedModels.join('|')})\\b`, 'i')
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|base|retrieval|uae-)/i
const NOT_SUPPORTED_REGEX = /(?:^text-|embed|tts|rerank|whisper|speech|davinci|babbage|bge-|base|retrieval|uae-)/i
export function getModelLogo(modelId: string) {
const isLight = true
@@ -89,10 +146,15 @@ export function getModelLogo(modelId: string) {
}
const logoMap = {
abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
'o1-': isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark,
'gpt-4': isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,
'text-moderation': isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,
'text-moderation': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
'babbage-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
'text-embedding': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
'davinci-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
qwen: isLight ? QwenModelLogo : QwenModelLogoDark,
@@ -110,7 +172,7 @@ export function getModelLogo(modelId: string) {
bison: isLight ? PalmModelLogo : PalmModelLogoDark,
palm: isLight ? PalmModelLogo : PalmModelLogoDark,
step: isLight ? StepModelLogo : StepModelLogoDark,
abab: isLight ? HailuoModelLogo : HailuoModelLogoDark,
hailuo: isLight ? HailuoModelLogo : HailuoModelLogoDark,
'ep-202': isLight ? DoubaoModelLogo : DoubaoModelLogoDark,
cohere: isLight ? CohereModelLogo : CohereModelLogoDark,
command: isLight ? CohereModelLogo : CohereModelLogoDark,
@@ -119,9 +181,12 @@ export function getModelLogo(modelId: string) {
aimass: isLight ? AimassModelLogo : AimassModelLogoDark,
codegeex: isLight ? CodegeexModelLogo : CodegeexModelLogoDark,
copilot: isLight ? CopilotModelLogo : CopilotModelLogoDark,
creative: isLight ? CopilotModelLogo : CopilotModelLogoDark,
balanced: isLight ? CopilotModelLogo : CopilotModelLogoDark,
precise: isLight ? CopilotModelLogo : CopilotModelLogoDark,
dalle: isLight ? DalleModelLogo : DalleModelLogoDark,
'dall-e': isLight ? DalleModelLogo : DalleModelLogoDark,
dbrx: isLight ? DbrxModalLogo : DbrxModalLogo,
dbrx: isLight ? DbrxModelLogo : DbrxModelLogo,
flashaudio: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark,
flux: isLight ? FluxModelLogo : FluxModelLogoDark,
gork: isLight ? GorkModelLogo : GorkModelLogoDark,
@@ -130,11 +195,41 @@ export function getModelLogo(modelId: string) {
llava: isLight ? LLavaModelLogo : LLavaModelLogoDark,
magic: isLight ? MagicModelLogo : MagicModelLogoDark,
midjourney: isLight ? MidjourneyModelLogo : MidjourneyModelLogoDark,
minimax: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
wenxin: isLight ? WenxinModelLogo : WenxinModelLogoDark,
'mj-': isLight ? MidjourneyModelLogo : MidjourneyModelLogoDark,
'ernie-': isLight ? WenxinModelLogo : WenxinModelLogoDark,
voice: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark,
tts: isLight ? FlashaudioModelLogo : FlashaudioModelLogoDark,
stability: isLight ? StabilityModelLogo : StabilityModelLogoDark
'tts-1': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
'whisper-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
'stable-': isLight ? StabilityModelLogo : StabilityModelLogoDark,
sd2: isLight ? StabilityModelLogo : StabilityModelLogoDark,
sd3: isLight ? StabilityModelLogo : StabilityModelLogoDark,
sdxl: isLight ? StabilityModelLogo : StabilityModelLogoDark,
sparkdesk: isLight ? SparkDeskModelLogo : SparkDeskModelLogoDark,
generalv: isLight ? SparkDeskModelLogo : SparkDeskModelLogoDark,
wizardlm: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,
microsoft: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,
hermes: isLight ? NousResearchModelLogo : NousResearchModelLogoDark,
gryphe: isLight ? GrypheModelLogo : GrypheModelLogoDark,
suno: isLight ? SunoModelLogo : SunoModelLogoDark,
chirp: isLight ? SunoModelLogo : SunoModelLogoDark,
luma: isLight ? LumaModelLogo : LumaModelLogoDark,
keling: isLight ? KeLingModelLogo : KeLingModelLogoDark,
'vidu-': isLight ? ViduModelLogo : ViduModelLogoDark,
ai21: isLight ? Ai21ModelLogo : Ai21ModelLogoDark,
'jamba-': isLight ? Ai21ModelLogo : Ai21ModelLogoDark,
mythomax: isLight ? GrypheModelLogo : GrypheModelLogoDark,
nvidia: isLight ? NvidiaModelLogo : NvidiaModelLogoDark,
dianxin: isLight ? DianxinModelLogo : DianxinModelLogoDark,
tele: isLight ? TeleModelLogo : TeleModelLogoDark,
adept: isLight ? AdeptModelLogo : AdeptModelLogoDark,
aisingapore: isLight ? AisingaporeModelLogo : AisingaporeModelLogoDark,
bigcode: isLight ? BigcodeModelLogo : BigcodeModelLogoDark,
mediatek: isLight ? MediatekModelLogo : MediatekModelLogoDark,
upstage: isLight ? UpstageModelLogo : UpstageModelLogoDark,
rakutenai: isLight ? RakutenaiModelLogo : RakutenaiModelLogoDark,
ibm: isLight ? IbmModelLogo : IbmModelLogoDark,
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark
}
for (const key in logoMap) {
@@ -149,6 +244,30 @@ export function getModelLogo(modelId: string) {
export const SYSTEM_MODELS: Record<string, Model[]> = {
ollama: [],
silicon: [
{
id: 'Qwen/Qwen2.5-72B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-72B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-32B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-32B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-14B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-14B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-7B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-7B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2-7B-Instruct',
provider: 'silicon',
@@ -204,6 +323,24 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'openai',
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',
name: ' o1-mini',
group: 'o1'
},
{
id: 'o1-preview',
provider: 'openai',
name: ' o1-preview',
group: 'o1'
}
],
gemini: [
@@ -260,12 +397,152 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'DeepSeek Coder'
}
],
together: [
{
id: 'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo',
provider: 'together',
name: 'Llama-3.2-11B-Vision',
group: 'Llama-3.2'
},
{
id: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
provider: 'together',
name: 'Llama-3.2-90B-Vision',
group: 'Llama-3.2'
},
{
id: 'google/gemma-2-27b-it',
provider: 'together',
name: 'gemma-2-27b-it',
group: 'Gemma'
},
{
id: 'google/gemma-2-9b-it',
provider: 'together',
name: 'gemma-2-9b-it',
group: 'Gemma'
}
],
ocoolai: [
{
id: 'gpt-4o',
provider: 'ocoolai',
name: 'OpenAI GPT-4o',
name: 'gpt-4o',
group: 'OpenAI'
},
{
id: 'gpt-4o-all',
provider: 'ocoolai',
name: 'gpt-4o-all',
group: 'OpenAI'
},
{
id: 'gpt-4-all',
provider: 'ocoolai',
name: 'gpt-4-all',
group: 'OpenAI'
},
{
id: 'gpt-4o-mini',
provider: 'ocoolai',
name: 'gpt-4o-mini',
group: 'OpenAI'
},
{
id: 'gpt-4',
provider: 'ocoolai',
name: 'gpt-4',
group: 'OpenAI'
},
{
id: 'gpt-4-turbo',
provider: 'ocoolai',
name: 'gpt-4-turbo',
group: 'OpenAI'
},
{
id: 'o1-preview',
provider: 'ocoolai',
name: 'o1-preview',
group: 'OpenAI'
},
{
id: 'o1-mini',
provider: 'ocoolai',
name: 'o1-mini',
group: 'OpenAI'
},
{
id: 'gpt-3.5-turbo',
provider: 'ocoolai',
name: 'gpt-3.5-turbo',
group: 'OpenAI'
},
{
id: 'claude-3-5-sonnet-20240620',
provider: 'ocoolai',
name: 'claude-3-5-sonnet-20240620',
group: 'Anthropic'
},
{
id: 'claude-3-opus-20240229',
provider: 'ocoolai',
name: 'claude-3-opus-20240229',
group: 'Anthropic'
},
{
id: 'claude-3-sonnet-20240229',
provider: 'ocoolai',
name: 'claude-3-sonnet-20240229',
group: 'Anthropic'
},
{
id: 'claude-3-haiku-20240307',
provider: 'ocoolai',
name: 'claude-3-haiku-20240307',
group: 'Anthropic'
},
{
id: 'gemini-pro',
provider: 'ocoolai',
name: 'gemini-pro',
group: 'Gemini'
},
{
id: 'gemini-1.5-pro',
provider: 'ocoolai',
name: 'gemini-1.5-pro',
group: 'Gemini'
},
{
id: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo',
provider: 'ocoolai',
name: 'Llama-3.2-90B-Vision-Instruct-Turbo',
group: 'Llama-3.2'
},
{
id: 'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo',
provider: 'ocoolai',
name: 'Llama-3.2-11B-Vision-Instruct-Turbo',
group: 'Llama-3.2'
},
{
id: 'meta-llama/Llama-3.2-3B-Vision-Instruct-Turbo',
provider: 'ocoolai',
name: 'Llama-3.2-3B-Vision-Instruct-Turbo',
group: 'Llama-3.2'
},
{
id: 'google/gemma-2-27b-it',
provider: 'ocoolai',
name: 'gemma-2-27b-it',
group: 'Gemma'
},
{
id: 'google/gemma-2-9b-it',
provider: 'ocoolai',
name: 'gemma-2-9b-it',
group: 'Gemma'
}
],
github: [
@@ -479,6 +756,48 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Llama3'
}
],
fireworks: [
{
id: 'accounts/fireworks/models/mythomax-l2-13b',
provider: 'fireworks',
name: 'mythomax-l2-13b',
group: 'Gryphe'
},
{
id: 'accounts/fireworks/models/llama-v3-70b-instruct',
provider: 'fireworks',
name: 'Llama-3-70B-Instruct',
group: 'Llama3'
}
],
zhinao: [
{
id: '360gpt-pro',
provider: 'zhinao',
name: '360gpt-pro',
group: '360Gpt'
},
{
id: '360gpt-turbo',
provider: 'zhinao',
name: '360gpt-turbo',
group: '360Gpt'
}
],
nvidia: [
{
id: '01-ai/yi-large',
provider: 'nvidia',
name: 'yi-large',
group: 'Yi'
},
{
id: 'meta/llama-3.1-405b-instruct',
provider: 'nvidia',
name: 'llama-3.1-405b-instruct',
group: 'llama-3.1'
}
],
openrouter: [
{
id: 'google/gemma-2-9b-it:free',
@@ -550,3 +869,7 @@ export function isEmbeddingModel(model: Model): boolean {
export function isVisionModel(model: Model): boolean {
return VISION_REGEX.test(model.id)
}
export function isSupportedModel(model: OpenAI.Models.Model): boolean {
return !NOT_SUPPORTED_REGEX.test(model.id)
}

View File

@@ -1,21 +1,25 @@
import ZhinaoProviderLogo from '@renderer/assets/images/models/360.png'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.png'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import BytedanceProviderLogo from '@renderer/assets/images/providers/bytedance.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.png'
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 GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.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'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
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'
@@ -61,6 +65,15 @@ export function getProviderLogo(providerId: string) {
return GithubProviderLogo
case 'ocoolai':
return OcoolAiProviderLogo
case 'together':
return TogetherProviderLogo
case 'fireworks':
return FireworksProviderLogo
case 'zhinao':
return ZhinaoProviderLogo
case 'nvidia':
return NvidiaProviderLogo
default:
return undefined
}
@@ -113,15 +126,26 @@ export const PROVIDER_CONFIG = {
},
ocoolai: {
api: {
url: 'https://one.ooo.cool'
url: 'https://api.ocoolai.com'
},
websites: {
official: 'https://ocoolai.com/',
official: 'https://one.ocoolai.com/',
apiKey: 'https://one.ocoolai.com/token',
docs: 'https://docs.ooo.cool/',
models: 'https://docs.ooo.cool/guides/jiage/'
}
},
together: {
api: {
url: 'https://api.tohgether.xyz'
},
websites: {
official: 'https://www.together.ai/',
apiKey: 'https://api.together.ai/settings/api-keys',
docs: 'https://docs.together.ai/docs/introduction',
models: 'https://docs.together.ai/docs/chat-models'
}
},
github: {
api: {
url: 'https://models.inference.ai.azure.com/'
@@ -279,5 +303,38 @@ export const PROVIDER_CONFIG = {
docs: 'https://doc.aihubmix.com/',
models: 'https://aihubmix.com/models'
}
},
fireworks: {
api: {
url: 'https://api.fireworks.ai/inference'
},
websites: {
official: 'https://fireworks.ai/',
apiKey: 'https://fireworks.ai/account/api-keys',
docs: 'https://docs.fireworks.ai/getting-started/introduction',
models: 'https://fireworks.ai/dashboard/models'
}
},
zhinao: {
api: {
url: 'https://api.360.cn'
},
websites: {
official: 'https://ai.360.com/',
apiKey: 'https://ai.360.com/platform/keys',
docs: 'https://ai.360.com/platform/docs/overview',
models: 'https://ai.360.com/platform/limit'
}
},
nvidia: {
api: {
url: 'https://integrate.api.nvidia.com'
},
websites: {
official: 'https://ai.360.com/',
apiKey: 'https://build.nvidia.com/meta/llama-3_1-405b-instruct',
docs: 'https://docs.api.nvidia.com/nim/reference/llm-apis',
models: 'https://build.nvidia.com/nim'
}
}
}

View File

@@ -19,12 +19,18 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
Segmented: {
trackBg: 'transparent',
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
boxShadowTertiary: undefined
boxShadowTertiary: undefined,
borderRadiusLG: 12,
borderRadiusSM: 12,
borderRadiusXS: 12
},
Menu: {
activeBarBorderWidth: 0,
darkItemBg: 'transparent'
}
},
token: {
colorPrimary: '#00b96b',
borderRadius: 6
colorPrimary: '#00b96b'
}
}}>
{children}

View File

@@ -14,7 +14,7 @@ import { useRuntime } from './useStore'
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle } = useSettings()
const { proxyUrl, language, windowStyle, manualUpdateCheck } = useSettings()
const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@@ -27,8 +27,11 @@ export function useAppInit() {
document.getElementById('spinner')?.remove()
runAsyncFunction(async () => {
const { isPackaged } = await window.api.getAppInfo()
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
if (isPackaged && !manualUpdateCheck) {
setTimeout(window.api.checkForUpdate, 3000)
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {

View File

@@ -71,7 +71,7 @@ export function useDefaultAssistant() {
return {
defaultAssistant: {
...defaultAssistant,
topics: [getDefaultTopic()]
topics: [getDefaultTopic(defaultAssistant.id)]
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}

View File

@@ -0,0 +1,20 @@
import { throttle } from 'lodash'
import { useEffect, useRef } from 'react'
export default function useScrollPosition(key: string) {
const containerRef = useRef<HTMLDivElement>(null)
const scrollKey = `scroll:${key}`
const handleScroll = throttle(() => {
const position = containerRef.current?.scrollTop ?? 0
window.keyv.set(scrollKey, position)
}, 100)
useEffect(() => {
const scroll = () => containerRef.current?.scrollTo({ top: window.keyv.get(scrollKey) || 0 })
scroll()
setTimeout(scroll, 50)
}, [scrollKey])
return { containerRef, handleScroll }
}

View File

@@ -1,5 +1,6 @@
import db from '@renderer/databases'
import { deleteMessageFiles } from '@renderer/services/messages'
import store from '@renderer/store'
import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash'
import { useEffect, useState } from 'react'
@@ -8,9 +9,9 @@ import { useAssistant } from './useAssistant'
let _activeTopic: Topic
export function useActiveTopic(_assistant: Assistant) {
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
const { assistant } = useAssistant(_assistant.id)
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
_activeTopic = activeTopic
@@ -28,6 +29,14 @@ export function getTopic(assistant: Assistant, topicId: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
}
export async function getTopicById(topicId: string) {
const assistants = store.getState().assistants.assistants
const topics = assistants.map((assistant) => assistant.topics).flat()
const topic = topics.find((topic) => topic.id === topicId)
const messages = await TopicManager.getTopicMessages(topicId)
return { ...topic, messages } as Topic
}
export class TopicManager {
static async getTopic(id: string) {
return await db.topics.get(id)

View File

@@ -0,0 +1,312 @@
{
"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"
},
"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"
},
"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.delete.all.title": "Delete all topics",
"topics.delete.all.content": "Are you sure you want to delete all topics?",
"topics.move_to": "Move to",
"topics.list": "Topic List",
"topics.export.title": "Export",
"topics.export.image": "Export as image",
"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 text 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",
"suggestions.title": "Suggested Questions",
"add.assistant.title": "Add Assistant",
"message.new.context": "New Context",
"message.new.branch": "New Branch",
"assistant.search.placeholder": "Search"
},
"assistants": {
"title": "Assistants",
"abbr": "Assistant",
"search": "Search assistants...",
"prompt_settings": "Prompt Settings",
"model_settings": "Model Settings"
},
"model": {
"stream_output": "Stream Output"
},
"files": {
"title": "Files",
"file": "File",
"name": "Name",
"size": "Size",
"count": "Count",
"created_at": "Created At"
},
"agents": {
"title": "Agents",
"my_agents": "My Agents",
"add.title": "Add Agent",
"edit.title": "Edit Agent",
"add.name": "Name",
"add.name.placeholder": "Enter name",
"add.prompt": "Prompt",
"add.prompt.placeholder": "Enter prompt",
"add.button": "Add",
"manage.title": "Manage Agents",
"delete.popup.content": "Are you sure you want to delete this agent?",
"tag.default": "Default",
"tag.system": "System",
"tag.user": "Mine"
},
"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",
"locate.message": "Locate the message"
},
"provider": {
"nvidia": "Nvidia",
"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",
"baichuan": "Baichuan",
"dashscope": "DashScope",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "StepFun",
"doubao": "Doubao",
"minimax": "MiniMax",
"graphrag-kylin-mountain": "GraphRAG",
"github": "GitHub Models",
"ocoolai": "ocoolAI"
},
"settings": {
"title": "Settings",
"general": "General 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",
"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.webdav.title": "WebDAV",
"general.webdav.host": "WebDAV Host",
"general.webdav.host.placeholder": "http://localhost:8080",
"general.webdav.user": "WebDAV User",
"general.webdav.password": "WebDAV Password",
"general.webdav.path": "WebDAV Path",
"general.webdav.path.placeholder": "/backup",
"general.webdav.backup.button": "Backup to WebDAV",
"general.webdav.restore.button": "Restore from WebDAV",
"general.reset.title": "Data Reset",
"general.reset.button": "Reset",
"general.check_update_setting": "Check for updates",
"general.manual_update_check": "Check for updates manually",
"general.auto_update_check": "Check for updates automatically",
"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.docs_check": "Check",
"provider.docs_more_details": "for more details",
"provider.search_placeholder": "Search model id or name",
"provider.api.url.reset": "Reset",
"models.default_assistant_model": "Default Assistant Model",
"models.topic_naming_model": "Topic Naming Model",
"models.translate_model": "Translate Model",
"models.add.add_model": "Add Model",
"models.add.model_id.placeholder": "Required e.g. gpt-3.5-turbo",
"models.add.model_id": "Model ID",
"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.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",
"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"
},
"translate": {
"title": "Translation",
"any.language": "Any language",
"button.translate": "Translate",
"error.not_configured": "Translation model is not configured",
"input.placeholder": "Enter text to translate",
"output.placeholder": "Translation"
},
"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"
},
"words": {
"knowledgeGraph": "Knowledge Graph",
"visualization": "Visualization"
}
}
}

View File

@@ -1,577 +1,14 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import enUS from './en-us.json'
import zhCN from './zh-cn.json'
import zhTW from './zh-tw.json'
const resources = {
'en-US': {
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'
},
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',
'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',
'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'
},
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.delete.all.title': 'Delete all topics',
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
'topics.move_to': 'Move to',
'topics.list': 'Topic List',
'topics.export.title': 'Export',
'topics.export.image': 'Export as image',
'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 text 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',
'suggestions.title': 'Suggested Questions',
'add.assistant.title': 'Add Assistant',
'message.new.context': 'New Context',
'message.new.branch': 'New Branch',
'assistant.search.placeholder': 'Search'
},
assistants: {
title: 'Assistants',
abbr: 'Assistant',
search: 'Search assistants...'
},
model: {
stream_output: 'Stream Output'
},
files: {
title: 'Files',
file: 'File',
name: 'Name',
size: 'Size',
count: 'Count',
created_at: 'Created At'
},
agents: {
title: 'Agents',
my_agents: 'My Agents',
'add.title': 'Add Agent',
'edit.title': 'Edit Agent',
'add.name': 'Name',
'add.name.placeholder': 'Enter name',
'add.prompt': 'Prompt',
'add.prompt.placeholder': 'Enter prompt',
'add.button': 'Add',
'manage.title': 'Manage Agents',
'delete.popup.content': 'Are you sure you want to delete this agent?',
'tag.default': 'Default',
'tag.system': 'System',
'tag.user': 'Mine'
},
provider: {
openai: 'OpenAI',
gemini: 'Gemini',
deepseek: 'DeepSeek',
moonshot: 'Moonshot',
silicon: 'SiliconFlow',
openrouter: 'OpenRouter',
yi: 'Yi',
zhipu: 'ZHIPU AI',
groq: 'Groq',
ollama: 'Ollama',
baichuan: 'Baichuan',
dashscope: 'DashScope',
anthropic: 'Anthropic',
aihubmix: 'AiHubMix',
stepfun: 'StepFun',
doubao: 'Doubao',
minimax: 'MiniMax',
'graphrag-kylin-mountain': 'GraphRAG',
github: 'GitHub Models',
ocoolai: 'ocoolAI'
},
settings: {
title: 'Settings',
general: 'General 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',
'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.reset.title': 'Data Reset',
'general.reset.button': 'Reset',
'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.docs_check': 'Check',
'provider.docs_more_details': 'for more details',
'provider.search_placeholder': 'Search model id or name',
'provider.api.url.reset': 'Reset',
'models.default_assistant_model': 'Default Assistant Model',
'models.topic_naming_model': 'Topic Naming Model',
'models.translate_model': 'Translate Model',
'models.add.add_model': 'Add Model',
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
'models.add.model_id': 'Model ID',
'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.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',
'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'
},
translate: {
title: 'Translation',
'any.language': 'Any language',
'button.translate': 'Translate',
'error.not_configured': 'Translation model is not configured',
'input.placeholder': 'Enter text to translate',
'output.placeholder': 'Translation'
},
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.'
},
minapp: {
title: 'MinApp'
},
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'
},
words: {
knowledgeGraph: 'Knowledge Graph',
visualization: 'Visualization'
}
}
},
'zh-CN': {
translation: {
common: {
avatar: '头像',
language: '语言',
model: '模型',
models: '模型',
topics: '话题',
docs: '文档',
and: '和',
assistant: '智能体',
name: '名称',
description: '描述',
prompt: '提示词',
rename: '重命名',
delete: '删除',
edit: '编辑',
duplicate: '复制',
copy: '复制',
regenerate: '重新生成',
provider: '提供商',
you: '用户',
footnote: '引用内容',
select: '选择',
search: '搜索',
default: '默认',
warning: '警告',
back: '返回',
chat: '聊天'
},
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': '无效的代理地址',
'api.connection.failed': '连接失败',
'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换',
'restore.success': '恢复成功',
'backup.success': '备份成功',
'reset.confirm.content': '确定要重置所有数据吗?',
'reset.double.confirm.title': '数据丢失!!!',
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?',
'upgrade.success.title': '升级成功',
'upgrade.success.content': '重启应用以完成升级',
'upgrade.success.button': '重启',
'topic.added': '话题添加成功'
},
chat: {
save: '保存',
'default.name': '⭐️ 默认助手',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题',
'topics.title': '话题',
'topics.auto_rename': '生成话题名',
'topics.edit.title': '编辑话题名',
'topics.edit.placeholder': '输入新名称',
'topics.delete.all.title': '删除所有话题',
'topics.delete.all.content': '确定要删除所有话题吗?',
'topics.move_to': '移动到',
'topics.list': '话题列表',
'topics.export.title': '导出',
'topics.export.image': '导出为图片',
'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': '不限',
'suggestions.title': '建议的问题',
'add.assistant.title': '添加助手',
'message.new.context': '清除上下文',
'message.new.branch': '新分支',
'assistant.search.placeholder': '搜索'
},
assistants: {
title: '助手',
abbr: '助手',
search: '搜索助手'
},
model: {
stream_output: '流式输出'
},
files: {
title: '文件',
file: '文件',
name: '文件名',
size: '大小',
count: '文件数',
created_at: '创建时间'
},
agents: {
title: '智能体',
my_agents: '我的智能体',
'add.title': '添加智能体',
'edit.title': '编辑智能体',
'add.name': '名称',
'add.name.placeholder': '输入名称',
'add.prompt': '提示词',
'add.prompt.placeholder': '输入提示词',
'add.button': '添加',
'manage.title': '管理智能体',
'delete.popup.content': '确定要删除此智能体吗?',
'tag.default': '默认',
'tag.system': '系统',
'tag.user': '我的'
},
provider: {
openai: 'OpenAI',
gemini: 'Gemini',
deepseek: '深度求索',
moonshot: '月之暗面',
silicon: '硅基流动',
openrouter: 'OpenRouter',
yi: '零一万物',
zhipu: '智谱AI',
groq: 'Groq',
ollama: 'Ollama',
baichuan: '百川',
dashscope: '阿里云灵积',
anthropic: 'Anthropic',
aihubmix: 'AiHubMix',
stepfun: '阶跃星辰',
doubao: '豆包',
minimax: 'MiniMax',
'graphrag-kylin-mountain': 'GraphRAG',
github: 'GitHub Models',
ocoolai: 'ocoolAI'
},
settings: {
title: '设置',
general: '常规设置',
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': '长文本粘贴为文件',
'general.title': '常规设置',
'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名',
'general.backup.title': '数据备份与恢复',
'general.backup.button': '备份',
'general.restore.button': '恢复',
'general.reset.title': '重置数据',
'general.reset.button': '重置',
'advanced.title': '高级设置',
'advanced.click_assistant_switch_to_topics': '点击助手切换到话题',
'provider.api_key': 'API 密钥',
'provider.check': '检查',
'provider.get_api_key': '点击这里获取密钥',
'provider.api_host': 'API 地址',
'provider.docs_check': '查看',
'provider.docs_more_details': '获取更多详情',
'provider.search_placeholder': '搜索模型 ID 或名称',
'provider.api.url.reset': '重置',
'models.default_assistant_model': '默认助手模型',
'models.topic_naming_model': '话题命名模型',
'models.translate_model': '翻译模型',
'models.add.add_model': '添加模型',
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
'models.add.model_id': '模型 ID',
'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.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': '邮件',
'proxy.title': '代理地址',
'theme.title': '主题',
'theme.dark': '深色主题',
'theme.light': '浅色主题',
'theme.auto': '跟随系统',
'theme.window.style.title': '窗口样式',
'theme.window.style.transparent': '透明窗口',
'theme.window.style.opaque': '不透明窗口',
'font_size.title': '消息字体大小',
'topic.position': '话题位置',
'topic.position.left': '左侧',
'topic.position.right': '右侧'
},
translate: {
title: '翻译',
'any.language': '任意语言',
'button.translate': '翻译',
'error.not_configured': '翻译模型未配置',
'input.placeholder': '输入文本进行翻译',
'output.placeholder': '翻译'
},
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分钟'
},
minapp: {
title: '小程序'
},
error: {
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥',
'backup.file_format': '备份文件格式错误'
},
words: {
knowledgeGraph: '知识图谱',
visualization: '可视化'
}
}
}
'en-US': enUS,
'zh-CN': zhCN,
'zh-TW': zhTW
}
i18n.use(initReactI18next).init({

View File

@@ -0,0 +1,312 @@
{
"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": "聊天"
},
"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": "话题添加成功"
},
"chat": {
"save": "保存",
"default.name": "⭐️ 默认助手",
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
"default.topic.name": "默认话题",
"topics.title": "话题",
"topics.auto_rename": "生成话题名",
"topics.edit.title": "编辑话题名",
"topics.edit.placeholder": "输入新名称",
"topics.delete.all.title": "删除所有话题",
"topics.delete.all.content": "确定要删除所有话题吗?",
"topics.move_to": "移动到",
"topics.list": "话题列表",
"topics.export.title": "导出",
"topics.export.image": "导出为图片",
"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": "不限",
"suggestions.title": "建议的问题",
"add.assistant.title": "添加助手",
"message.new.context": "清除上下文",
"message.new.branch": "新分支",
"assistant.search.placeholder": "搜索"
},
"assistants": {
"title": "助手",
"abbr": "助手",
"search": "搜索助手",
"prompt_settings": "提示词设置",
"model_settings": "模型设置"
},
"model": {
"stream_output": "流式输出"
},
"files": {
"title": "文件",
"file": "文件",
"name": "文件名",
"size": "大小",
"count": "文件数",
"created_at": "创建时间"
},
"agents": {
"title": "智能体",
"my_agents": "我的智能体",
"add.title": "添加智能体",
"edit.title": "编辑智能体",
"add.name": "名称",
"add.name.placeholder": "输入名称",
"add.prompt": "提示词",
"add.prompt.placeholder": "输入提示词",
"add.button": "添加",
"manage.title": "管理智能体",
"delete.popup.content": "确定要删除此智能体吗?",
"tag.default": "默认",
"tag.system": "系统",
"tag.user": "我的"
},
"minapp": {
"title": "小程序"
},
"history": {
"title": "话题搜索",
"search.placeholder": "搜索话题或消息...",
"continue_chat": "继续聊天",
"search.topics.empty": "没有找到相关话题, 点击回车键搜索所有消息",
"locate.message": "定位到消息"
},
"provider": {
"nvidia": "英伟达",
"zhinao": "360智脑",
"fireworks": "Fireworks",
"together": "Together",
"openai": "OpenAI",
"gemini": "Gemini",
"deepseek": "深度求索",
"moonshot": "月之暗面",
"silicon": "硅基流动",
"openrouter": "OpenRouter",
"yi": "零一万物",
"zhipu": "智谱AI",
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "百川",
"dashscope": "阿里云灵积",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "阶跃星辰",
"doubao": "豆包",
"minimax": "MiniMax",
"graphrag-kylin-mountain": "GraphRAG",
"github": "GitHub Models",
"ocoolai": "ocoolAI"
},
"settings": {
"title": "设置",
"general": "常规设置",
"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 渲染输入消息",
"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.webdav.title": "WebDAV",
"general.webdav.host": "WebDAV 地址",
"general.webdav.host.placeholder": "http://localhost:8080",
"general.webdav.user": "WebDAV 用户名",
"general.webdav.password": "WebDAV 密码",
"general.webdav.path": "WebDAV 路径",
"general.webdav.path.placeholder": "/backup",
"general.webdav.backup.button": "备份到 WebDAV",
"general.webdav.restore.button": "从 WebDAV 恢复",
"general.check_update_setting": "更新设置",
"general.manual_update_check": "手动检查更新",
"general.auto_update_check": "自动检查更新",
"advanced.title": "高级设置",
"advanced.click_assistant_switch_to_topics": "点击助手切换到话题",
"provider.api_key": "API 密钥",
"provider.check": "检查",
"provider.get_api_key": "点击这里获取密钥",
"provider.api_host": "API 地址",
"provider.docs_check": "查看",
"provider.docs_more_details": "获取更多详情",
"provider.search_placeholder": "搜索模型 ID 或名称",
"provider.api.url.reset": "重置",
"models.default_assistant_model": "默认助手模型",
"models.topic_naming_model": "话题命名模型",
"models.translate_model": "翻译模型",
"models.add.add_model": "添加模型",
"models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo",
"models.add.model_id": "模型 ID",
"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.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": "邮件",
"proxy.title": "代理地址",
"theme.title": "主题",
"theme.dark": "深色主题",
"theme.light": "浅色主题",
"theme.auto": "跟随系统",
"theme.window.style.title": "窗口样式",
"theme.window.style.transparent": "透明窗口",
"theme.window.style.opaque": "不透明窗口",
"font_size.title": "消息字体大小",
"topic.position": "话题位置",
"topic.position.left": "左侧",
"topic.position.right": "右侧"
},
"translate": {
"title": "翻译",
"any.language": "任意语言",
"button.translate": "翻译",
"error.not_configured": "翻译模型未配置",
"input.placeholder": "输入文本进行翻译",
"output.placeholder": "翻译"
},
"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": "备份文件格式错误"
},
"words": {
"knowledgeGraph": "知识图谱",
"visualization": "可视化"
}
}
}

View File

@@ -0,0 +1,312 @@
{
"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": "聊天"
},
"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": "新話題已添加"
},
"chat": {
"save": "保存",
"default.name": "⭐️ 預設助手",
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
"default.topic.name": "預設話題",
"topics.title": "話題",
"topics.auto_rename": "自動重新命名",
"topics.edit.title": "編輯名稱",
"topics.edit.placeholder": "輸入新名稱",
"topics.delete.all.title": "刪除所有話題",
"topics.delete.all.content": "確定要刪除所有話題嗎?",
"topics.move_to": "移動到",
"topics.list": "話題列表",
"topics.export.title": "匯出",
"topics.export.image": "匯出為圖片",
"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": "最大",
"suggestions.title": "建議的問題",
"add.assistant.title": "添加助手",
"message.new.context": "新上下文",
"message.new.branch": "新分支",
"assistant.search.placeholder": "搜尋"
},
"assistants": {
"title": "助手",
"abbr": "助",
"search": "搜尋助手...",
"prompt_settings": "提示詞設定",
"model_settings": "模型設定"
},
"model": {
"stream_output": "串流輸出"
},
"files": {
"title": "檔案",
"file": "檔案",
"name": "名稱",
"size": "大小",
"count": "數量",
"created_at": "建立時間"
},
"agents": {
"title": "智能體",
"my_agents": "我的智能體",
"add.title": "添加智能體",
"edit.title": "編輯智能體",
"add.name": "名稱",
"add.name.placeholder": "輸入名稱",
"add.prompt": "提示詞",
"add.prompt.placeholder": "輸入提示詞",
"add.button": "添加",
"manage.title": "管理智能體",
"delete.popup.content": "確定要刪除此智能體嗎?",
"tag.default": "預設",
"tag.system": "系統",
"tag.user": "我的"
},
"minapp": {
"title": "小程序"
},
"history": {
"title": "搜尋話題",
"search.placeholder": "搜尋話題或訊息...",
"continue_chat": "繼續聊天",
"search.topics.empty": "沒有找到相關話題, 點擊回車鍵搜尋所有訊息",
"locate.message": "定位到訊息"
},
"provider": {
"nvidia": "輝達",
"zhinao": "360智腦",
"fireworks": "Fireworks",
"together": "Together",
"openai": "OpenAI",
"gemini": "Gemini",
"deepseek": "深度求索",
"moonshot": "月之暗面",
"silicon": "SiliconFlow",
"openrouter": "OpenRouter",
"yi": "零一万物",
"zhipu": "智谱AI",
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "百川",
"dashscope": "DashScope",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "StepFun",
"doubao": "豆包",
"minimax": "MiniMax",
"graphrag-kylin-mountain": "GraphRAG",
"github": "GitHub Models",
"ocoolai": "ocoolAI"
},
"settings": {
"title": "設定",
"general": "一般設定",
"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.markdown_rendering_input_message": "Markdown 渲染輸入訊息",
"general.title": "一般設定",
"general.user_name": "使用者名稱",
"general.user_name.placeholder": "輸入您的名稱",
"general.backup.title": "資料備份與復原",
"general.backup.button": "備份",
"general.restore.button": "復原",
"general.view_webdav_settings": "查看 WebDAV 設定",
"general.webdav.title": "WebDAV",
"general.webdav.host": "WebDAV 主機位址",
"general.webdav.host.placeholder": "http://localhost:8080",
"general.webdav.user": "WebDAV 使用者名稱",
"general.webdav.password": "WebDAV 密碼",
"general.webdav.path": "WebDAV Path",
"general.webdav.path.placeholder": "/backup",
"general.webdav.backup.button": "從 WebDAV 備份",
"general.webdav.restore.button": "從 WebDAV 恢復",
"general.reset.title": "資料重置",
"general.reset.button": "重置",
"general.check_update_setting": "更新設定",
"general.manual_update_check": "手動檢查更新",
"general.auto_update_check": "自動檢查更新",
"advanced.title": "進階設定",
"advanced.click_assistant_switch_to_topics": "點擊助手切換到話題",
"provider.api_key": "API 密鑰",
"provider.check": "檢查",
"provider.get_api_key": "獲取 API 密鑰",
"provider.api_host": "API 主機地址",
"provider.docs_check": "檢查",
"provider.docs_more_details": "查看更多細節",
"provider.search_placeholder": "搜尋模型 ID 或名稱",
"provider.api.url.reset": "重置",
"models.default_assistant_model": "預設助手模型",
"models.topic_naming_model": "話題命名模型",
"models.translate_model": "翻譯模型",
"models.add.add_model": "添加模型",
"models.add.model_id.placeholder": "必填,例如 gpt-3.5-turbo",
"models.add.model_id": "模型 ID",
"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.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": "郵件",
"proxy.title": "代理地址",
"theme.title": "主題",
"theme.dark": "深色主題",
"theme.light": "淺色主題",
"theme.auto": "自動",
"theme.window.style.title": "視窗樣式",
"theme.window.style.transparent": "透明視窗",
"theme.window.style.opaque": "不透明視窗",
"font_size.title": "訊息字體大小",
"topic.position": "話題位置",
"topic.position.left": "左側",
"topic.position.right": "右側"
},
"translate": {
"title": "翻譯",
"any.language": "任意語言",
"button.translate": "翻譯",
"error.not_configured": "翻譯模型未配置",
"input.placeholder": "輸入文字進行翻譯",
"output.placeholder": "翻譯"
},
"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": "備份文件格式錯誤"
},
"words": {
"knowledgeGraph": "知識圖譜",
"visualization": "可視化"
}
}
}

View File

@@ -1,13 +1,8 @@
import './assets/styles/index.scss'
import './init'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)

View File

@@ -29,7 +29,7 @@ const AppsPage: FC = () => {
window.modal.confirm({
title: agent.emoji + ' ' + agent.name,
content: agent.description || agent.prompt,
content: (agent.description || agent.prompt).substring(0, 1000) + '...',
icon: null,
closable: true,
maskClosable: true,

View File

@@ -12,7 +12,7 @@ import styled from 'styled-components'
const FilesPage: FC = () => {
const { t } = useTranslation()
const files = useLiveQuery<FileType[]>(() => db.files.orderBy('created_at').reverse().toArray())
const files = useLiveQuery<FileType[]>(() => db.files.orderBy('ext').reverse().toArray())
const dataSource = files?.map((file) => {
const isImage = file.type === FileTypes.IMAGE
@@ -65,8 +65,14 @@ const FilesPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<VStack style={{ flex: 1 }}>
<Table dataSource={dataSource} columns={columns} style={{ width: '100%', height: '100%' }} size="small" />
<VStack style={{ width: '100%' }}>
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%', marginBottom: 20 }}
size="small"
pagination={{ pageSize: 15 }}
/>
</VStack>
</ContentContainer>
</Container>
@@ -87,7 +93,7 @@ const ContentContainer = styled.div`
justify-content: center;
height: 100%;
overflow-y: scroll;
padding: 20px;
padding: 15px;
`
const FileNameText = styled.div`

View File

@@ -0,0 +1,157 @@
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 { last } from 'lodash'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SearchMessage from './components/SearchMessage'
import SearchResults from './components/SearchResults'
import TopicMessages from './components/TopicMessages'
import TopicsHistory from './components/TopicsHistory'
type Route = 'topics' | 'topic' | 'search' | 'message'
let _search = ''
let _stack: Route[] = ['topics']
let _topic: Topic | undefined
let _message: Message | undefined
const TopicsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState(_search)
const [stack, setStack] = useState<Route[]>(_stack)
const [topic, setTopic] = useState<Topic | undefined>(_topic)
const [message, setMessage] = useState<Message | undefined>(_message)
_search = search
_stack = stack
_topic = topic
_message = message
const goBack = () => {
const _stack = [...stack]
const route = _stack.pop()
setStack(_stack)
route === 'search' && setSearch('')
route === 'topic' && setTopic(undefined)
route === 'message' && setMessage(undefined)
}
const onSearch = () => {
setStack(['topics', 'search'])
setTopic(undefined)
}
const onTopicClick = (topic: Topic) => {
setStack((prev) => [...prev, 'topic'])
setTopic(topic)
}
const onMessageClick = (message: Message) => {
setStack(['topics', 'search', 'message'])
setMessage(message)
}
const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none')
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} style={{ display: isShow('topics') }} />
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
<SearchResults
keywords={search}
onMessageClick={onMessageClick}
onTopicClick={onTopicClick}
style={{ display: isShow('search') }}
/>
<SearchMessage message={message} style={{ display: isShow('message') }} />
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
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;
width: 100%;
position: relative;
`
const HeaderLeft = styled.div`
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
top: 8px;
left: 15px;
`
const MenuIcon = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
border-radius: 50%;
&:hover {
background-color: var(--color-background-mute);
.anticon {
color: var(--color-text-1);
}
}
`
const SearchInput = styled(Input)`
border-radius: 30px;
width: 800px;
height: 36px;
`
export default TopicsPage

View File

@@ -0,0 +1,62 @@
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/messages'
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> {
message?: Message
}
const SearchMessage: FC<Props> = ({ message, ...props }) => {
const navigate = useNavigate()
const { t } = useTranslation()
if (!message) {
return null
}
return (
<MessagesContainer {...props}>
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
<MessageItem message={message} showMenu={false} />
<Button
type="text"
size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
onClick={() => locateToMessage(navigate, message)}
icon={<ArrowRightOutlined />}
/>
<HStack mt="10px" justifyContent="center">
<Button onClick={() => locateToMessage(navigate, message)} icon={<ArrowRightOutlined />}>
{t('history.locate.message')}
</Button>
</HStack>
</ContainerWrapper>
</MessagesContainer>
)
}
const MessagesContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: scroll;
`
const ContainerWrapper = styled.div`
width: 800px;
display: flex;
flex-direction: column;
.message {
padding: 0;
}
`
export default SearchMessage

View File

@@ -0,0 +1,154 @@
import db from '@renderer/databases'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { getTopicById } from '@renderer/hooks/useTopic'
import { Message, Topic } from '@renderer/types'
import { List, Typography } from 'antd'
import { useLiveQuery } from 'dexie-react-hooks'
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
import styled from 'styled-components'
const { Text, Title } = Typography
interface Props extends React.HTMLAttributes<HTMLDivElement> {
keywords: string
onMessageClick: (message: Message) => void
onTopicClick: (topic: Topic) => void
}
const SearchResults: FC<Props> = ({ keywords, onMessageClick, onTopicClick, ...props }) => {
const { handleScroll, containerRef } = useScrollPosition('SearchResults')
const [searchTerms, setSearchTerms] = useState<string[]>(
keywords
.toLowerCase()
.split(' ')
.filter((term) => term.length > 0)
)
const topics = useLiveQuery(() => db.topics.toArray(), [])
const messages = useMemo(
() => (topics || [])?.map((topic) => topic.messages.filter((message) => message.role !== 'user')).flat(),
[topics]
)
const [searchResults, setSearchResults] = useState<{ message: Message; topic: Topic }[]>([])
const [searchStats, setSearchStats] = useState({ count: 0, time: 0 })
const removeMarkdown = (text: string) => {
return text
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
.replace(/```[\s\S]*?```/g, '')
.replace(/`(.*?)`/g, '$1')
.replace(/#+\s/g, '')
.replace(/<[^>]*>/g, '')
}
const onSearch = useCallback(async () => {
setSearchResults([])
const startTime = performance.now()
const results: { message: Message; topic: Topic }[] = []
const newSearchTerms = keywords
.toLowerCase()
.split(' ')
.filter((term) => term.length > 0)
for (const message of messages) {
const cleanContent = removeMarkdown(message.content.toLowerCase())
if (newSearchTerms.every((term) => cleanContent.includes(term))) {
results.push({ message, topic: await getTopicById(message.topicId)! })
}
}
const endTime = performance.now()
setSearchResults(results)
setSearchStats({
count: results.length,
time: (endTime - startTime) / 1000
})
setSearchTerms(newSearchTerms)
}, [messages, keywords])
const highlightText = (text: string) => {
let highlightedText = removeMarkdown(text)
searchTerms.forEach((term) => {
const regex = new RegExp(term, 'gi')
highlightedText = highlightedText.replace(regex, (match) => `<mark>${match}</mark>`)
})
return <span dangerouslySetInnerHTML={{ __html: highlightedText }} />
}
useEffect(() => {
onSearch()
}, [onSearch])
return (
<Container ref={containerRef} {...props} onScroll={handleScroll}>
<ContainerWrapper>
{searchResults.length > 0 && (
<SearchStats>
Found {searchStats.count} results in {searchStats.time.toFixed(3)} seconds
</SearchStats>
)}
<List
itemLayout="vertical"
dataSource={searchResults}
pagination={{
pageSize: 10,
onChange: () => {
setTimeout(() => containerRef.current?.scrollTo({ top: 0 }), 0)
}
}}
renderItem={({ message, topic }) => (
<List.Item>
<Title
level={5}
style={{ color: 'var(--color-primary)', cursor: 'pointer' }}
onClick={async () => {
const _topic = await getTopicById(topic.id)
onTopicClick(_topic)
}}>
{topic.name}
</Title>
<div style={{ cursor: 'pointer' }} onClick={() => onMessageClick(message)}>
<Text>{highlightText(message.content)}</Text>
</div>
<SearchResultTime>
<Text type="secondary">{new Date(message.createdAt).toLocaleString()}</Text>
</SearchResultTime>
</List.Item>
)}
/>
<div style={{ minHeight: 30 }}></div>
</ContainerWrapper>
</Container>
)
}
const Container = styled.div`
width: 100%;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: row;
justify-content: center;
`
const ContainerWrapper = styled.div`
width: 800px;
display: flex;
flex-direction: column;
`
const SearchStats = styled.div`
font-size: 13px;
color: var(--color-text-3);
`
const SearchResultTime = styled.div`
margin-top: 10px;
`
export default memo(SearchResults)

View File

@@ -0,0 +1,82 @@
import { ArrowRightOutlined, MessageOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { getAssistantById } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { locateToMessage } from '@renderer/services/messages'
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'
interface Props extends React.HTMLAttributes<HTMLDivElement> {
topic?: Topic
}
const TopicMessages: FC<Props> = ({ topic, ...props }) => {
const navigate = useNavigate()
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const isEmpty = (topic?.messages || []).length === 0
if (!topic) {
return null
}
const onContinueChat = (topic: Topic) => {
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}>
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
{topic?.messages.map((message) => (
<div key={message.id} style={{ position: 'relative' }}>
<MessageItem message={message} showMenu={false} />
<Button
type="text"
size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
onClick={() => locateToMessage(navigate, message)}
icon={<ArrowRightOutlined />}
/>
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
</div>
))}
{isEmpty && <Empty />}
{!isEmpty && (
<HStack justifyContent="center">
<Button onClick={() => onContinueChat(topic)} icon={<MessageOutlined />}>
{t('history.continue_chat')}
</Button>
</HStack>
)}
</ContainerWrapper>
</MessagesContainer>
)
}
const MessagesContainer = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: scroll;
`
const ContainerWrapper = styled.div`
width: 800px;
display: flex;
flex-direction: column;
.message {
padding: 0;
}
`
export default TopicMessages

View File

@@ -0,0 +1,116 @@
import { useAssistants } from '@renderer/hooks/useAssistant'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { getTopicById } from '@renderer/hooks/useTopic'
import { Topic } from '@renderer/types'
import { Divider, Empty } from 'antd'
import dayjs from 'dayjs'
import { groupBy, isEmpty, orderBy } from 'lodash'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
type Props = {
keywords: string
onClick: (topic: Topic) => void
} & React.HTMLAttributes<HTMLDivElement>
const TopicsHistory: React.FC<Props> = ({ keywords, onClick, ...props }) => {
const { assistants } = useAssistants()
const { t } = useTranslation()
const { handleScroll, containerRef } = useScrollPosition('TopicsHistory')
const topics = orderBy(assistants.map((assistant) => assistant.topics).flat(), 'createdAt', 'desc')
const filteredTopics = topics.filter((topic) => {
return topic.name.toLowerCase().includes(keywords.toLowerCase())
})
const groupedTopics = groupBy(filteredTopics, (topic) => {
return dayjs(topic.createdAt).format('MM/DD')
})
if (isEmpty(filteredTopics)) {
return (
<ListContainer {...props}>
<ContainerWrapper>
<Empty description={t('history.search.topics.empty')} />
</ContainerWrapper>
</ListContainer>
)
}
return (
<ListContainer {...props} ref={containerRef} onScroll={handleScroll}>
<ContainerWrapper>
{Object.entries(groupedTopics).map(([date, items]) => (
<ListItem key={date}>
<Date>{date}</Date>
<Divider style={{ margin: '5px 0' }} />
{items.map((topic) => (
<TopicItem
key={topic.id}
onClick={async () => {
const _topic = await getTopicById(topic.id)
onClick(_topic)
}}>
<TopicName>{topic.name.substring(0, 50)}</TopicName>
<TopicDate>{dayjs(topic.updatedAt).format('HH:mm')}</TopicDate>
</TopicItem>
))}
</ListItem>
))}
<div style={{ minHeight: 30 }}></div>
</ContainerWrapper>
</ListContainer>
)
}
const ContainerWrapper = styled.div`
width: 800px;
display: flex;
flex-direction: column;
`
const ListContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow-y: scroll;
width: 100%;
align-items: center;
padding-top: 20px;
padding-bottom: 20px;
`
const ListItem = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 15px;
`
const Date = styled.div`
font-size: 26px;
font-weight: bold;
color: var(--color-text-3);
`
const TopicItem = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 30px;
`
const TopicName = styled.div`
font-size: 14px;
color: var(--color-text);
`
const TopicDate = styled.div`
font-size: 14px;
color: var(--color-text-3);
margin-left: 10px;
`
export default TopicsHistory

View File

@@ -1,10 +1,10 @@
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSearching } from '@renderer/store/runtime'
@@ -34,7 +34,7 @@ const Assistants: FC<Props> = ({
const generating = useAppSelector((state) => state.runtime.generating)
const [search, setSearch] = useState('')
const [dragging, setDragging] = useState(false)
const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id)
const { removeAllTopics } = useAssistant(activeAssistant.id)
const { clickAssistantToShowTopic, topicPosition } = useSettings()
const searchRef = useRef<InputRef>(null)
const { t } = useTranslation()
@@ -49,15 +49,6 @@ const Assistants: FC<Props> = ({
[assistants, onCreateDefaultAssistant, removeAssistant, setActiveAssistant]
)
const onEditAssistant = useCallback(
async (assistant: Assistant) => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
},
[updateAssistant]
)
const getMenuItems = useCallback(
(assistant: Assistant) =>
[
@@ -65,14 +56,14 @@ const Assistants: FC<Props> = ({
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
onClick: () => onEditAssistant(assistant)
onClick: () => AssistantSettingPopup.show({ assistant })
},
{
label: t('common.duplicate'),
key: 'duplicate',
icon: <CopyIcon />,
onClick: async () => {
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] }
addAssistant(_assistant)
setActiveAssistant(_assistant)
}
@@ -100,7 +91,7 @@ const Assistants: FC<Props> = ({
onClick: () => onDelete(assistant)
}
] as ItemType[],
[addAssistant, onDelete, onEditAssistant, removeAllTopics, setActiveAssistant, t]
[addAssistant, onDelete, removeAllTopics, setActiveAssistant, t]
)
const onSwitchAssistant = useCallback(
@@ -231,9 +222,9 @@ const AssistantItem = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 7px 10px;
padding: 7px 12px;
position: relative;
border-radius: 4px;
border-radius: 17px;
margin: 0 10px;
padding-right: 35px;
font-family: Ubuntu;
@@ -248,7 +239,6 @@ const AssistantItem = styled.div`
&.active {
background-color: var(--color-background-mute);
.name {
font-weight: 500;
}
.topics-count {
display: none;

View File

@@ -3,6 +3,7 @@ import { useShowAssistants } from '@renderer/hooks/useStore'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { Assistant } from '@renderer/types'
import { FC, useState } from 'react'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
import Chat from './Chat'
@@ -13,9 +14,13 @@ let _activeAssistant: Assistant
const HomePage: FC = () => {
const { assistants } = useAssistants()
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
const location = useLocation()
const state = location.state
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants } = useShowAssistants()
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant)
_activeAssistant = activeAssistant

View File

@@ -21,7 +21,6 @@ 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 { insertTextAtCursor } from '@renderer/utils/input'
import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
@@ -124,11 +123,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
const addNewTopic = useCallback(() => {
const topic = getDefaultTopic()
const topic = getDefaultTopic(assistant.id)
addTopic(topic)
setActiveTopic(topic)
db.topics.add({ id: topic.id, messages: [] })
}, [addTopic, setActiveTopic])
}, [addTopic, assistant.id, setActiveTopic])
const clearTopic = async () => {
if (generating) {
@@ -202,22 +201,20 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
if (pasteLongTextAsFile) {
const item = event.clipboardData?.items[0]
if (item && item.kind === 'string' && item.type === 'text/plain') {
event.preventDefault()
item.getAsString(async (pasteText) => {
if (pasteText.length > 1500) {
const tempFilePath = await window.api.file.create('pasted_text.txt')
await window.api.file.write(tempFilePath, pasteText)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
} else {
insertTextAtCursor({ text, pasteText, textareaRef, setText })
setText((prevText) => prevText.replace(pasteText, ''))
setTimeout(() => resizeTextArea(), 0)
}
})
}
}
},
[pasteLongTextAsFile, supportExts, text]
[pasteLongTextAsFile, supportExts]
)
// Command or Ctrl + N create new topic

View File

@@ -81,7 +81,7 @@ const CodeHeader = styled.div`
color: var(--color-text);
font-size: 14px;
font-weight: bold;
background-color: var(--color-code-background);
/* background-color: var(--color-code-background); */
height: 36px;
padding: 0 10px;
border-top-left-radius: 8px;

View File

@@ -1,5 +1,6 @@
import 'katex/dist/katex.min.css'
import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types'
import { escapeBrackets } from '@renderer/utils/formula'
import { isEmpty } from 'lodash'
@@ -27,6 +28,7 @@ const components = {
const Markdown: FC<Props> = ({ message }) => {
const { t } = useTranslation()
const { renderInputMessageAsMarkdown } = useSettings()
const messageContent = useMemo(() => {
const empty = isEmpty(message.content)
@@ -35,6 +37,10 @@ const Markdown: FC<Props> = ({ message }) => {
return escapeBrackets(content)
}, [message.content, message.status, t])
if (message.role === 'user' && !renderInputMessageAsMarkdown) {
return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
}
return (
<ReactMarkdown
className="markdown"

View File

@@ -1,25 +1,16 @@
import { SyncOutlined } from '@ant-design/icons'
import UserPopup from '@renderer/components/Popups/UserPopup'
import { FONT_FAMILY } from '@renderer/config/constant'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { startMinAppById } from '@renderer/config/minapps'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useModel } from '@renderer/hooks/useModel'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Alert, Avatar, Divider } from 'antd'
import dayjs from 'dayjs'
import { upperFirst } from 'lodash'
import { FC, memo, useCallback, useMemo } from 'react'
import { Divider } from 'antd'
import { FC, memo, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import Markdown from '../Markdown/Markdown'
import MessageAttachments from './MessageAttachments'
import MessageContent from './MessageContent'
import MessageHeader from './MessageHeader'
import MessageMenubar from './MessageMenubar'
import MessgeTokens from './MessageTokens'
@@ -28,43 +19,43 @@ interface Props {
index?: number
total?: number
lastMessage?: boolean
showMenu?: boolean
onEditMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => void
}
const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }) => {
const avatar = useAvatar()
const MessageItem: FC<Props> = ({ message, index, lastMessage, showMenu = true, onEditMessage, onDeleteMessage }) => {
const { t } = useTranslation()
const { assistant, setModel } = useAssistant(message.assistantId)
const model = useModel(message.modelId)
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
const { theme } = useTheme()
const { showMessageDivider, messageFont, fontSize } = useSettings()
const messageRef = useRef<HTMLDivElement>(null)
const isLastMessage = lastMessage || index === 0
const isAssistantMessage = message.role === 'assistant'
const getUserName = useCallback(() => {
if (isLocalAi && message.role !== 'user') return APP_NAME
if (message.role === 'assistant') return upperFirst(model?.name || model?.id)
return userName || t('common.you')
}, [message.role, model?.id, model?.name, t, userName])
const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
}, [messageFont])
const messageBorder = showMessageDivider ? undefined : 'none'
const avatarSource = useMemo(() => {
if (isLocalAi) return AppLogo
return message.modelId ? getModelLogo(message.modelId) : undefined
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message.modelId, theme])
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, () => {
if (messageRef.current) {
messageRef.current.scrollIntoView({ behavior: 'smooth' })
setTimeout(() => {
messageRef.current?.classList.add('message-highlight')
setTimeout(() => {
messageRef.current?.classList.remove('message-highlight')
}, 2500)
}, 500)
}
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [message])
if (message.type === 'clear') {
return (
@@ -75,39 +66,11 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }
}
return (
<MessageContainer key={message.id} className="message">
<MessageHeader>
<AvatarWrapper>
{isAssistantMessage ? (
<Avatar
src={avatarSource}
size={35}
style={{
borderRadius: '20%',
cursor: 'pointer',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined
}}
onClick={showMiniApp}>
{avatarName}
</Avatar>
) : (
<Avatar
src={avatar}
size={35}
style={{ borderRadius: '20%', cursor: 'pointer' }}
onClick={() => UserPopup.show()}
/>
)}
<UserWrap>
<UserName>{username}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
</MessageHeader>
<MessageContainer key={message.id} className="message" ref={messageRef}>
<MessageHeader message={message} assistant={assistant} model={model} />
<MessageContentContainer style={{ fontFamily, fontSize }}>
<MessageContent message={message} />
{!lastMessage && (
<MessageContent message={message} model={model} />
{!lastMessage && showMenu && (
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
<MessgeTokens message={message} />
<MessageMenubar
@@ -117,6 +80,7 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }
isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage}
setModel={setModel}
onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage}
/>
</MessageFooter>
@@ -126,41 +90,15 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }
)
}
const MessageContent: React.FC<{ message: Message }> = ({ message }) => {
const { t } = useTranslation()
if (message.status === 'sending') {
return (
<MessageContentLoading>
<SyncOutlined spin size={24} />
</MessageContentLoading>
)
}
if (message.status === 'error') {
return (
<Alert
message={<div style={{ fontSize: 14 }}>{t('error.chat.response')}</div>}
description={<Markdown message={message} />}
type="error"
style={{ marginBottom: 15, padding: 10, fontSize: 12 }}
/>
)
}
return (
<>
<Markdown message={message} />
<MessageAttachments message={message} />
</>
)
}
const MessageContainer = styled.div`
display: flex;
flex-direction: column;
padding: 0 20px;
padding: 15px 20px 0 20px;
position: relative;
transition: background-color 0.3s ease;
&.message-highlight {
background-color: var(--color-primary-mute);
}
.menubar {
opacity: 0;
transition: opacity 0.2s ease;
@@ -175,38 +113,6 @@ const MessageContainer = styled.div`
}
`
const MessageHeader = styled.div`
margin-right: 10px;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
justify-content: space-between;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 12px;
`
const UserName = styled.div`
font-size: 14px;
font-weight: 600;
`
const MessageTime = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MessageContentContainer = styled.div`
display: flex;
flex: 1;
@@ -222,15 +128,8 @@ const MessageFooter = styled.div`
justify-content: space-between;
align-items: center;
padding: 2px 0;
margin: 2px 0 8px 0;
margin-top: 2px;
border-top: 0.5px dashed var(--color-border);
`
const MessageContentLoading = styled.div`
display: flex;
flex-direction: row;
align-items: center;
height: 32px;
`
export default memo(MessageItem)

View File

@@ -0,0 +1,57 @@
import { SyncOutlined } from '@ant-design/icons'
import { Message, Model } from '@renderer/types'
import { getBriefInfo } from '@renderer/utils'
import { Alert } from 'antd'
import React from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import Markdown from '../Markdown/Markdown'
import MessageAttachments from './MessageAttachments'
const MessageContent: React.FC<{
message: Message
model?: Model
}> = ({ message, model }) => {
const { t } = useTranslation()
if (message.status === 'sending') {
return (
<MessageContentLoading>
<SyncOutlined spin size={24} />
</MessageContentLoading>
)
}
if (message.status === 'error') {
return (
<Alert
message={<div style={{ fontSize: 14 }}>{t('error.chat.response')}</div>}
description={<Markdown message={message} />}
type="error"
style={{ marginBottom: 15, padding: 10, fontSize: 12 }}
/>
)
}
if (message.type === '@' && model) {
const content = `[@${model.name}](#) ${getBriefInfo(message.content)}`
return <Markdown message={{ ...message, content }} />
}
return (
<>
<Markdown message={message} />
<MessageAttachments message={message} />
</>
)
}
const MessageContentLoading = styled.div`
display: flex;
flex-direction: row;
align-items: center;
height: 32px;
`
export default React.memo(MessageContent)

View File

@@ -0,0 +1,114 @@
import UserPopup from '@renderer/components/Popups/UserPopup'
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
import { startMinAppById } from '@renderer/config/minapps'
import { getModelLogo } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { Assistant, Message, Model } from '@renderer/types'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar } from 'antd'
import dayjs from 'dayjs'
import { upperFirst } from 'lodash'
import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
message: Message
assistant: Assistant
model?: Model
}
const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const { userName } = useSettings()
const { t } = useTranslation()
const isAssistantMessage = message.role === 'assistant'
const avatarSource = useMemo(() => {
if (isLocalAi) return AppLogo
return message.modelId ? getModelLogo(message.modelId) : undefined
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message.modelId, theme])
const getUserName = useCallback(() => {
if (isLocalAi && message.role !== 'user') return APP_NAME
if (message.role === 'assistant') return upperFirst(model?.name || model?.id)
return userName || t('common.you')
}, [message.role, model?.id, model?.name, t, userName])
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
return (
<Container>
<AvatarWrapper>
{isAssistantMessage ? (
<Avatar
src={avatarSource}
size={35}
style={{
borderRadius: '20%',
cursor: 'pointer',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined
}}
onClick={showMiniApp}>
{avatarName}
</Avatar>
) : (
<Avatar
src={avatar}
size={35}
style={{ borderRadius: '20%', cursor: 'pointer' }}
onClick={() => UserPopup.show()}
/>
)}
<UserWrap>
<UserName>{username}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
</Container>
)
}
const Container = styled.div`
margin-right: 10px;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
justify-content: space-between;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 12px;
`
const UserName = styled.div`
font-size: 14px;
font-weight: 600;
`
const MessageTime = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
export default MessageHeader

View File

@@ -8,6 +8,7 @@ import {
SaveOutlined,
SyncOutlined
} from '@ant-design/icons'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message, Model } from '@renderer/types'
import { removeTrailingDoubleSpaces } from '@renderer/utils'
@@ -25,19 +26,18 @@ interface Props {
isLastMessage: boolean
isAssistantMessage: boolean
setModel: (model: Model) => void
onEditMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => void
}
const MessageMenubar: FC<Props> = (props) => {
const { message, index, model, isLastMessage, isAssistantMessage, setModel, onDeleteMessage } = props
const { message, index, model, isLastMessage, isAssistantMessage, setModel, onEditMessage, onDeleteMessage } = props
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
const isUserMessage = message.role === 'user'
const canRegenerate = isLastMessage && isAssistantMessage
const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
window.message.success({ content: t('message.copied'), key: 'copy-message' })
@@ -57,6 +57,11 @@ const MessageMenubar: FC<Props> = (props) => {
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
}, [index])
const onEdit = useCallback(async () => {
const editedText = await TextEditPopup.show({ text: message.content })
editedText && onEditMessage?.({ ...message, content: editedText })
}, [message, onEditMessage])
const dropdownItems = useMemo(
() => [
{
@@ -67,9 +72,15 @@ const MessageMenubar: FC<Props> = (props) => {
const fileName = message.createdAt + '.md'
window.api.file.save(fileName, message.content)
}
},
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
onClick: onEdit
}
],
[t, message]
[message.content, message.createdAt, onEdit, t]
)
return (

View File

@@ -1,5 +1,6 @@
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { getTopic, TopicManager } from '@renderer/hooks/useTopic'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { getDefaultTopic } from '@renderer/services/assistant'
@@ -7,10 +8,10 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { deleteMessageFiles, filterMessages, getContextCount } from '@renderer/services/messages'
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/tokens'
import { Assistant, Message, Model, Topic } from '@renderer/types'
import { captureScrollableDiv, getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
import { captureScrollableDiv, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next'
import { flatten, last, reverse, take } from 'lodash'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import Suggestions from '../components/Suggestions'
@@ -28,6 +29,14 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const { updateTopic, addTopic } = useAssistant(assistant.id)
const { showTopics, topicPosition, showAssistants } = useSettings()
const maxWidth = useMemo(() => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth}`
}, [showAssistants, showTopics, topicPosition])
const onSendMessage = useCallback(
async (message: Message) => {
@@ -69,11 +78,27 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
[messages, topic.id]
)
const onEditMessage = useCallback(
(message: Message) => {
const _messages = messages.map((m) => (m.id === message.id ? message : m))
setMessages(_messages)
db.topics.update(topic.id, { messages: _messages })
},
[messages, topic.id]
)
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
await onSendMessage(msg)
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' })
// Scroll to bottom
setTimeout(
() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }),
10
)
// Fetch completion
fetchChatCompletion({
assistant,
messages: [...messages, msg],
@@ -89,8 +114,12 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
const lastUserMessage = last(filterMessages(messages).filter((m) => m.role === 'user'))
if (lastUserMessage) {
const content = `[@${model.name}](#) ${getBriefInfo(lastUserMessage.content)}`
onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', content })
onSendMessage({
...lastUserMessage,
id: uuid(),
type: '@',
modelId: model.id
})
fetchChatCompletion({
assistant,
topic,
@@ -135,7 +164,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
} as Message)
}),
EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => {
const newTopic = getDefaultTopic()
const newTopic = getDefaultTopic(assistant.id)
newTopic.name = topic.name
const branchMessages = take(messages, messages.length - index)
@@ -184,11 +213,17 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
}, [assistant, messages])
return (
<Container id="messages" key={assistant.id} ref={containerRef}>
<Container id="messages" style={{ maxWidth }} key={assistant.id} ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} lastMessage />}
{reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} index={index} onDeleteMessage={onDeleteMessage} />
<MessageItem
key={message.id}
message={message}
index={index}
onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage}
/>
))}
<Prompt assistant={assistant} key={assistant.prompt} />
</Container>
@@ -205,6 +240,7 @@ const Container = styled.div`
padding: 10px 0;
background-color: var(--color-background);
padding-bottom: 20px;
overflow-x: hidden;
`
export default Messages

View File

@@ -1,6 +1,4 @@
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@@ -12,22 +10,15 @@ interface Props {
const Prompt: FC<Props> = ({ assistant }) => {
const { t } = useTranslation()
const { updateAssistant } = useAssistant(assistant.id)
const prompt = assistant.prompt || t('chat.default.description')
const onEdit = async () => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
if (!prompt) {
return null
}
return (
<Container onClick={onEdit}>
<Container onClick={() => AssistantSettingPopup.show({ assistant })}>
<Text>{prompt}</Text>
</Container>
)
@@ -37,7 +28,7 @@ const Container = styled.div`
padding: 10px 20px;
background-color: var(--color-background-soft);
margin-bottom: 20px;
margin: 0 20px 20px 20px;
margin: 0 20px 0 20px;
border-radius: 6px;
cursor: pointer;
`

View File

@@ -1,14 +1,14 @@
import { FormOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import { HStack } from '@renderer/components/Layout'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { isMac, isWindows } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types'
import { Switch } from 'antd'
@@ -25,27 +25,21 @@ interface Props {
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
const { assistant, updateAssistant, addTopic } = useAssistant(activeAssistant.id)
const { assistant, addTopic } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { theme, toggleTheme } = useTheme()
const { topicPosition } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation()
const onEditAssistant = useCallback(async () => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}, [assistant, updateAssistant])
const addNewTopic = useCallback(() => {
const topic = getDefaultTopic()
const topic = getDefaultTopic(assistant.id)
addTopic(topic)
setActiveTopic(topic)
db.topics.add({ id: topic.id, messages: [] })
window.message.success({ content: t('message.topic.added'), key: 'topic-added' })
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, setActiveTopic, t])
}, [addTopic, assistant.id, setActiveTopic, t])
return (
<Navbar>
@@ -68,7 +62,10 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
<i className="iconfont icon-show-sidebar" />
</NewButton>
)}
<TitleText style={{ marginRight: 10, cursor: 'pointer' }} className="nodrag" onClick={onEditAssistant}>
<TitleText
style={{ marginRight: 10, cursor: 'pointer' }}
className="nodrag"
onClick={() => AssistantSettingPopup.show({ assistant })}>
{assistant.name}
</TitleText>
<SelectModelButton assistant={assistant} />

View File

@@ -103,6 +103,7 @@ const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssist
borderRadius: 0,
padding: '10px 0',
margin: '0 10px',
paddingBottom: 10,
borderBottom: '0.5px solid var(--color-border)',
gap: 2
}}

Some files were not shown because too many files have changed in this diff Show More