Compare commits

...

32 Commits

Author SHA1 Message Date
kangfenmao
6f502049f4 chore(version): 0.8.0 2024-10-14 14:57:19 +08:00
kangfenmao
c68ad4febb feat: add artifacts preview 2024-10-14 14:37:04 +08:00
kangfenmao
2ebcec9f59 feat: add event listeners and topic handling improvements #181
- Added event listeners for estimated token count and add new topic events.
- Updated default topic handling when clearing messages.
- Removed feature to add new topics directly in Navbar and replaced it with emitting an event to create a new topic.
- Added the functionality to add new topics, clear messages, and handle topic switching with improved conditional logic.
- The event constants configuration has been updated to include two new event names.
2024-10-14 10:39:14 +08:00
kangfenmao
7bc74a5b86 feat: add clear message menu to topic context menu 2024-10-14 10:19:48 +08:00
kangfenmao
75152421d9 fix: DashScope upgrade 2024-10-14 09:57:56 +08:00
1355873789
3326074076 chore: 更新 provider 名称, Dashscope 更新为 Bailian 2024-10-14 09:17:01 +08:00
kangfenmao
362d82bdcc fix: text input token caused stuttering 2024-10-13 00:50:28 +08:00
kangfenmao
fcce241c82 style: improved visual separation and aesthetic
- Added a border radius to scrollbar thumb styles for improved aesthetic.
- Updated the Divider component to include a border for better visual separation.
- Added border to the divider in SelectModelPopup for improved visibility.
2024-10-13 00:38:13 +08:00
kangfenmao
693b06c126 docs: remove uppercase filename docs 2024-10-12 23:24:00 +08:00
kangfenmao
c310c71576 fix: 长文本输入时生成文件后文本依旧保留 #179 2024-10-12 23:22:32 +08:00
kangfenmao
bea95fc52f fix: 使用滚动条显示不全 #176 2024-10-12 17:42:16 +08:00
kangfenmao
969cf8ea21 fix: 移除 input 等输入标签的渲染 2024-10-12 17:37:56 +08:00
kangfenmao
5b357f14e5 chore(version): 0.7.16 2024-10-12 15:31:21 +08:00
kangfenmao
de5db4f805 fix: 修复无法正常选择文本文档的问题 2024-10-12 14:56:17 +08:00
kangfenmao
1ccb5edda7 chore(version): 0.7.15 2024-10-12 14:14:46 +08:00
kangfenmao
97b8749dd1 fix: 一键返回到消息顶部 #166
close #166
2024-10-12 14:03:06 +08:00
kangfenmao
a6d7ecae81 fix: 自定义界面字体 #158 2024-10-12 13:57:45 +08:00
kangfenmao
938efb5aef refactor: renamed model display names and fixed logic
- Renamed the display of model names to show the exact model name instead of capitalized first letter.
- Fixed logic to handle model name retrieval for assistant messages.
- Renaming of model display name to use the model's original name instead of a capitalized version.
- Removed unnecessary import and corrected label formatting in the options array.
2024-10-12 13:52:17 +08:00
kangfenmao
9baf0f772e fix: 黑暗模式的启动页是白色的 #118
close #118
2024-10-12 13:40:34 +08:00
kangfenmao
ff5de3625e fix: o1模型设置使用优化 #172 2024-10-12 13:28:42 +08:00
kangfenmao
f1cfdb29f8 feat: add document files support 2024-10-12 13:18:53 +08:00
kangfenmao
26e48f07fd fix: old version of the backup file cannot be restored. 2024-10-12 10:09:52 +08:00
kangfenmao
8bb5fb9811 feat: add event handling to blur current target element after showing popup
- Added event handling to the onSelectModel function to blur the current target element after showing the SelectModelPopup.
2024-10-12 10:00:03 +08:00
kangfenmao
d41667b599 feat: update release notes and add image preview component
- Updated release notes to reflect changes including image preview and download.
- Added interactive image preview component with toolbar for rotation, zooming, and downloading.
- Added support for image previews in Markdown rendering.
- Added functionality to download files from a URL with automatic filename detection and handling.
2024-10-12 09:53:20 +08:00
kangfenmao
85152cbcd7 chore(version): 0.7.14 2024-10-11 23:22:51 +08:00
kangfenmao
b80863111f feat: update release notes and fix issues
- This commit updates release notes to include new features and fix existing issues.
- Removed non-essential keyboard shortcuts from context menu items.
2024-10-11 18:04:08 +08:00
kangfenmao
6cd88fa51d feat: add bolt minapp 2024-10-11 14:15:37 +08:00
kangfenmao
3619e8f47b style: updated ui styles and translations
- Adjusted padding styles in AssistantModelSettings component.
- The addition of a close button to the Assistant Prompt Settings component to enhance its functionality.
- Added OK callback event to AssistantPromptSettings component.
- Added translations for new UI elements.
- Updated translation data for Chinese language.
- Added new translations and updated existing values in the language file to incorporate additional features.
2024-10-11 14:05:50 +08:00
kangfenmao
5b41dd24d4 refactor: regenerate model on selection
- Updated the logic in the `onSelectModel` function to regenerate the model when a selection is made.
2024-10-11 13:49:06 +08:00
kangfenmao
91dd2f233a fix: azure openai model provider wrong 2024-10-11 13:42:36 +08:00
kangfenmao
7e651f9abc feat: quickly select model 2024-10-11 13:31:14 +08:00
kangfenmao
e44f666c5c refactor: latex解析不支持矩阵环境 #169 2024-10-11 10:15:46 +08:00
65 changed files with 1478 additions and 519 deletions

View File

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

View File

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

View File

@@ -65,9 +65,12 @@ afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
本次更新:
增加 Azure OpenAI 服务商
修复表格换行问题
增加 Artifacts 网页预览功能
内置助理新增网页生成助理
DashScope 服务商修改为阿里云百炼
修复粘贴长文本后不能自动清除的问题
话题右键菜单增加删除消息功能
修复选择模型弹窗滚动条消失问题
近期更新:
增加 WebDAV 备份功能 by @DrayChou
增加话题历史记录
增加消息搜索功能
支持 PDF, DOC等办公文件格式
支持图片的预览和下载

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.7.13",
"version": "0.8.0",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -39,6 +39,7 @@
"electron-window-state": "^5.0.3",
"fs-extra": "^11.2.0",
"html2canvas": "^1.4.1",
"officeparser": "^4.1.1",
"unzipper": "^0.12.3",
"webdav": "4.11.4"
},
@@ -96,7 +97,7 @@
"react-syntax-highlighter": "^15.5.0",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"rehype-katex": "^7.0.0",
"rehype-mathjax": "^6.0.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",

