Compare commits

...

24 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
52 changed files with 863 additions and 371 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,11 +65,12 @@ afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
本次更新:
支持更多的数学公式显示
可以通过搜索快速切换模型
增加 Bolt 小程序
修复 Azure OpenAI 默认模型无法使用问题
增加 Artifacts 网页预览功能
内置助理新增网页生成助理
DashScope 服务商修改为阿里云百炼
修复粘贴长文本后不能自动清除的问题
话题右键菜单增加删除消息功能
修复选择模型弹窗滚动条消失问题
近期更新:
增加 WebDAV 备份功能 by @DrayChou
增加话题历史记录
增加 Azure OpenAI 服务商
支持 PDF, DOC等办公文件格式
支持图片的预览和下载

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.7.14",
"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"
},

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: {

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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

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

@@ -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

@@ -6,7 +6,7 @@ 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, upperFirst } from 'lodash'
import { first, reverse, sortBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -42,7 +42,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
key: getModelUniqId(m),
label: (
<ModelItem>
{upperFirst(m?.name)} {isVisionModel(m) && <VisionIcon />}
{m?.name} {isVisionModel(m) && <VisionIcon />}
</ModelItem>
),
icon: (
@@ -100,7 +100,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
size="middle"
/>
</HStack>
<Divider style={{ margin: 0 }} />
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Container>
{filteredItems.length > 0 ? (
<StyledMenu
@@ -123,9 +123,6 @@ const Container = styled.div`
height: 50vh;
margin-top: 10px;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const StyledMenu = styled(Menu)`
@@ -133,7 +130,6 @@ const StyledMenu = styled(Menu)`
padding: 5px;
margin-top: -10px;
max-height: calc(60vh - 50px);
overflow-y: auto;
.ant-menu-item-group-title {
padding: 5px 10px 0;

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

@@ -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

@@ -72,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",
@@ -90,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",
@@ -106,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",
@@ -169,7 +172,7 @@
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "Baichuan",
"dashscope": "DashScope",
"dashscope": "Alibaba Cloud",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "StepFun",

View File

@@ -72,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": "移动到",
@@ -84,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": "模型温度",
@@ -106,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": "助手",
@@ -169,7 +172,7 @@
"groq": "Groq",
"ollama": "Ollama",
"baichuan": "百川",
"dashscope": "阿里云灵积",
"dashscope": "阿里云百炼",
"anthropic": "Anthropic",
"aihubmix": "AiHubMix",
"stepfun": "阶跃星辰",

View File

@@ -72,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": "移動到",
@@ -90,7 +91,7 @@
"input.send": "發送",
"input.pause": "暫停",
"input.settings": "設定",
"input.upload": "上傳圖片或文檔",
"input.upload": "上傳圖片或文檔",
"input.context_count.tip": "上下文數量",
"input.estimated_tokens.tip": "預估 Token 數",
"settings.temperature": "溫度",
@@ -106,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": "助手",
@@ -164,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 './Tabs'
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

@@ -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

@@ -14,20 +14,13 @@ 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, rehypeMathjax]
const remarkPlugins = [remarkMath, remarkGfm]
const components = {
code: CodeBlock,
a: Link
}
const Markdown: FC<Props> = ({ message }) => {
const { t } = useTranslation()
const { renderInputMessageAsMarkdown } = useSettings()
@@ -39,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>
}
@@ -47,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

@@ -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

@@ -6,7 +6,6 @@ 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'
@@ -23,7 +22,8 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
return null
}
const onSelectModel = async () => {
const onSelectModel = async (event: React.MouseEvent<HTMLElement>) => {
event.currentTarget.blur()
const selectedModel = await SelectModelPopup.show({ model })
if (selectedModel) {
setModel(selectedModel)
@@ -33,7 +33,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
return (
<DropdownButton size="small" type="default" onClick={onSelectModel}>
<ModelAvatar model={model} size={20} />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
<ModelName>{model ? model.name : t('button.select_model')}</ModelName>
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
</DropdownButton>
)

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

@@ -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
},

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

@@ -32,3 +32,14 @@ $$
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
}

279
yarn.lock
View File

@@ -2227,7 +2227,7 @@ __metadata:
languageName: node
linkType: hard
"@xmldom/xmldom@npm:^0.8.8":
"@xmldom/xmldom@npm:^0.8.10, @xmldom/xmldom@npm:^0.8.8":
version: 0.8.10
resolution: "@xmldom/xmldom@npm:0.8.10"
checksum: 10c0/c7647c442502720182b0d65b17d45d2d95317c1c8c497626fe524bda79b4fb768a9aa4fae2da919f308e7abcff7d67c058b102a9d641097e9a57f0b80187851f
@@ -2287,6 +2287,7 @@ __metadata:
localforage: "npm:^1.10.0"
lodash: "npm:^4.17.21"
mime: "npm:^4.0.4"
officeparser: "npm:^4.1.1"
openai: "npm:^4.52.1"
prettier: "npm:^3.2.4"
react: "npm:^18.2.0"
@@ -2941,6 +2942,16 @@ __metadata:
languageName: node
linkType: hard
"bl@npm:^1.0.0":
version: 1.2.3
resolution: "bl@npm:1.2.3"
dependencies:
readable-stream: "npm:^2.3.5"
safe-buffer: "npm:^5.1.1"
checksum: 10c0/ee6478864d3b1295614f269f3fbabeb2362a2f2fc7f8dc2f6c1f944a278d84e0572ecefd6d0b0736d7418763f98dc3b2738253191ea9e98e4b08de211cfac0a6
languageName: node
linkType: hard
"bluebird-lst@npm:^1.0.9":
version: 1.0.9
resolution: "bluebird-lst@npm:1.0.9"
@@ -3022,6 +3033,23 @@ __metadata:
languageName: node
linkType: hard
"buffer-alloc-unsafe@npm:^1.1.0":
version: 1.1.0
resolution: "buffer-alloc-unsafe@npm:1.1.0"
checksum: 10c0/06b9298c9369621a830227c3797ceb3ff5535e323946d7b39a7398fed8b3243798259b3c85e287608c5aad35ccc551cec1a0a5190cc8f39652e8eee25697fc9c
languageName: node
linkType: hard
"buffer-alloc@npm:^1.2.0":
version: 1.2.0
resolution: "buffer-alloc@npm:1.2.0"
dependencies:
buffer-alloc-unsafe: "npm:^1.1.0"
buffer-fill: "npm:^1.0.0"
checksum: 10c0/09d87dd53996342ccfbeb2871257d8cdb25ce9ee2259adc95c6490200cd6e528c5fbae8f30bcc323fe8d8efb0fe541e4ac3bbe9ee3f81c6b7c4b27434cc02ab4
languageName: node
linkType: hard
"buffer-crc32@npm:^1.0.0":
version: 1.0.0
resolution: "buffer-crc32@npm:1.0.0"
@@ -3050,6 +3078,13 @@ __metadata:
languageName: node
linkType: hard
"buffer-fill@npm:^1.0.0":
version: 1.0.0
resolution: "buffer-fill@npm:1.0.0"
checksum: 10c0/55b5654fbbf2d7ceb4991bb537f5e5b5b5b9debca583fee416a74fcec47c16d9e7a90c15acd27577da7bd750b7fa6396e77e7c221e7af138b6d26242381c6e4d
languageName: node
linkType: hard
"buffer-from@npm:^1.0.0":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
@@ -3057,7 +3092,7 @@ __metadata:
languageName: node
linkType: hard
"buffer@npm:^5.1.0, buffer@npm:^5.2.0":
"buffer@npm:^5.1.0, buffer@npm:^5.2.0, buffer@npm:^5.2.1":
version: 5.7.1
resolution: "buffer@npm:5.7.1"
dependencies:
@@ -3489,6 +3524,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^2.8.1":
version: 2.20.3
resolution: "commander@npm:2.20.3"
checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288
languageName: node
linkType: hard
"commander@npm:^5.0.0":
version: 5.1.0
resolution: "commander@npm:5.1.0"
@@ -3843,6 +3885,69 @@ __metadata:
languageName: node
linkType: hard
"decompress-tar@npm:^4.0.0, decompress-tar@npm:^4.1.0, decompress-tar@npm:^4.1.1":
version: 4.1.1
resolution: "decompress-tar@npm:4.1.1"
dependencies:
file-type: "npm:^5.2.0"
is-stream: "npm:^1.1.0"
tar-stream: "npm:^1.5.2"
checksum: 10c0/92d86c5dfe2a89f9b5db584668f8ed2a3107339083872c7f78b5f7d55222d954545e018c10346a50991542ad6d1406128bf1c97a24f023810993a1dcfb3c3f21
languageName: node
linkType: hard
"decompress-tarbz2@npm:^4.0.0":
version: 4.1.1
resolution: "decompress-tarbz2@npm:4.1.1"
dependencies:
decompress-tar: "npm:^4.1.0"
file-type: "npm:^6.1.0"
is-stream: "npm:^1.1.0"
seek-bzip: "npm:^1.0.5"
unbzip2-stream: "npm:^1.0.9"
checksum: 10c0/d5ab2c2435a53f45da8348ffdb5ae0a3ff8fec55948b7890a1c55413de4d1e539a22978e7dcd8bd3561985878c9778253fe146cbdea429f04fa4529abb57c54e
languageName: node
linkType: hard
"decompress-targz@npm:^4.0.0":
version: 4.1.1
resolution: "decompress-targz@npm:4.1.1"
dependencies:
decompress-tar: "npm:^4.1.1"
file-type: "npm:^5.2.0"
is-stream: "npm:^1.1.0"
checksum: 10c0/42514fb2df6248c56b2b115494b7d1d046bc582e960354ba4faad5792f261782a61d17d9ef53845abe78c0f0ecafc195cb0754c00227fa0bd0642a1bfd8eafad
languageName: node
linkType: hard
"decompress-unzip@npm:^4.0.1":
version: 4.0.1
resolution: "decompress-unzip@npm:4.0.1"
dependencies:
file-type: "npm:^3.8.0"
get-stream: "npm:^2.2.0"
pify: "npm:^2.3.0"
yauzl: "npm:^2.4.2"
checksum: 10c0/896f88e1c23b59cdce022227a8910c06158bd4b296c21d61af7167bd50d00e9e4355b605bdbfd7ba75d46ad277d4f881cdd037aec7165a40ccd0ee4ef59443a8
languageName: node
linkType: hard
"decompress@npm:^4.2.0":
version: 4.2.1
resolution: "decompress@npm:4.2.1"
dependencies:
decompress-tar: "npm:^4.0.0"
decompress-tarbz2: "npm:^4.0.0"
decompress-targz: "npm:^4.0.0"
decompress-unzip: "npm:^4.0.1"
graceful-fs: "npm:^4.1.10"
make-dir: "npm:^1.0.0"
pify: "npm:^2.3.0"
strip-dirs: "npm:^2.0.0"
checksum: 10c0/6730279fa206aad04a8338a88ab49c596034c502b2d5f23a28d0a28290b82d9217f9e60c8b5739805474ca842fc856e08e2d64ed759f2118c2bcabe42fa9eece
languageName: node
linkType: hard
"deep-is@npm:^0.1.3":
version: 0.1.4
resolution: "deep-is@npm:0.1.4"
@@ -4295,7 +4400,7 @@ __metadata:
languageName: node
linkType: hard
"end-of-stream@npm:^1.1.0":
"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0":
version: 1.4.4
resolution: "end-of-stream@npm:1.4.4"
dependencies:
@@ -4993,6 +5098,27 @@ __metadata:
languageName: node
linkType: hard
"file-type@npm:^3.8.0":
version: 3.9.0
resolution: "file-type@npm:3.9.0"
checksum: 10c0/7ae074b350c2300807a99d428600a8ee6b2ace901400898706a20ddc2c43c9abb7e05177ff55ed67a2fd26dfa9b91857b21ec9c0ab3202b9cabebc7e65900240
languageName: node
linkType: hard
"file-type@npm:^5.2.0":
version: 5.2.0
resolution: "file-type@npm:5.2.0"
checksum: 10c0/c16c2f4e484a838c12b63e08637277905f08aebb1afbc291086029210aea17ded5ed701c9a4588313446ae0c1da71566b58df9a9c758a1ec300c4f80b9713cbf
languageName: node
linkType: hard
"file-type@npm:^6.1.0":
version: 6.2.0
resolution: "file-type@npm:6.2.0"
checksum: 10c0/3d7fe85a10bd97ca0c35fd9a20d21f5b20849bbb70985d37c34475051433f3c6109c76a3e5893bff6773037b769be9730a2db762789ecf25def9b62a4c2ee953
languageName: node
linkType: hard
"file-url@npm:^2.0.0":
version: 2.0.2
resolution: "file-url@npm:2.0.2"
@@ -5147,6 +5273,13 @@ __metadata:
languageName: node
linkType: hard
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"
checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8
languageName: node
linkType: hard
"fs-extra@npm:^1.0.0":
version: 1.0.0
resolution: "fs-extra@npm:1.0.0"
@@ -5307,6 +5440,16 @@ __metadata:
languageName: node
linkType: hard
"get-stream@npm:^2.2.0":
version: 2.3.1
resolution: "get-stream@npm:2.3.1"
dependencies:
object-assign: "npm:^4.0.1"
pinkie-promise: "npm:^2.0.0"
checksum: 10c0/46c12f496e7edec688a1cc570fe7556ce91e91201fa7efb146853fb9f0a8f0b0bb9a02cf9d9e4e9d4e2097f98c83b09621d9034c25ca0cf80ae6f4dace9c3465
languageName: node
linkType: hard
"get-stream@npm:^5.1.0":
version: 5.2.0
resolution: "get-stream@npm:5.2.0"
@@ -5497,7 +5640,7 @@ __metadata:
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -6329,6 +6472,13 @@ __metadata:
languageName: node
linkType: hard
"is-natural-number@npm:^4.0.1":
version: 4.0.1
resolution: "is-natural-number@npm:4.0.1"
checksum: 10c0/f05c544cb0ad39d4410e2ae2244282bf61918ebbb808b665436ffca4f6bbe908d3ae3a8d21fe143d302951f157d969986dd432098b63899561639fcd1ce1c280
languageName: node
linkType: hard
"is-negative-zero@npm:^2.0.3":
version: 2.0.3
resolution: "is-negative-zero@npm:2.0.3"
@@ -6413,7 +6563,7 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^1.0.1":
"is-stream@npm:^1.0.1, is-stream@npm:^1.1.0":
version: 1.1.0
resolution: "is-stream@npm:1.1.0"
checksum: 10c0/b8ae7971e78d2e8488d15f804229c6eed7ed36a28f8807a1815938771f4adff0e705218b7dab968270433f67103e4fef98062a0beea55d64835f705ee72c7002
@@ -7087,6 +7237,15 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^1.0.0":
version: 1.3.0
resolution: "make-dir@npm:1.3.0"
dependencies:
pify: "npm:^3.0.0"
checksum: 10c0/5eb94f47d7ef41d89d1b8eef6539b8950d5bd99eeba093a942bfd327faa37d2d62227526b88b73633243a2ec7972d21eb0f4e5d62ae4e02a79e389f4a7bb3022
languageName: node
linkType: hard
"make-fetch-happen@npm:^13.0.0":
version: 13.0.1
resolution: "make-fetch-happen@npm:13.0.1"
@@ -8053,6 +8212,13 @@ __metadata:
languageName: node
linkType: hard
"node-ensure@npm:^0.0.0":
version: 0.0.0
resolution: "node-ensure@npm:0.0.0"
checksum: 10c0/7af391aee024a8b7df77c239ed8b90417e3f2539824fa06b60f243ce14c75ee455766464c7c3ba9407d5b1e4d1d74ed5cf5f8af10c67b0fc05aa6e29f5d2462b
languageName: node
linkType: hard
"node-fetch@npm:^2.6.7":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
@@ -8152,7 +8318,7 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
"object-assign@npm:^4.0.1, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
@@ -8219,6 +8385,21 @@ __metadata:
languageName: node
linkType: hard
"officeparser@npm:^4.1.1":
version: 4.1.1
resolution: "officeparser@npm:4.1.1"
dependencies:
"@xmldom/xmldom": "npm:^0.8.10"
decompress: "npm:^4.2.0"
file-type: "npm:^16.5.4"
node-ensure: "npm:^0.0.0"
rimraf: "npm:^2.6.3"
bin:
officeparser: officeParser.js
checksum: 10c0/7c5f01b3fe74fd31ca6ae6a6f46b2336d953eece3ca61d171e417b50d7e1962268af630bef0c8d40e39a1c471e7146281eecd880f0eecc576895eee9bc20b6a7
languageName: node
linkType: hard
"omggif@npm:^1.0.10, omggif@npm:^1.0.9":
version: 1.0.10
resolution: "omggif@npm:1.0.10"
@@ -8610,13 +8791,20 @@ __metadata:
languageName: node
linkType: hard
"pify@npm:^2.0.0":
"pify@npm:^2.0.0, pify@npm:^2.3.0":
version: 2.3.0
resolution: "pify@npm:2.3.0"
checksum: 10c0/551ff8ab830b1052633f59cb8adc9ae8407a436e06b4a9718bcb27dc5844b83d535c3a8512b388b6062af65a98c49bdc0dd523d8b2617b188f7c8fee457158dc
languageName: node
linkType: hard
"pify@npm:^3.0.0":
version: 3.0.0
resolution: "pify@npm:3.0.0"
checksum: 10c0/fead19ed9d801f1b1fcd0638a1ac53eabbb0945bf615f2f8806a8b646565a04a1b0e7ef115c951d225f042cca388fdc1cd3add46d10d1ed6951c20bd2998af10
languageName: node
linkType: hard
"pinkie-promise@npm:^2.0.0":
version: 2.0.1
resolution: "pinkie-promise@npm:2.0.1"
@@ -9664,7 +9852,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:~2.3.6":
"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
dependencies:
@@ -10073,6 +10261,17 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^2.6.3":
version: 2.7.1
resolution: "rimraf@npm:2.7.1"
dependencies:
glob: "npm:^7.1.3"
bin:
rimraf: ./bin.js
checksum: 10c0/4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40
languageName: node
linkType: hard
"rimraf@npm:^3.0.2":
version: 3.0.2
resolution: "rimraf@npm:3.0.2"
@@ -10196,7 +10395,7 @@ __metadata:
languageName: node
linkType: hard
"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:~5.2.0":
"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
@@ -10284,6 +10483,18 @@ __metadata:
languageName: node
linkType: hard
"seek-bzip@npm:^1.0.5":
version: 1.0.6
resolution: "seek-bzip@npm:1.0.6"
dependencies:
commander: "npm:^2.8.1"
bin:
seek-bunzip: bin/seek-bunzip
seek-table: bin/seek-bzip-table
checksum: 10c0/e4019e4498bb725ff855603595c4116ca003674b13d29cb9ed9891b2ceec884ccf7c1cb5dba0d6b50fe6ce760a011078f5744efb79823f4ddb9decb1571e9912
languageName: node
linkType: hard
"semver-compare@npm:^1.0.0":
version: 1.0.0
resolution: "semver-compare@npm:1.0.0"
@@ -10776,6 +10987,15 @@ __metadata:
languageName: node
linkType: hard
"strip-dirs@npm:^2.0.0":
version: 2.1.0
resolution: "strip-dirs@npm:2.1.0"
dependencies:
is-natural-number: "npm:^4.0.1"
checksum: 10c0/073d6d08331ec2e87afc2c2535d7336fee1d63797384045e4ecb9908a5ac6615022ee000cc278d6bbc94147bed7350f7cf4657b6d18c377813f37e7ae329fb52
languageName: node
linkType: hard
"strip-json-comments@npm:^3.1.1":
version: 3.1.1
resolution: "strip-json-comments@npm:3.1.1"
@@ -10908,6 +11128,21 @@ __metadata:
languageName: node
linkType: hard
"tar-stream@npm:^1.5.2":
version: 1.6.2
resolution: "tar-stream@npm:1.6.2"
dependencies:
bl: "npm:^1.0.0"
buffer-alloc: "npm:^1.2.0"
end-of-stream: "npm:^1.0.0"
fs-constants: "npm:^1.0.0"
readable-stream: "npm:^2.3.0"
to-buffer: "npm:^1.1.1"
xtend: "npm:^4.0.0"
checksum: 10c0/ab8528d2cc9ccd0906d1ce6d8089030b2c92a578c57645ff4971452c8c5388b34c7152c04ed64b8510d22a66ffaf0fee58bada7d6ab41ad1e816e31993d59cf3
languageName: node
linkType: hard
"tar-stream@npm:^3.0.0":
version: 3.1.7
resolution: "tar-stream@npm:3.1.7"
@@ -10982,6 +11217,13 @@ __metadata:
languageName: node
linkType: hard
"through@npm:^2.3.8":
version: 2.3.8
resolution: "through@npm:2.3.8"
checksum: 10c0/4b09f3774099de0d4df26d95c5821a62faee32c7e96fb1f4ebd54a2d7c11c57fe88b0a0d49cf375de5fee5ae6bf4eb56dbbf29d07366864e2ee805349970d3cc
languageName: node
linkType: hard
"timm@npm:^1.6.1":
version: 1.7.1
resolution: "timm@npm:1.7.1"
@@ -11026,6 +11268,13 @@ __metadata:
languageName: node
linkType: hard
"to-buffer@npm:^1.1.1":
version: 1.1.1
resolution: "to-buffer@npm:1.1.1"
checksum: 10c0/fb9fc6a0103f2b06e2e01c3d291586d0148759d5584f35d0973376434d1b58bd6ee5df9273f0bb1190eb2a5747c894bf49fed571325a7ac10208a48f31736439
languageName: node
linkType: hard
"to-fast-properties@npm:^2.0.0":
version: 2.0.0
resolution: "to-fast-properties@npm:2.0.0"
@@ -11280,6 +11529,16 @@ __metadata:
languageName: node
linkType: hard
"unbzip2-stream@npm:^1.0.9":
version: 1.4.3
resolution: "unbzip2-stream@npm:1.4.3"
dependencies:
buffer: "npm:^5.2.1"
through: "npm:^2.3.8"
checksum: 10c0/2ea2048f3c9db3499316ccc1d95ff757017ccb6f46c812d7c42466247e3b863fb178864267482f7f178254214247779daf68e85f50bd7736c3c97ba2d58b910a
languageName: node
linkType: hard
"undici-types@npm:~5.26.4":
version: 5.26.5
resolution: "undici-types@npm:5.26.5"
@@ -12101,7 +12360,7 @@ __metadata:
languageName: node
linkType: hard
"yauzl@npm:^2.10.0":
"yauzl@npm:^2.10.0, yauzl@npm:^2.4.2":
version: 2.10.0
resolution: "yauzl@npm:2.10.0"
dependencies: