Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68d57ba238 | ||
|
|
cf98675223 | ||
|
|
4cc140e4f2 | ||
|
|
2da3a3f010 | ||
|
|
fa6f7ecab0 | ||
|
|
31ab444300 | ||
|
|
85453f5a3a | ||
|
|
6d92539524 | ||
|
|
a605ae6043 | ||
|
|
6aaa6bf042 | ||
|
|
aa578194c7 | ||
|
|
220600070c | ||
|
|
32cdfbbfb0 | ||
|
|
33b83bf242 | ||
|
|
2e1b433365 | ||
|
|
2771a842fe | ||
|
|
4af3d16e61 | ||
|
|
eb47fb051b | ||
|
|
0f9655611b | ||
|
|
0c72ccac12 | ||
|
|
09f7fcd2b4 | ||
|
|
b9250df347 | ||
|
|
ca897db0d2 | ||
|
|
af75d4139c | ||
|
|
d2e35a888d | ||
|
|
fb56c3744b | ||
|
|
26942cfd1f | ||
|
|
1601fc6d81 | ||
|
|
f543a9ff80 | ||
|
|
5299a2a687 | ||
|
|
fcc627db6f | ||
|
|
1035019fc2 | ||
|
|
9d311a7261 | ||
|
|
a973c5fb89 | ||
|
|
be081ccf7a | ||
|
|
c25db02acf | ||
|
|
01f98235c6 | ||
|
|
00f3b87215 | ||
|
|
849958eeec | ||
|
|
9655153e01 |
14
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 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)
|
||||
|
||||
@@ -87,6 +87,10 @@ $ yarn build:linux
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
# 赞助
|
||||
|
||||
[微信赞赏码](docs/sponsor.md)
|
||||
|
||||
# 📃 许可证
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
5
docs/Sponsor.md
Normal 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
@@ -0,0 +1,95 @@
|
||||
# FAQ 文档
|
||||
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
|
||||
|
||||
## 问题1:Cherry Studio 支持哪些操作系统?
|
||||
- **答案**:Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
|
||||
|
||||
## 问题2:Cherry Studio 的主要功能有哪些?
|
||||
- **答案**:Cherry Studio 的主要功能包括:
|
||||
1. 支持多个 LLM 提供商
|
||||
2. 允许创建多个助手
|
||||
3. 支持创建多个主题
|
||||
4. 允许在同一对话中使用多个模型来回答问题
|
||||
5. 支持拖放排序
|
||||
6. 代码高亮
|
||||
7. Mermaid 图表支持
|
||||
|
||||
## 问题3:Cherry 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`
|
||||
|
||||
## 问题5:Cherry 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
|
||||
|
||||
## 问题7:Cherry Studio 的 `/src` 目录主要包含哪些内容?
|
||||
- **答案**:Cherry Studio 的 `/src` 目录主要包含以下内容:
|
||||
- 主进程代码(Electron 主进程)
|
||||
- 渲染进程代码(用户界面)
|
||||
- 组件
|
||||
- 工具函数
|
||||
- 状态管理
|
||||
- 样式文件
|
||||
|
||||
## 问题8:如何在 Cherry Studio 中添加新的 LLM 提供商?
|
||||
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
|
||||
1. 在 `/src/services` 或类似目录下创建新的服务文件
|
||||
2. 实现与新 LLM 提供商 API 的集成
|
||||
3. 在用户界面中添加新提供商的选项
|
||||
4. 更新配置和状态管理以支持新提供商
|
||||
|
||||
## 问题9:Cherry 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. 在用户界面中添加新主题的选项
|
||||
|
||||
## 问题11:Cherry 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
@@ -0,0 +1,95 @@
|
||||
# FAQ 文档
|
||||
本文档适用于:产品手册、官网页面、课程测验、现场 Q&A。
|
||||
|
||||
## 问题1:Cherry Studio 支持哪些操作系统?
|
||||
- **答案**:Cherry Studio 支持 Windows、Mac 和 Linux 操作系统。
|
||||
|
||||
## 问题2:Cherry Studio 的主要功能有哪些?
|
||||
- **答案**:Cherry Studio 的主要功能包括:
|
||||
1. 支持多个 LLM 提供商
|
||||
2. 允许创建多个助手
|
||||
3. 支持创建多个主题
|
||||
4. 允许在同一对话中使用多个模型来回答问题
|
||||
5. 支持拖放排序
|
||||
6. 代码高亮
|
||||
7. Mermaid 图表支持
|
||||
|
||||
## 问题3:Cherry 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`
|
||||
|
||||
## 问题5:Cherry 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
|
||||
|
||||
## 问题7:Cherry Studio 的 `/src` 目录主要包含哪些内容?
|
||||
- **答案**:Cherry Studio 的 `/src` 目录主要包含以下内容:
|
||||
- 主进程代码(Electron 主进程)
|
||||
- 渲染进程代码(用户界面)
|
||||
- 组件
|
||||
- 工具函数
|
||||
- 状态管理
|
||||
- 样式文件
|
||||
|
||||
## 问题8:如何在 Cherry Studio 中添加新的 LLM 提供商?
|
||||
- **答案**:要在 Cherry Studio 中添加新的 LLM 提供商,你需要:
|
||||
1. 在 `/src/services` 或类似目录下创建新的服务文件
|
||||
2. 实现与新 LLM 提供商 API 的集成
|
||||
3. 在用户界面中添加新提供商的选项
|
||||
4. 更新配置和状态管理以支持新提供商
|
||||
|
||||
## 问题9:Cherry 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. 在用户界面中添加新主题的选项
|
||||
|
||||
## 问题11:Cherry 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
@@ -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
@@ -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>
|
||||
@@ -65,15 +65,9 @@ afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
本次更新:
|
||||
增加流式输出开关
|
||||
Windows 安装程序支持修改安装位置了
|
||||
服务商和小程序图标更新
|
||||
增加 ocoolAI 服务商
|
||||
小程序增加 HuggingChat
|
||||
Gemini 模型回复安全级别关闭
|
||||
修复 macOS 切换窗口透明不生效问题
|
||||
修复消息回复完成界面会自动滚动到最底部的问题
|
||||
增加话题历史记录
|
||||
增加消息搜索功能
|
||||
近期更新:
|
||||
全新应用图标
|
||||
模型图标更新
|
||||
支持 Linux ARM 架构
|
||||
增加 WebDAV 备份功能 by @DrayChou
|
||||
增加使用 Markdown 渲染用户消息开关
|
||||
增加 Felo 小程序
|
||||
|
||||
10
package.json
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
110
src/main/services/BackupManager.ts
Normal 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
|
||||
@@ -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
|
||||
|
||||
66
src/main/services/WebDav.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
13
src/preload/index.d.ts
vendored
@@ -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 }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
BIN
src/renderer/src/assets/images/apps/felo.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src/renderer/src/assets/images/apps/qingyan.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/renderer/src/assets/images/models/adept.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/renderer/src/assets/images/models/adept_dark.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/models/ai21.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src/renderer/src/assets/images/models/ai21_dark.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/models/aisingapore.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/models/aisingapore_dark.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/renderer/src/assets/images/models/bigcode.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/renderer/src/assets/images/models/bigcode_dark.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/renderer/src/assets/images/models/dianxin.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src/renderer/src/assets/images/models/dianxin_dark.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/renderer/src/assets/images/models/google.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src/renderer/src/assets/images/models/gryphe.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/renderer/src/assets/images/models/gryphe_dark.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/renderer/src/assets/images/models/huggingface.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/renderer/src/assets/images/models/huggingface_dark.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/renderer/src/assets/images/models/ibm.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/models/ibm_dark.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/models/keling.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/models/keling_dark.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/models/luma.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/renderer/src/assets/images/models/luma_dark.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/renderer/src/assets/images/models/mediatek.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/renderer/src/assets/images/models/mediatek_dark.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/renderer/src/assets/images/models/nousresearch.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/models/nvidia.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/renderer/src/assets/images/models/nvidia_dark.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
src/renderer/src/assets/images/models/rakutenai.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/models/rakutenai_dark.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
src/renderer/src/assets/images/models/sparkdesk.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/renderer/src/assets/images/models/sparkdesk_dark.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/renderer/src/assets/images/models/suno.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/models/suno_dark.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/models/tele.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/renderer/src/assets/images/models/tele_dark.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/renderer/src/assets/images/models/upstage.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/renderer/src/assets/images/models/upstage_dark.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/renderer/src/assets/images/models/vidu.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/renderer/src/assets/images/models/vidu_dark.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/providers/fireworks.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/providers/nvidia.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
@@ -41,6 +41,9 @@
|
||||
}
|
||||
|
||||
.segmented-tab {
|
||||
.ant-segmented-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
.ant-segmented-item-selected {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
154
src/renderer/src/components/AssistantSettings/index.tsx
Normal 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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
98
src/renderer/src/components/Popups/TextEditPopup.tsx
Normal 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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useDefaultAssistant() {
|
||||
return {
|
||||
defaultAssistant: {
|
||||
...defaultAssistant,
|
||||
topics: [getDefaultTopic()]
|
||||
topics: [getDefaultTopic(defaultAssistant.id)]
|
||||
},
|
||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
||||
}
|
||||
|
||||
20
src/renderer/src/hooks/useScrollPosition.ts
Normal 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 }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
312
src/renderer/src/i18n/en-us.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
312
src/renderer/src/i18n/zh-cn.json
Normal 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": "可视化"
|
||||
}
|
||||
}
|
||||
}
|
||||
312
src/renderer/src/i18n/zh-tw.json
Normal 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": "可視化"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 />)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
157
src/renderer/src/pages/history/HistoryPage.tsx
Normal 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
|
||||
62
src/renderer/src/pages/history/components/SearchMessage.tsx
Normal 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
|
||||
154
src/renderer/src/pages/history/components/SearchResults.tsx
Normal 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)
|
||||
82
src/renderer/src/pages/history/components/TopicMessages.tsx
Normal 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
|
||||
116
src/renderer/src/pages/history/components/TopicsHistory.tsx
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
57
src/renderer/src/pages/home/Messages/MessageContent.tsx
Normal 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)
|
||||
114
src/renderer/src/pages/home/Messages/MessageHeader.tsx
Normal 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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||