94
src/main/constant.ts Normal file
View File

@@ -0,0 +1,94 @@
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.properties', // 配置属性文件
'.latex', // LaTeX 文档文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.gradle', // Gradle 构建文件
'.kts' // Kotlin Script 文件
]

View File

@@ -4,6 +4,7 @@ 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()
@@ -29,6 +30,8 @@ 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)

View File

@@ -1,3 +1,4 @@
import { documentExts } from '@main/constant'
import { getFileType } from '@main/utils/file'
import { FileType } from '@types'
import * as crypto from 'crypto'
@@ -13,11 +14,14 @@ import logger from 'electron-log'
import * as fs from 'fs'
import { writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
import officeParser from 'officeparser'
import * as path from 'path'
import { chdir } from 'process'
import { v4 as uuidv4 } from 'uuid'
class FileManager {
private storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
private tempDir = path.join(app.getPath('temp'), 'CherryStudio')
constructor() {
this.initStorageDir()
@@ -27,6 +31,9 @@ class FileManager {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
}
private getFileHash = async (filePath: string): Promise<string> => {
@@ -173,15 +180,29 @@ class FileManager {
public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<string> => {
const filePath = path.join(this.storageDir, id)
if (documentExts.includes(path.extname(filePath))) {
const originalCwd = process.cwd()
try {
chdir(this.tempDir)
const data = await officeParser.parseOfficeAsync(filePath)
chdir(originalCwd)
return data
} catch (error) {
chdir(originalCwd)
logger.error(error)
throw error
}
}
return fs.readFileSync(filePath, 'utf8')
}
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 })
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true })
}
const tempFilePath = path.join(tempDir, `temp_file_${uuidv4()}_${fileName}`)
const tempFilePath = path.join(this.tempDir, `temp_file_${uuidv4()}_${fileName}`)
return tempFilePath
}

View File

@@ -1,101 +1,8 @@
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
import { FileTypes } from '../../renderer/src/types'
export function getFileType(ext: string): FileTypes {
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.properties', // 配置属性文件
'.latex', // LaTeX 文档文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.gradle', // Gradle 构建文件
'.kts' // Kotlin Script 文件
]
ext = ext.toLowerCase()
if (imageExts.includes(ext)) return FileTypes.IMAGE
if (videoExts.includes(ext)) return FileTypes.VIDEO

39
src/main/utils/zip.ts Normal file
View File

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

View File

@@ -16,6 +16,8 @@ export function createMainWindow() {
const theme = appConfig.get('theme') || 'light'
// Create the browser window.
const isMac = process.platform === 'darwin'
const mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
@@ -25,11 +27,12 @@ export function createMainWindow() {
minHeight: 600,
show: true,
autoHideMenuBar: true,
transparent: process.platform === 'darwin',
transparent: isMac,
vibrancy: 'fullscreen-ui',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
@@ -45,11 +48,9 @@ export function createMainWindow() {
mainWindow.webContents.on('context-menu', () => {
const menu = new Menu()
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
menu.append(new MenuItem({ label: '复制', role: 'copy' }))
menu.append(new MenuItem({ label: '粘贴', role: 'paste' }))
menu.append(new MenuItem({ label: '剪切', role: 'cut' }))
menu.popup()
})

View File

@@ -11,6 +11,8 @@ 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),

View File

@@ -1,36 +1,40 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
<style>
html,
body {
margin: 0;
}
#spinner {
position: fixed;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.5);
}
#spinner img {
width: 100px;
}
</style>
</head>
<body>
<div id="root"></div>
<div id="spinner">
<img src="/src/assets/images/logo/cherry-text.svg" />
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
<style>
html,
body {
margin: 0;
}
#spinner {
position: fixed;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
#spinner img {
width: 100px;
border-radius: 50px;
}
</style>
</head>
<body>
<div id="root"></div>
<div id="spinner">
<img src="/src/assets/images/logo.png" />
</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" rx="4" fill="black"/>
<g filter="url(#filter0_i_2119_154)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64368 11.7731C7.91976 11.7731 7.20901 11.5147 6.80099 10.9591L6.65707 11.6143L4 13L4.28684 11.6143L6.22186 3H8.59103L7.9066 6.03634C8.45941 5.44199 8.97273 5.22234 9.63083 5.22234C11.0523 5.22234 12 6.1397 12 7.81938C12 9.55074 10.9076 11.7731 8.64368 11.7731ZM9.55186 8.31036C9.55186 9.11144 8.97273 9.71871 8.22249 9.71871C7.8013 9.71871 7.4196 9.56366 7.16952 9.29233L7.53806 7.70309C7.81447 7.43176 8.13036 7.27671 8.49889 7.27671C9.06486 7.27671 9.55186 7.69017 9.55186 8.31036Z" fill="white"/>
</g>
<defs>
<filter id="filter0_i_2119_154" x="4" y="3" width="8" height="10" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.0192413"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.95 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2119_154"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -2,10 +2,8 @@
resize: none;
}
.chat-nav-dropdown {
.ant-dropdown-menu {
padding-bottom: 12px;
}
.ant-btn:not(:disabled):focus-visible {
outline: none;
}
.ant-segmented-group {

View File

@@ -55,6 +55,8 @@
p {
margin: 1em 0;
white-space: pre-wrap;
&:last-child {
margin-bottom: 5px;
}

View File

@@ -1,6 +1,6 @@
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 2px;
width: 4px;
height: 2px;
}
@@ -10,6 +10,7 @@
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
border-radius: 4px;
&:hover {
background: var(--color-scrollbar-thumb-hover);
}

View File

@@ -166,7 +166,7 @@ const Container = styled.div`
flex: 1;
flex-direction: column;
overflow: hidden;
padding-bottom: 10px;
padding: 5px;
`
const Label = styled.p`

View File

@@ -1,14 +1,14 @@
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { Input } from 'antd'
import { Button, Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box, VStack } from '../Layout'
import { Box, HStack, VStack } from '../Layout'
const AssistantPromptSettings: React.FC<{ assistant: Assistant }> = (props) => {
const AssistantPromptSettings: React.FC<{ assistant: Assistant; onOk: () => void }> = (props) => {
const { assistant, updateAssistant } = useAssistant(props.assistant.id)
const [name, setName] = useState(assistant.name)
const [prompt, setPrompt] = useState(assistant.prompt)
@@ -22,14 +22,16 @@ const AssistantPromptSettings: React.FC<{ assistant: Assistant }> = (props) => {
return (
<VStack flex={1}>
<Box mb={8}>{t('common.name')}</Box>
<Box mb={8} style={{ fontWeight: 'bold' }}>
{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}>
<Box mt={8} mb={8} style={{ fontWeight: 'bold' }}>
{t('common.prompt')}
</Box>
<TextArea
@@ -38,8 +40,13 @@ const AssistantPromptSettings: React.FC<{ assistant: Assistant }> = (props) => {
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onBlur={onUpdate}
style={{ minHeight: 'calc(80vh - 150px)', maxHeight: 'calc(80vh - 150px)' }}
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }}
/>
<HStack width="100%" justifyContent="flex-end" mt="10px">
<Button type="primary" onClick={props.onOk}>
{t('common.close')}
</Button>
</HStack>
</VStack>
)
}

View File

@@ -81,7 +81,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
/>
</LeftMenu>
<Settings>
{menu === 'prompt' && <AssistantPromptSettings assistant={assistant} />}
{menu === 'prompt' && <AssistantPromptSettings assistant={assistant} onOk={onOk} />}
{menu === 'model' && <AssistantModelSettings assistant={assistant} />}
</Settings>
</HStack>

View File

@@ -102,7 +102,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
size="middle"
/>
</HStack>
<Divider style={{ margin: 0 }} />
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Container>
{agents.map((agent) => (
<AgentItem

View File

@@ -0,0 +1,180 @@
import { SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isVisionModel } from '@renderer/config/models'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/model'
import { Model } from '@renderer/types'
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
import { first, reverse, sortBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { HStack } from '../Layout'
type MenuItem = Required<MenuProps>['items'][number]
interface Props {
model?: Model
}
interface PopupContainerProps extends Props {
resolve: (value: Model | undefined) => void
}
const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(null)
const { providers } = useProviders()
const filteredItems: MenuItem[] = providers
.filter((p) => p.models && p.models.length > 0)
.map((p) => ({
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: reverse(sortBy(p.models, 'name'))
.filter((m) => m.name.toLowerCase().includes(searchText.toLowerCase()))
.map((m) => ({
key: getModelUniqId(m),
label: (
<ModelItem>
{m?.name} {isVisionModel(m) && <VisionIcon />}
</ModelItem>
),
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => {
resolve(m)
setOpen(false)
}
}))
}))
.filter((item) => item.children && item.children.length > 0) as MenuItem[]
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
resolve(undefined)
SelectModelPopup.hide()
}
useEffect(() => {
open && setTimeout(() => inputRef.current?.focus(), 0)
}, [open])
return (
<Modal
centered
open={open}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
closeIcon={null}
footer={null}>
<HStack style={{ padding: '0 12px', marginTop: 5 }}>
<Input
prefix={
<SearchIcon>
<SearchOutlined />
</SearchIcon>
}
ref={inputRef}
placeholder={t('model.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
autoFocus
style={{ paddingLeft: 0 }}
bordered={false}
size="middle"
/>
</HStack>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Container>
{filteredItems.length > 0 ? (
<StyledMenu
items={filteredItems}
selectedKeys={model ? [getModelUniqId(model)] : []}
mode="inline"
inlineIndent={6}
/>
) : (
<EmptyState>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</EmptyState>
)}
</Container>
</Modal>
)
}
const Container = styled.div`
height: 50vh;
margin-top: 10px;
overflow-y: auto;
`
const StyledMenu = styled(Menu)`
background-color: transparent;
padding: 5px;
margin-top: -10px;
max-height: calc(60vh - 50px);
.ant-menu-item-group-title {
padding: 5px 10px 0;
font-size: 12px;
}
.ant-menu-item {
height: 36px;
line-height: 36px;
}
`
const ModelItem = styled.div`
display: flex;
align-items: center;
font-size: 14px;
`
const EmptyState = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 200px;
`
const SearchIcon = styled.div`
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
margin-right: 2px;
`
export default class SelectModelPopup {
static topviewId = 0
static hide() {
TopView.hide('SelectModelPopup')
}
static show(params: Props) {
return new Promise<Model | undefined>((resolve) => {
TopView.show(<PopupContainer {...params} resolve={resolve} />, 'SelectModelPopup')
})
}
}

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@ export const isWindows = platform === 'win32' || platform === 'win64'
export const isLinux = platform === 'linux'
export const imageExts = ['.jpg', '.png', '.jpeg']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件

View File

@@ -1,10 +1,11 @@
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 BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
@@ -207,6 +208,13 @@ const _apps: MinAppType[] = [
logo: FeloAppLogo,
url: 'https://felo.ai/',
bodered: true
},
{
id: 'bolt',
name: 'bolt',
logo: BoltAppLogo,
url: 'https://bolt.new/',
bodered: true
}
]

View File

@@ -346,13 +346,13 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
'azure-openai': [
{
id: 'gpt-4o',
provider: 'openai',
provider: 'azure-openai',
name: ' GPT-4o',
group: 'GPT 4o'
},
{
id: 'gpt-4o-mini',
provider: 'openai',
provider: 'azure-openai',
name: ' GPT-4o-mini',
group: 'GPT 4o'
}
@@ -695,7 +695,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Baichuan3'
}
],
dashscope: [
bailian: [
{
id: 'qwen-turbo',
provider: 'dashscope',

View File

@@ -3,8 +3,8 @@ import AzureProviderLogo from '@renderer/assets/images/models/microsoft.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 BailianProviderLogo from '@renderer/assets/images/providers/bailian.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'
@@ -47,7 +47,7 @@ export function getProviderLogo(providerId: string) {
case 'baichuan':
return BaichuanProviderLogo
case 'dashscope':
return DashScopeProviderLogo
return BailianProviderLogo
case 'anthropic':
return AnthropicProviderLogo
case 'aihubmix':
@@ -208,10 +208,10 @@ export const PROVIDER_CONFIG = {
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/'
},
websites: {
official: 'https://dashscope.aliyun.com/',
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
docs: 'https://help.aliyun.com/zh/dashscope/',
models: 'https://dashscope.console.aliyun.com/model'
official: 'https://www.aliyun.com/product/bailian',
apiKey: 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
docs: 'https://help.aliyun.com/zh/model-studio/getting-started/',
models: 'https://bailian.console.aliyun.com/model-market#/model-market'
}
},
stepfun: {

View File

@@ -22,7 +22,7 @@ export function useProviders() {
const dispatch = useAppDispatch()
return {
providers,
providers: providers || {},
addProvider: (provider: Provider) => dispatch(addProvider(provider)),
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),

View File

@@ -27,7 +27,8 @@
"default": "Default",
"warning": "Warning",
"back": "Back",
"chat": "Chat"
"chat": "Chat",
"close": "Close"
},
"button": {
"add": "Add",
@@ -71,6 +72,7 @@
"topics.auto_rename": "Auto Rename",
"topics.edit.title": "Edit Name",
"topics.edit.placeholder": "Enter new name",
"topics.clear.title": "Clear Messages",
"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",
@@ -89,7 +91,7 @@
"input.send": "Send",
"input.pause": "Pause",
"input.settings": "Settings",
"input.upload": "Upload image or text file",
"input.upload": "Upload image or document file",
"input.context_count.tip": "Context Count",
"input.estimated_tokens.tip": "Estimated tokens",
"settings.temperature": "Temperature",
@@ -105,7 +107,9 @@
"add.assistant.title": "Add Assistant",
"message.new.context": "New Context",
"message.new.branch": "New Branch",
"assistant.search.placeholder": "Search"
"assistant.search.placeholder": "Search",
"artifacts.button.preview": "Preview",
"artifacts.button.download": "Download"
},
"assistants": {
"title": "Assistants",
@@ -115,7 +119,8 @@
"model_settings": "Model Settings"
},
"model": {
"stream_output": "Stream Output"
"stream_output": "Stream Output",
"search": "Search models..."
},
"files": {
"title": "Files",
@@ -167,7 +172,7 @@
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "Baichuan",
"dashscope": "DashScope",
"dashscope": "Alibaba Cloud",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "StepFun",

View File

@@ -27,7 +27,8 @@
"default": "默认",
"warning": "警告",
"back": "返回",
"chat": "聊天"
"chat": "聊天",
"close": "关闭"
},
"button": {
"add": "添加",
@@ -71,6 +72,7 @@
"topics.auto_rename": "生成话题名",
"topics.edit.title": "编辑话题名",
"topics.edit.placeholder": "输入新名称",
"topics.clear.title": "清空消息",
"topics.delete.all.title": "删除所有话题",
"topics.delete.all.content": "确定要删除所有话题吗?",
"topics.move_to": "移动到",
@@ -83,13 +85,13 @@
"input.new.context": "清除上下文",
"input.expand": "展开",
"input.collapse": "收起",
"input.clear.title": "清消息?",
"input.clear.title": "清消息",
"input.clear.content": "确定要清除当前会话所有消息吗?",
"input.placeholder": "在这里输入消息...",
"input.send": "发送",
"input.pause": "暂停",
"input.settings": "设置",
"input.upload": "上传图片或纯文本文件",
"input.upload": "上传图片或文档",
"input.context_count.tip": "上下文数",
"input.estimated_tokens.tip": "预估 token 数",
"settings.temperature": "模型温度",
@@ -105,7 +107,9 @@
"add.assistant.title": "添加助手",
"message.new.context": "清除上下文",
"message.new.branch": "新分支",
"assistant.search.placeholder": "搜索"
"assistant.search.placeholder": "搜索",
"artifacts.button.preview": "预览",
"artifacts.button.download": "下载"
},
"assistants": {
"title": "助手",
@@ -115,7 +119,8 @@
"model_settings": "模型设置"
},
"model": {
"stream_output": "流式输出"
"stream_output": "流式输出",
"search": "搜索模型..."
},
"files": {
"title": "文件",
@@ -167,7 +172,7 @@
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "百川",
"dashscope": "阿里云灵积",
"dashscope": "阿里云百炼",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "阶跃星辰",

View File

@@ -27,7 +27,8 @@
"default": "預設",
"warning": "警告",
"back": "返回",
"chat": "聊天"
"chat": "聊天",
"close": "關閉"
},
"button": {
"add": "添加",
@@ -71,6 +72,7 @@
"topics.auto_rename": "自動重新命名",
"topics.edit.title": "編輯名稱",
"topics.edit.placeholder": "輸入新名稱",
"topics.clear.title": "清空消息",
"topics.delete.all.title": "刪除所有話題",
"topics.delete.all.content": "確定要刪除所有話題嗎?",
"topics.move_to": "移動到",
@@ -89,7 +91,7 @@
"input.send": "發送",
"input.pause": "暫停",
"input.settings": "設定",
"input.upload": "上傳圖片或文檔",
"input.upload": "上傳圖片或文檔",
"input.context_count.tip": "上下文數量",
"input.estimated_tokens.tip": "預估 Token 數",
"settings.temperature": "溫度",
@@ -105,7 +107,9 @@
"add.assistant.title": "添加助手",
"message.new.context": "新上下文",
"message.new.branch": "新分支",
"assistant.search.placeholder": "搜尋"
"assistant.search.placeholder": "搜尋",
"artifacts.button.preview": "預覽",
"artifacts.button.download": "下載"
},
"assistants": {
"title": "助手",
@@ -115,7 +119,8 @@
"model_settings": "模型設定"
},
"model": {
"stream_output": "串流輸出"
"stream_output": "串流輸出",
"search": "搜尋模型..."
},
"files": {
"title": "檔案",
@@ -162,12 +167,12 @@
"moonshot": "月之暗面",
"silicon": "SiliconFlow",
"openrouter": "OpenRouter",
"yi": "零一物",
"zhipu": "智AI",
"yi": "零一物",
"zhipu": "智AI",
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "百川",
"dashscope": "DashScope",
"dashscope": "阿里雲百鍊",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "StepFun",

View File

@@ -61,22 +61,24 @@ const AppsPage: FC = () => {
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
</HStack>
<UserAgents onAdd={onAddAgentConfirm} />
{Object.keys(agentGroups).map((group) => (
<div key={group}>
<Title level={4} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
</Col>
)
})}
</Row>
</div>
))}
{Object.keys(agentGroups)
.reverse()
.map((group) => (
<div key={group}>
<Title level={4} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
</Col>
)
})}
</Row>
</div>
))}
<div style={{ minHeight: 20 }} />
</AssistantsContainer>
</ContentContainer>

View File

@@ -8,7 +8,7 @@ import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages'
import RightSidebar from './RightSidebar'
import Tabs from './Tabs'
interface Props {
assistant: Assistant
@@ -29,7 +29,7 @@ const Chat: FC<Props> = (props) => {
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} />
</Main>
{topicPosition === 'right' && showTopics && (
<RightSidebar
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}

View File

@@ -8,7 +8,7 @@ import styled from 'styled-components'
import Chat from './Chat'
import Navbar from './Navbar'
import RightSidebar from './RightSidebar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant
@@ -29,7 +29,7 @@ const HomePage: FC = () => {
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<ContentContainer id="content-container">
{showAssistants && (
<RightSidebar
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}

View File

@@ -1,5 +1,5 @@
import { PaperClipOutlined } from '@ant-design/icons'
import { imageExts, textExts } from '@renderer/config/constant'
import { documentExts, imageExts, textExts } from '@renderer/config/constant'
import { isVisionModel } from '@renderer/config/models'
import { FileType, Model } from '@renderer/types'
import { Tooltip } from 'antd'
@@ -15,7 +15,9 @@ interface Props {
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton }) => {
const { t } = useTranslation()
const extensions = isVisionModel(model) ? [...imageExts, ...textExts] : [...textExts]
const extensions = isVisionModel(model)
? [...imageExts, ...documentExts, ...textExts]
: [...documentExts, ...textExts]
const onSelectFile = async () => {
if (files.length > 0) {

View File

@@ -7,7 +7,7 @@ import {
PauseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons'
import { imageExts, textExts } from '@renderer/config/constant'
import { documentExts, imageExts, textExts } from '@renderer/config/constant'
import { isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
@@ -16,7 +16,7 @@ import { useRuntime, useShowTopics } from '@renderer/hooks/useStore'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import FileManager from '@renderer/services/file'
import { estimateTextTokens } from '@renderer/services/tokens'
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/tokens'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, Message, Topic } from '@renderer/types'
@@ -46,7 +46,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false)
const { addTopic, model } = useAssistant(assistant.id)
const { sendMessageShortcut, fontSize, pasteLongTextAsFile } = useSettings()
const { sendMessageShortcut, fontSize, pasteLongTextAsFile, showInputEstimatedTokens } = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const [contextCount, setContextCount] = useState(0)
@@ -60,8 +60,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const dispatch = useAppDispatch()
const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...(isVision ? imageExts : [])], [isVision])
const inputTokenCount = useMemo(() => estimateTextTokens(text), [text])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), [])
const inputTokenCount = useMemo(
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
[estimateTextTokens, showInputEstimatedTokens, text]
)
_text = text
_files = files
@@ -207,14 +212,14 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
await window.api.file.write(tempFilePath, pasteText)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
setText((prevText) => prevText.replace(pasteText, ''))
setText(text)
setTimeout(() => resizeTextArea(), 0)
}
})
}
}
},
[pasteLongTextAsFile, supportExts]
[pasteLongTextAsFile, supportExts, text]
)
// Command or Ctrl + N create new topic
@@ -243,10 +248,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
_setEstimateTokenCount(tokensCount)
setContextCount(contextCount)
}),
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, () => {
addNewTopic()
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [])
}, [addNewTopic])
useEffect(() => {
textareaRef.current?.focus()

View File

@@ -0,0 +1,51 @@
import MinApp from '@renderer/components/MinApp'
import { AppLogo } from '@renderer/config/env'
import { extractTitle } from '@renderer/utils/formula'
import { Button } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
html: string
}
const Artifacts: FC<Props> = ({ html }) => {
const { t } = useTranslation()
const title = extractTitle(html) || 'Artifacts' + ' ' + t('chat.artifacts.button.preview')
const onPreview = async () => {
const path = await window.api.file.create('artifacts-preview.html')
await window.api.file.write(path, html)
MinApp.start({
name: title,
logo: AppLogo,
url: `file://${path}`
})
}
const onDownload = () => {
window.api.file.save(`${title}.html`, html)
}
return (
<Container>
<Button type="primary" size="middle" onClick={onPreview}>
{t('chat.artifacts.button.preview')}
</Button>
<Button size="middle" onClick={onDownload}>
{t('chat.artifacts.button.download')}
</Button>
</Container>
)
}
const Container = styled.div`
margin: 10px;
display: flex;
flex-direction: row;
gap: 8px;
`
export default Artifacts

View File

@@ -9,6 +9,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import Artifacts from './Artifacts'
import Mermaid from './Mermaid'
interface CodeBlockProps {
@@ -21,8 +22,9 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const match = /language-(\w+)/.exec(className || '')
const showFooterCopyButton = children && children.length > 500
const { theme } = useTheme()
const language = match?.[1]
if (match && match[1] === 'mermaid') {
if (language === 'mermaid') {
initMermaid(theme)
return <Mermaid chart={children} />
}
@@ -50,6 +52,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
<CopyButton text={children} style={{ marginTop: -40, marginRight: 10 }} />
</CodeFooter>
)}
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
</div>
) : (
<code className={className}>{children}</code>

View File

@@ -0,0 +1,62 @@
import {
DownloadOutlined,
RotateLeftOutlined,
RotateRightOutlined,
SwapOutlined,
UndoOutlined,
ZoomInOutlined,
ZoomOutOutlined
} from '@ant-design/icons'
import { download } from '@renderer/utils/download'
import { Image, Space } from 'antd'
import React from 'react'
import styled from 'styled-components'
interface ImagePreviewProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string
}
const ImagePreview: React.FC<ImagePreviewProps> = ({ src }) => {
return (
<Image
src={src}
preview={{
toolbarRender: (
_,
{
transform: { scale },
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
}
) => (
<ToobarWrapper size={12} className="toolbar-wrapper">
<SwapOutlined rotate={90} onClick={onFlipY} />
<SwapOutlined onClick={onFlipX} />
<RotateLeftOutlined onClick={onRotateLeft} />
<RotateRightOutlined onClick={onRotateRight} />
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
<UndoOutlined onClick={onReset} />
<DownloadOutlined onClick={() => download(src)} />
</ToobarWrapper>
)
}}
/>
)
}
const ToobarWrapper = styled(Space)`
padding: 0px 24px;
color: #fff;
font-size: 20px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 100px;
.anticon {
padding: 12px;
cursor: pointer;
}
.anticon:hover {
opacity: 0.3;
}
`
export default ImagePreview

View File

@@ -7,26 +7,20 @@ import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown, { Components } from 'react-markdown'
import rehypeKatex from 'rehype-katex'
// @ts-ignore next-line
import rehypeMathjax from 'rehype-mathjax'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import CodeBlock from './CodeBlock'
import ImagePreview from './ImagePreview'
import Link from './Link'
interface Props {
message: Message
}
const rehypePlugins = [rehypeRaw, rehypeKatex]
const remarkPlugins = [remarkMath, remarkGfm]
const components = {
code: CodeBlock,
a: Link
}
const Markdown: FC<Props> = ({ message }) => {
const { t } = useTranslation()
const { renderInputMessageAsMarkdown } = useSettings()
@@ -38,6 +32,11 @@ const Markdown: FC<Props> = ({ message }) => {
return escapeBrackets(content)
}, [message.content, message.status, t])
const rehypePlugins = useMemo(() => {
const hasUnsafeElements = /<(input|textarea|select)/i.test(messageContent)
return hasUnsafeElements ? [rehypeMathjax] : [rehypeRaw, rehypeMathjax]
}, [messageContent])
if (message.role === 'user' && !renderInputMessageAsMarkdown) {
return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
}
@@ -46,8 +45,14 @@ const Markdown: FC<Props> = ({ message }) => {
<ReactMarkdown
className="markdown"
rehypePlugins={rehypePlugins}
remarkPlugins={remarkPlugins}
components={components as Partial<Components>}
remarkPlugins={[remarkMath, remarkGfm]}
components={
{
a: Link,
code: CodeBlock,
img: ImagePreview
} as Partial<Components>
}
remarkRehypeOptions={{
footnoteLabel: t('common.footnotes'),
footnoteLabelTagName: 'h4',

View File

@@ -42,15 +42,17 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, showMenu = true,
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, () => {
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, (highlight: boolean = true) => {
if (messageRef.current) {
messageRef.current.scrollIntoView({ behavior: 'smooth' })
setTimeout(() => {
messageRef.current?.classList.add('message-highlight')
if (highlight) {
setTimeout(() => {
messageRef.current?.classList.remove('message-highlight')
}, 2500)
}, 500)
messageRef.current?.classList.add('message-highlight')
setTimeout(() => {
messageRef.current?.classList.remove('message-highlight')
}, 2500)
}, 500)
}
}
})
]

View File

@@ -9,7 +9,6 @@ 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'
@@ -36,7 +35,7 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
const getUserName = useCallback(() => {
if (isLocalAi && message.role !== 'user') return APP_NAME
if (message.role === 'assistant') return upperFirst(model?.name || model?.id)
if (message.role === 'assistant') return model?.name || model?.id || ''
return userName || t('common.you')
}, [message.role, model?.id, model?.name, t, userName])

View File

@@ -8,6 +8,7 @@ import {
SaveOutlined,
SyncOutlined
} from '@ant-design/icons'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message, Model } from '@renderer/types'
@@ -17,8 +18,6 @@ import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SelectModelDropdown from '../components/SelectModelDropdown'
interface Props {
message: Message
model?: Model
@@ -83,6 +82,11 @@ const MessageMenubar: FC<Props> = (props) => {
[message.content, message.createdAt, onEdit, t]
)
const onSelectModel = async () => {
const selectedModel = await SelectModelPopup.show({ model })
selectedModel && onRegenerate(selectedModel)
}
return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && (
@@ -99,13 +103,11 @@ const MessageMenubar: FC<Props> = (props) => {
</ActionButton>
</Tooltip>
{canRegenerate && (
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topLeft">
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton>
<SyncOutlined />
</ActionButton>
</Tooltip>
</SelectModelDropdown>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton onClick={onSelectModel}>
<SyncOutlined />
</ActionButton>
</Tooltip>
)}
{isAssistantMessage && (
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>

View File

@@ -1,16 +1,21 @@
import { useRuntime } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types'
import styled from 'styled-components'
const MessgeTokens: React.FC<{ message: Message }> = ({ message }) => {
const { generating } = useRuntime()
const locateMessage = () => {
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, false)
}
if (!message.usage) {
return null
}
if (message.role === 'user') {
return <MessageMetadata>Tokens: {message?.usage?.total_tokens}</MessageMetadata>
return <MessageMetadata onClick={locateMessage}>Tokens: {message?.usage?.total_tokens}</MessageMetadata>
}
if (generating) {
@@ -19,7 +24,7 @@ const MessgeTokens: React.FC<{ message: Message }> = ({ message }) => {
if (message.role === 'assistant') {
return (
<MessageMetadata>
<MessageMetadata onClick={locateMessage}>
Tokens: {message?.usage?.total_tokens} | {message?.usage?.prompt_tokens} | {message?.usage?.completion_tokens}
</MessageMetadata>
)
@@ -33,6 +38,7 @@ const MessageMetadata = styled.div`
color: var(--color-text-2);
user-select: text;
margin: 2px 0;
cursor: pointer;
`
export default MessgeTokens

View File

@@ -131,7 +131,8 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
setMessages([])
updateTopic({ ...topic, messages: [] })
const defaultTopic = getDefaultTopic(assistant.id)
updateTopic({ ...topic, name: defaultTopic.name, messages: [] })
TopicManager.clearTopicMessages(topic.id)
}),
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {

View File

@@ -4,11 +4,9 @@ import AssistantSettingPopup from '@renderer/components/AssistantSettings'
import { HStack } from '@renderer/components/Layout'
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 } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types'
import { Switch } from 'antd'
@@ -24,8 +22,8 @@ interface Props {
setActiveTopic: (topic: Topic) => void
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
const { assistant, addTopic } = useAssistant(activeAssistant.id)
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { theme, toggleTheme } = useTheme()
const { topicPosition } = useSettings()
@@ -33,13 +31,10 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
const { t } = useTranslation()
const addNewTopic = useCallback(() => {
const topic = getDefaultTopic(assistant.id)
addTopic(topic)
setActiveTopic(topic)
db.topics.add({ id: topic.id, messages: [] })
EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)
window.message.success({ content: t('message.topic.added'), key: 'topic-added' })
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant.id, setActiveTopic, t])
}, [t])
return (
<Navbar>

View File

@@ -214,12 +214,12 @@ const SettingsTab: FC<Props> = (props) => {
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))}
min={12}
max={18}
max={22}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
18: <span style={{ fontSize: '18px' }}>A</span>
22: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</Col>

View File

@@ -1,11 +1,19 @@
import { CloseOutlined, DeleteOutlined, EditOutlined, FolderOutlined, UploadOutlined } from '@ant-design/icons'
import {
ClearOutlined,
CloseOutlined,
DeleteOutlined,
EditOutlined,
FolderOutlined,
UploadOutlined
} from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useAppSelector } from '@renderer/store'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { findIndex } from 'lodash'
@@ -31,11 +39,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
if (assistant.topics.length > 1) {
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
removeTopic(topic)
}
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
removeTopic(topic)
},
[assistant.topics, generating, removeTopic, setActiveTopic, t]
)
@@ -64,6 +70,12 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
[generating, setActiveTopic, t]
)
const onClearMessages = useCallback(() => {
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
store.dispatch(setGenerating(false))
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
}, [])
const getTopicMenuItems = useCallback(
(topic: Topic) => {
const menus: MenuProps['items'] = [
@@ -96,6 +108,18 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}
}
},
{
label: t('chat.topics.clear.title'),
key: 'clear-messages',
icon: <ClearOutlined />,
async onClick() {
window.modal.confirm({
title: t('chat.input.clear.content'),
centered: true,
onOk: onClearMessages
})
}
},
{
label: t('chat.topics.export.title'),
key: 'export',
@@ -138,7 +162,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
return menus
},
[assistant, assistants, onDeleteTopic, onMoveTopic, t, updateTopic]
[assistant, assistants, onClearMessages, onDeleteTopic, onMoveTopic, t, updateTopic]
)
return (
@@ -150,11 +174,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)}>
<TopicName className="name">{topic.name.replace('`', '')}</TopicName>
{assistant.topics.length > 1 && isActive && (
{isActive && (
<MenuButton
className="menu"
onClick={(e) => {
e.stopPropagation()
if (assistant.topics.length === 1) {
return onClearMessages()
}
onDeleteTopic(topic)
}}>
<CloseOutlined />

View File

@@ -27,7 +27,7 @@ type Tab = 'assistants' | 'topic' | 'settings'
let _tab: any = ''
const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, position }) => {
const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, position }) => {
const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
@@ -164,4 +164,4 @@ const TabContent = styled.div`
overflow-x: hidden;
`
export default RightSidebar
export default HomeTabs

View File

@@ -1,17 +1,15 @@
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isLocalAi } from '@renderer/config/env'
import { isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Button } from 'antd'
import { upperFirst } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SelectModelDropdown from './SelectModelDropdown'
interface Props {
assistant: Assistant
}
@@ -24,14 +22,20 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
return null
}
const onSelectModel = async (event: React.MouseEvent<HTMLElement>) => {
event.currentTarget.blur()
const selectedModel = await SelectModelPopup.show({ model })
if (selectedModel) {
setModel(selectedModel)
}
}
return (
<SelectModelDropdown model={model} onSelect={setModel} placement="top">
<DropdownButton size="small" type="default">
<ModelAvatar model={model} size={20} />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
</DropdownButton>
</SelectModelDropdown>
<DropdownButton size="small" type="default" onClick={onSelectModel}>
<ModelAvatar model={model} size={20} />
<ModelName>{model ? model.name : t('button.select_model')}</ModelName>
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
</DropdownButton>
)
}
@@ -39,6 +43,7 @@ const DropdownButton = styled(Button)`
font-size: 11px;
border-radius: 15px;
padding: 12px 8px 12px 3px;
-webkit-app-region: none;
`
const ModelName = styled.span`

View File

@@ -1,64 +0,0 @@
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { getModelLogo, isVisionModel } from '@renderer/config/models'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/model'
import { Model } from '@renderer/types'
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
import { first, reverse, sortBy, upperFirst } from 'lodash'
import { FC, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props extends DropdownProps {
model?: Model
onSelect: (model: Model) => void
}
const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, onSelect, ...props }) => {
const { t } = useTranslation()
const { providers } = useProviders()
const items: MenuProps['items'] = (providers || [])
.filter((p) => p.models && p.models.length > 0)
.map((p) => ({
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: reverse(sortBy(p.models, 'name')).map((m) => ({
key: getModelUniqId(m),
label: (
<div>
{upperFirst(m?.name)} {isVisionModel(m) && <VisionIcon />}
</div>
),
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => m && onSelect(m)
}))
}))
return (
<DropdownMenu
menu={{
items,
style: { maxHeight: '55vh', overflow: 'auto' },
selectedKeys: model ? [getModelUniqId(model)] : []
}}
trigger={['click']}
arrow
placement="bottom"
overlayClassName="chat-nav-dropdown"
{...props}>
{children}
</DropdownMenu>
)
}
const DropdownMenu = styled(Dropdown)`
-webkit-app-region: none;
`
export default SelectModelDropdown

View File

@@ -4,7 +4,7 @@ import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId, hasModel } from '@renderer/services/model'
import { Model } from '@renderer/types'
import { Select } from 'antd'
import { find, sortBy, upperFirst } from 'lodash'
import { find, sortBy } from 'lodash'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -23,7 +23,7 @@ const ModelSettings: FC = () => {
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
title: p.name,
options: sortBy(p.models, 'name').map((m) => ({
label: upperFirst(m.name),
label: m.name,
value: getModelUniqId(m)
}))
}))

View File

@@ -37,7 +37,7 @@ export default class AnthropicProvider extends BaseProvider {
}
})
}
if (file.type === FileTypes.TEXT) {
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({
type: 'text',

View File

@@ -40,7 +40,7 @@ export default class GeminiProvider extends BaseProvider {
}
} as InlineDataPart)
}
if (file.type === FileTypes.TEXT) {
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({
text: file.origin_name + '\n' + fileContent

View File

@@ -64,7 +64,7 @@ export default class OpenAIProvider extends BaseProvider {
if (this.isNotSupportFiles) {
if (message.files) {
const textFiles = message.files.filter((file) => file.type === FileTypes.TEXT)
const textFiles = message.files.filter((file) => [FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type))
if (textFiles.length > 0) {
let text = ''
@@ -104,7 +104,7 @@ export default class OpenAIProvider extends BaseProvider {
image_url: { url: image.data }
})
}
if (file.type === FileTypes.TEXT) {
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({
type: 'text',
@@ -134,13 +134,16 @@ export default class OpenAIProvider extends BaseProvider {
userMessages.push(await this.getMessageParam(message, model))
}
const isOpenAIo1 = model.id.includes('o1-')
const isSupportStreamOutput = streamOutput && this.isSupportStreamOutput(model.id)
// @ts-ignore key is not typed
const stream = await this.sdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
temperature: assistant?.settings?.temperature,
messages: [isOpenAIo1 ? undefined : systemMessage, ...userMessages].filter(
Boolean
) as ChatCompletionMessageParam[],
temperature: isOpenAIo1 ? 1 : assistant?.settings?.temperature,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime,
stream: isSupportStreamOutput

View File

@@ -29,9 +29,6 @@ export async function restore() {
data = JSON.parse(await window.api.decompress(file.content))
}
// 处理文件内容
console.log('Parsed file content:', data)
await handleData(data)
} catch (error) {
console.error(error)

View File

@@ -19,5 +19,6 @@ export const EVENT_NAMES = {
NEW_CONTEXT: 'NEW_CONTEXT',
NEW_BRANCH: 'NEW_BRANCH',
EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE',
LOCATE_MESSAGE: 'LOCATE_MESSAGE'
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC'
}

View File

@@ -17,10 +17,8 @@ async function getFileContent(file: FileType) {
return ''
}
const fileId = file.id + file.ext
if (file.type === FileTypes.TEXT) {
return await window.api.file.read(fileId)
return await window.api.file.read(file.id + file.ext)
}
return ''

View File

@@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 30,
version: 31,
blacklist: ['runtime'],
migrate
},

View File

@@ -143,10 +143,10 @@ const initialState: LlmState = {
},
{
id: 'dashscope',
name: 'DashScope',
name: 'Bailian',
apiKey: '',
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
models: SYSTEM_MODELS.dashscope,
models: SYSTEM_MODELS.bailian,
isSystem: true,
enabled: false
},

View File

@@ -182,7 +182,7 @@ const migrateConfig = {
name: 'DashScope',
apiKey: '',
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
models: SYSTEM_MODELS.dashscope,
models: SYSTEM_MODELS.bailian,
isSystem: true,
enabled: false
},
@@ -525,6 +525,20 @@ const migrateConfig = {
]
}
}
},
'31': (state: RootState) => {
return {
...state,
llm: {
...state.llm,
providers: state.llm.providers.map((provider) => {
if (provider.id === 'azure-openai') {
provider.models = provider.models.map((model) => ({ ...model, provider: 'azure-openai' }))
}
return provider
})
}
}
}
}

View File

@@ -0,0 +1,61 @@
export const download = (url: string) => {
fetch(url)
.then((response) => {
// 尝试从Content-Disposition头获取文件名
const contentDisposition = response.headers.get('Content-Disposition')
let filename = 'download' // 默认文件名
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i)
if (filenameMatch) {
filename = filenameMatch[1]
}
}
// 如果URL中有文件名使用URL中的文件名
const urlFilename = url.split('/').pop()
if (urlFilename && urlFilename.includes('.')) {
filename = urlFilename
}
// 如果文件名没有后缀根据Content-Type添加后缀
if (!filename.includes('.')) {
const contentType = response.headers.get('Content-Type')
const extension = getExtensionFromMimeType(contentType)
filename += extension
}
// 添加时间戳以确保文件名唯一
const timestamp = Date.now()
const finalFilename = `${timestamp}_${filename}`
return response.blob().then((blob) => ({ blob, finalFilename }))
})
.then(({ blob, finalFilename }) => {
const blobUrl = URL.createObjectURL(new Blob([blob]))
const link = document.createElement('a')
link.href = blobUrl
link.download = finalFilename
document.body.appendChild(link)
link.click()
URL.revokeObjectURL(blobUrl)
link.remove()
})
}
// 辅助函数根据MIME类型获取文件扩展名
function getExtensionFromMimeType(mimeType: string | null): string {
if (!mimeType) return '.bin' // 默认二进制文件扩展名
const mimeToExtension: { [key: string]: string } = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/gif': '.gif',
'application/pdf': '.pdf',
'text/plain': '.txt',
'application/msword': '.doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx'
}
return mimeToExtension[mimeType] || '.bin'
}

View File

@@ -21,10 +21,25 @@ export function escapeBrackets(text: string) {
if (codeBlock) {
return codeBlock
} else if (squareBracket) {
return `$$${squareBracket}$$`
return `
$$
${squareBracket}
$$
`
} else if (roundBracket) {
return `$${roundBracket}$`
}
return match
})
}
export function extractTitle(html: string): string | null {
const titleRegex = /<title>(.*?)<\/title>/i
const match = html.match(titleRegex)
if (match && match[1]) {
return match[1].trim()
}
return null
}

647
yarn.lock

File diff suppressed because it is too large Load Diff