Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c250124043 | ||
|
|
7aa6d6ebeb | ||
|
|
e962351b13 | ||
|
|
80e34688b1 | ||
|
|
8c23d6ec55 | ||
|
|
2cc09a52f4 | ||
|
|
6b872f58ed | ||
|
|
d5da7e4413 | ||
|
|
3e88aa3c36 | ||
|
|
d6fc1cb364 | ||
|
|
224f23aea0 | ||
|
|
8c186b757e | ||
|
|
3f32775b98 | ||
|
|
067819652b | ||
|
|
b3a023e4ac | ||
|
|
1da1b6622d | ||
|
|
78e1626e52 | ||
|
|
ec49cf61d6 | ||
|
|
b487c68822 | ||
|
|
74aa95339c | ||
|
|
e90ef9d05f | ||
|
|
e8bdf9d5fd | ||
|
|
a97d6f024b | ||
|
|
b4eb35d86a | ||
|
|
3fc45187eb | ||
|
|
f1c5a41fff | ||
|
|
5a7f943448 | ||
|
|
b02e83f68e |
@@ -2,3 +2,4 @@ node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -70,7 +70,6 @@ jobs:
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
dist/latest*.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
@@ -4,3 +4,4 @@ pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
CHANGELOG*.md
|
||||
|
||||
16
README.md
16
README.md
@@ -2,6 +2,22 @@
|
||||
|
||||
Cherry Studio is a desktop client for multiple cutting-edge LLM models, available on Windows, Mac and Linux.
|
||||
|
||||
# Screenshot
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Feature
|
||||
|
||||
1. Supports multiple large language model service providers.
|
||||
2. Allows creation of multiple Assistants.
|
||||
3. Enables creation of multiple topics.
|
||||
4. Allows using multiple models to answer questions in the same conversation.
|
||||
5. Supports drag-and-drop sorting.
|
||||
6. Code highlighting.
|
||||
|
||||
# Develop
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
provider: generic
|
||||
url: http://127.0.0.1:8080
|
||||
updaterCacheDirName: cherry-studio-updater
|
||||
# provider: generic
|
||||
# url: http://127.0.0.1:8080
|
||||
# updaterCacheDirName: cherry-studio-updater
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
|
||||
@@ -26,8 +26,17 @@ mac:
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
- target: zip
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
dmg:
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
@@ -45,3 +54,7 @@ publish:
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
- 修复更新日志页面不能滚动问题
|
||||
- 新增检查更新按钮
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
plugins: [react()],
|
||||
assetsInclude: ['**/*.md'],
|
||||
server: {
|
||||
host: '0.0.0.0'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.4",
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
@@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@sentry/electron": "^5.2.0",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-window-state": "^5.0.3"
|
||||
@@ -33,6 +34,7 @@
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@fontsource/inter": "^5.0.18",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/node": "^18.19.9",
|
||||
|
||||
@@ -6,9 +6,14 @@ exports.default = async function notarizing(context) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
|
||||
console.log('Skipping notarization')
|
||||
return
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
|
||||
const notarized = await notarize({
|
||||
await notarize({
|
||||
appPath: `${context.appOutDir}/${appName}.app`,
|
||||
appBundleId: 'com.kangfenmao.CherryStudio',
|
||||
appleId: process.env.APPLE_ID,
|
||||
@@ -16,7 +21,5 @@ exports.default = async function notarizing(context) {
|
||||
teamId: process.env.APPLE_TEAM_ID
|
||||
})
|
||||
|
||||
console.log('Notarized:', notarized)
|
||||
|
||||
return notarized
|
||||
console.log('Notarized app:', appName)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import * as Sentry from '@sentry/electron/main'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import AppUpdater from './updater'
|
||||
|
||||
function createWindow(): void {
|
||||
function createWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
@@ -61,6 +62,8 @@ function createWindow(): void {
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -77,24 +80,36 @@ app.whenReady().then(() => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
// IPC
|
||||
ipcMain.handle('get-app-info', () => ({
|
||||
version: app.getVersion()
|
||||
}))
|
||||
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log('An error occurred: ', err))
|
||||
const mainWindow = createWindow()
|
||||
|
||||
setTimeout(() => new AppUpdater(), 5000)
|
||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||
|
||||
// IPC
|
||||
ipcMain.handle('get-app-info', () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged
|
||||
}))
|
||||
|
||||
ipcMain.handle('open-website', (_, url: string) => {
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
ipcMain.handle('check-for-update', async () => {
|
||||
autoUpdater.logger?.info('触发检查更新')
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
}
|
||||
})
|
||||
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
@@ -108,3 +123,6 @@ app.on('window-all-closed', () => {
|
||||
|
||||
// In this file you can include the rest of your app"s specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
Sentry.init({
|
||||
dsn: 'https://f0e972deff79c2df3e887e232d8a46a3@o4507610668007424.ingest.us.sentry.io/4507610670563328'
|
||||
})
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
import logger from 'electron-log'
|
||||
import { dialog, ipcMain } from 'electron'
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
autoUpdater: _AppUpdater = autoUpdater
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
logger.transports.file.level = 'debug'
|
||||
autoUpdater.logger = logger
|
||||
autoUpdater.forceDevUpdateConfig = true
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
ipcMain.on('check-for-update', () => {
|
||||
logger.info('触发检查更新')
|
||||
autoUpdater.checkForUpdates()
|
||||
})
|
||||
|
||||
// 检测下载错误
|
||||
autoUpdater.on('error', (error) => {
|
||||
logger.error('更新异常', error)
|
||||
mainWindow.webContents.send('update-error', error)
|
||||
})
|
||||
|
||||
// 检测是否需要更新
|
||||
@@ -28,6 +24,7 @@ export default class AppUpdater {
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||
mainWindow.webContents.send('update-available', releaseInfo)
|
||||
const releaseNotes = releaseInfo.releaseNotes
|
||||
let releaseContent = ''
|
||||
if (releaseNotes) {
|
||||
@@ -49,10 +46,12 @@ export default class AppUpdater {
|
||||
title: '应用有新的更新',
|
||||
detail: releaseContent,
|
||||
message: '发现新版本,是否现在更新?',
|
||||
buttons: ['否', '是']
|
||||
buttons: ['下次再说', '更新']
|
||||
})
|
||||
.then(({ response }) => {
|
||||
if (response === 1) {
|
||||
logger.info('用户选择更新,准备下载更新')
|
||||
mainWindow.webContents.send('download-update')
|
||||
autoUpdater.downloadUpdate()
|
||||
}
|
||||
})
|
||||
@@ -61,11 +60,13 @@ export default class AppUpdater {
|
||||
// 检测到不需要更新时
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
logger.info('现在使用的就是最新版本,不用更新')
|
||||
mainWindow.webContents.send('update-not-available')
|
||||
})
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
logger.info('下载进度', progress)
|
||||
mainWindow.webContents.send('download-progress', progress)
|
||||
})
|
||||
|
||||
// 当需要更新的内容下载完成后
|
||||
@@ -80,5 +81,7 @@ export default class AppUpdater {
|
||||
setImmediate(() => autoUpdater.quitAndInstall())
|
||||
})
|
||||
})
|
||||
|
||||
this.autoUpdater = autoUpdater
|
||||
}
|
||||
}
|
||||
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@@ -6,7 +6,10 @@ declare global {
|
||||
api: {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
getAppInfo: () => ipcRenderer.invoke('get-app-info')
|
||||
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import { AntdThemeConfig, getAntdLocale } from './config/antd'
|
||||
import './i18n'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
|
||||
25
src/renderer/src/CHANGELOG.en.md
Normal file
25
src/renderer/src/CHANGELOG.en.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# CHANGES LOG
|
||||
|
||||
### v0.2.4 - 2024-07-16
|
||||
|
||||
- Fixed the issue of the update log page cannot be scrolled
|
||||
- Added a check for updates button
|
||||
|
||||
### v0.2.3 - 2024-07-16
|
||||
|
||||
- Fixed multi-language prompt errors
|
||||
- Fixed default model error issues with ZHIPU AI
|
||||
- Fixed OpenRouter API detection error issues
|
||||
- Fixed multi-language translation errors with model providers
|
||||
|
||||
### v0.2.2 - 2024-07-15
|
||||
|
||||
- Fix the issue where the default assistant name is empty.
|
||||
- Fix the problem with default language detection during the first installation.
|
||||
- Adjust the changelog style.
|
||||
|
||||
### v0.2.1 - 2024-07-15
|
||||
|
||||
- **Feature**: Add new feature for pausing message sending
|
||||
- **Fix**: Resolve incomplete translation issue upon language switch
|
||||
- **Build**: Support for macOS Intel architecture
|
||||
26
src/renderer/src/CHANGELOG.zh.md
Normal file
26
src/renderer/src/CHANGELOG.zh.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 更新日志
|
||||
|
||||
### v0.2.4 - 2024-07-16
|
||||
|
||||
- 修复更新日志页面不能滚动问题
|
||||
- 新增检查更新按钮
|
||||
|
||||
### v0.2.3 - 2024-07-16
|
||||
|
||||
- 修复多语言提示错误
|
||||
- 修复智谱AI默认模型错误问题
|
||||
- 修复 OpenRouter API 检测出错问题
|
||||
- 修复模型提供商多语言翻译错误问题
|
||||
|
||||
### v0.2.2 - 2024-07-15
|
||||
|
||||
- 修复默认助理名称为空的问题
|
||||
- 修复首次安装默认语言检测问题
|
||||
- 更新日志样式微调
|
||||
|
||||
### v0.2.1 - 2024-07-15
|
||||
|
||||
- 【功能】新增消息暂停发送功能
|
||||
- 【修复】修复多语言切换不彻底问题
|
||||
- 【构建】支持 macOS Intel 架构
|
||||
|
||||
2
src/renderer/src/env.d.ts
vendored
2
src/renderer/src/env.d.ts
vendored
@@ -2,10 +2,12 @@
|
||||
|
||||
import { MessageInstance } from 'antd/es/message/interface'
|
||||
import { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
message: MessageInstance
|
||||
modal: HookAPI
|
||||
keyv: KeyvStorage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { i18nInit } from '@renderer/i18n'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
@@ -12,5 +13,13 @@ export function useAppInitEffect() {
|
||||
const storedImage = await LocalStorage.getImage('avatar')
|
||||
storedImage && dispatch(setAvatar(storedImage))
|
||||
})
|
||||
i18nInit()
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ const resources = {
|
||||
'error.enter.api.host': 'Please enter your API host first',
|
||||
'error.enter.model': 'Please select a model first',
|
||||
'api.connection.failed': 'Connection failed',
|
||||
'api.connection.success': 'Connection successful'
|
||||
'api.connection.success': 'Connection successful',
|
||||
'chat.completion.paused': 'Chat completion paused'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': 'Default Assistant',
|
||||
@@ -61,7 +62,8 @@ const resources = {
|
||||
'input.clear.title': 'Clear all messages?',
|
||||
'input.clear.content': 'Are you sure to clear all messages?',
|
||||
'input.placeholder': 'Type your message here...',
|
||||
'input.send': 'Send'
|
||||
'input.send': 'Send',
|
||||
'input.pause': 'Pause'
|
||||
},
|
||||
apps: {
|
||||
title: 'Agents'
|
||||
@@ -72,8 +74,8 @@ const resources = {
|
||||
moonshot: 'Moonshot',
|
||||
silicon: 'SiliconFlow',
|
||||
openrouter: 'OpenRouter',
|
||||
yi: 'Lingyiwanwu',
|
||||
zhipu: 'BigModel',
|
||||
yi: 'Yi',
|
||||
zhipu: 'ZHIPU AI',
|
||||
groq: 'Groq',
|
||||
ollama: 'Ollama'
|
||||
},
|
||||
@@ -106,7 +108,12 @@ const resources = {
|
||||
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
|
||||
'models.empty': 'No models found',
|
||||
'assistant.title': 'Default Assistant',
|
||||
'about.description': 'A powerful AI assistant for producer'
|
||||
'about.description': 'A powerful AI assistant for producer',
|
||||
'about.updateNotAvailable': 'You are using the latest version',
|
||||
'about.checkingUpdate': 'Checking for updates...',
|
||||
'about.updateError': 'Update error',
|
||||
'about.checkUpdate': 'Check Update',
|
||||
'about.downloading': 'Downloading...'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -146,7 +153,8 @@ const resources = {
|
||||
'error.enter.api.host': '请输入您的 API 地址',
|
||||
'error.enter.model': '请选择一个模型',
|
||||
'api.connection.failed': '连接失败',
|
||||
'api.connection.successful': '连接成功'
|
||||
'api.connection.successful': '连接成功',
|
||||
'chat.completion.paused': '会话已停止'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': '默认助手',
|
||||
@@ -168,7 +176,8 @@ const resources = {
|
||||
'input.clear.title': '清除所有消息?',
|
||||
'input.clear.content': '确定要清除所有消息吗?',
|
||||
'input.placeholder': '在这里输入消息...',
|
||||
'input.send': '发送'
|
||||
'input.send': '发送',
|
||||
'input.pause': '暂停'
|
||||
},
|
||||
apps: {
|
||||
title: '智能体'
|
||||
@@ -213,7 +222,12 @@ const resources = {
|
||||
'models.add.group_name.placeholder': '例如 ChatGPT',
|
||||
'models.empty': '没有模型',
|
||||
'assistant.title': '默认助手',
|
||||
'about.description': '一个为创造者而生的 AI 助手'
|
||||
'about.description': '一个为创造者而生的 AI 助手',
|
||||
'about.updateNotAvailable': '你的软件已是最新版本',
|
||||
'about.checkingUpdate': '正在检查更新...',
|
||||
'about.updateError': '更新出错',
|
||||
'about.checkUpdate': '检查更新',
|
||||
'about.downloading': '正在下载更新...'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,11 +235,15 @@ const resources = {
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: store.getState().settings.language || 'en-US',
|
||||
lng: localStorage.getItem('language') || navigator.language || 'en-US',
|
||||
fallbackLng: 'en-US',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
})
|
||||
|
||||
export function i18nInit() {
|
||||
i18n.changeLanguage(store.getState().settings.language || 'en-US')
|
||||
}
|
||||
|
||||
export default i18n
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
import localforage from 'localforage'
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import * as Sentry from '@sentry/electron/renderer'
|
||||
import { isProduction } from './utils'
|
||||
|
||||
async function initSentry() {
|
||||
if (await isProduction()) {
|
||||
Sentry.init({
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
localforage.config({
|
||||
@@ -8,6 +29,11 @@ function init() {
|
||||
storeName: 'cherryai',
|
||||
description: 'Cherry Studio Storage'
|
||||
})
|
||||
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
|
||||
initSentry()
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './assets/styles/index.scss'
|
||||
import './init'
|
||||
import './i18n'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
HistoryOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlusCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
@@ -19,9 +20,10 @@ import { isEmpty } from 'lodash'
|
||||
import SendMessageSetting from './SendMessageSetting'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import dayjs from 'dayjs'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -86,6 +88,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
|
||||
const clearTopic = () => EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
||||
|
||||
const onPause = () => {
|
||||
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||
store.dispatch(setGenerating(false))
|
||||
}
|
||||
|
||||
// Command or Ctrl + N create new topic
|
||||
useEffect(() => {
|
||||
const onKeydown = (e) => {
|
||||
@@ -148,6 +155,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
</Tooltip>
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{generating && (
|
||||
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause}>
|
||||
<PauseCircleOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<SendMessageSetting>
|
||||
<ToolbarButton type="text" style={{ marginRight: 0 }}>
|
||||
<MoreOutlined />
|
||||
|
||||
@@ -12,6 +12,7 @@ import Logo from '@renderer/assets/images/logo.png'
|
||||
import { SyncOutlined } from '@ant-design/icons'
|
||||
import { firstLetter } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -53,6 +54,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
|
||||
}
|
||||
|
||||
const getMessageContent = (message: Message) => {
|
||||
if (isEmpty(message.content) && message.status === 'paused') {
|
||||
return t('message.chat.completion.paused')
|
||||
}
|
||||
return message.content
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageContainer key={message.id}>
|
||||
<AvatarWrapper>
|
||||
@@ -72,7 +80,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
)}
|
||||
{message.status !== 'sending' && (
|
||||
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
|
||||
{message.content}
|
||||
{getMessageContent(message)}
|
||||
</Markdown>
|
||||
)}
|
||||
{showMenu && (
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import { Avatar } from 'antd'
|
||||
import { Avatar, Button, Progress } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Changelog from './components/Changelog'
|
||||
import { debounce } from 'lodash'
|
||||
import { ProgressInfo } from 'electron-updater'
|
||||
|
||||
const AboutSettings: FC = () => {
|
||||
const [version, setVersion] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const [percent, setPercent] = useState(0)
|
||||
const [checkUpdateLoading, setCheckUpdateLoading] = useState(false)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
|
||||
const onCheckUpdate = debounce(
|
||||
async () => {
|
||||
if (checkUpdateLoading || downloading) return
|
||||
setCheckUpdateLoading(true)
|
||||
await window.api.checkForUpdate()
|
||||
setCheckUpdateLoading(false)
|
||||
},
|
||||
2000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
|
||||
const onOpenWebsite = (suffix = '') => {
|
||||
window.api.openWebsite('https://github.com/kangfenmao/cherry-studio' + suffix)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@@ -16,23 +37,74 @@ const AboutSettings: FC = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
const removers = [
|
||||
ipcRenderer.on('update-not-available', () => {
|
||||
setCheckUpdateLoading(false)
|
||||
window.message.success(t('settings.about.updateNotAvailable'))
|
||||
}),
|
||||
ipcRenderer.on('update-available', () => {
|
||||
setCheckUpdateLoading(false)
|
||||
}),
|
||||
ipcRenderer.on('download-update', () => {
|
||||
setCheckUpdateLoading(false)
|
||||
setDownloading(true)
|
||||
}),
|
||||
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => {
|
||||
setPercent(progress.percent)
|
||||
}),
|
||||
ipcRenderer.on('update-error', (_, error) => {
|
||||
setCheckUpdateLoading(false)
|
||||
setDownloading(false)
|
||||
setPercent(0)
|
||||
window.modal.info({
|
||||
title: t('settings.about.updateError'),
|
||||
content: error?.message || t('settings.about.updateError'),
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
]
|
||||
return () => removers.forEach((remover) => remover())
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Avatar src={Logo} size={100} style={{ marginTop: 50 }} />
|
||||
<AvatarWrapper onClick={() => onOpenWebsite()}>
|
||||
{percent > 0 && (
|
||||
<ProgressCircle
|
||||
type="circle"
|
||||
size={104}
|
||||
percent={percent}
|
||||
showInfo={false}
|
||||
strokeLinecap="butt"
|
||||
strokeColor="#67ad5b"
|
||||
/>
|
||||
)}
|
||||
<Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} />
|
||||
</AvatarWrapper>
|
||||
<Title>
|
||||
Cherry Studio <Version>(v{version})</Version>
|
||||
Cherry Studio <Version onClick={() => onOpenWebsite('/releases')}>(v{version})</Version>
|
||||
</Title>
|
||||
<Description>{t('settings.about.description')}</Description>
|
||||
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
|
||||
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
|
||||
</CheckUpdateButton>
|
||||
<Changelog />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-y: scroll;
|
||||
padding: 0;
|
||||
padding-bottom: 50px;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
@@ -47,6 +119,7 @@ const Version = styled.span`
|
||||
color: var(--color-text-2);
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const Description = styled.div`
|
||||
@@ -55,4 +128,19 @@ const Description = styled.div`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const CheckUpdateButton = styled(Button)`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const ProgressCircle = styled(Progress)`
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
left: -2px;
|
||||
`
|
||||
|
||||
export default AboutSettings
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC } from 'react'
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
import { Avatar, message, Select, Upload } from 'antd'
|
||||
import { Avatar, Select, Upload } from 'antd'
|
||||
import styled from 'styled-components'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { compressImage } from '@renderer/utils'
|
||||
@@ -14,7 +14,6 @@ import i18next from 'i18next'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const avatar = useAvatar()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const { language } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
@@ -22,14 +21,26 @@ const GeneralSettings: FC = () => {
|
||||
const onSelectLanguage = (value: string) => {
|
||||
dispatch(setLanguage(value))
|
||||
i18next.changeLanguage(value)
|
||||
// window.location.reload()
|
||||
localStorage.setItem('language', value)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
{contextHolder}
|
||||
<SettingTitle>{t('settings.general.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={language || 'en-US'}
|
||||
style={{ width: 120 }}
|
||||
onChange={onSelectLanguage}
|
||||
options={[
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
{ value: 'en-US', label: 'English' }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
|
||||
<Upload
|
||||
@@ -44,29 +55,13 @@ const GeneralSettings: FC = () => {
|
||||
await LocalStorage.storeImage('avatar', compressedFile)
|
||||
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
|
||||
} catch (error: any) {
|
||||
messageApi.open({
|
||||
type: 'error',
|
||||
content: error.message
|
||||
})
|
||||
window.message.error(error.message)
|
||||
}
|
||||
}}>
|
||||
<UserAvatar src={avatar} size="large" />
|
||||
</Upload>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={language || 'en-US'}
|
||||
style={{ width: 120 }}
|
||||
onChange={onSelectLanguage}
|
||||
options={[
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
{ value: 'en-US', label: 'English' }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
29
src/renderer/src/pages/settings/components/Changelog.tsx
Normal file
29
src/renderer/src/pages/settings/components/Changelog.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import changelogEn from '@renderer/CHANGELOG.en.md?raw'
|
||||
import changelogZh from '@renderer/CHANGELOG.zh.md?raw'
|
||||
import { FC } from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
import styles from './changelog.module.scss'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
const Changelog: FC = () => {
|
||||
const language = i18n.language
|
||||
const changelog = language === 'zh-CN' ? changelogZh : changelogEn
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Markdown className={styles.markdown}>{changelog}</Markdown>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
font-size: 14px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
width: 650px;
|
||||
`
|
||||
|
||||
export default Changelog
|
||||
@@ -86,7 +86,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
return (
|
||||
<Flex>
|
||||
<ModelHeaderTitle>
|
||||
{provider.name} {t('common.models')}
|
||||
{t(`provider.${provider.id}`)} {t('common.models')}
|
||||
</ModelHeaderTitle>
|
||||
{loading && <LoadingOutlined size={20} />}
|
||||
</Flex>
|
||||
|
||||
@@ -18,7 +18,8 @@ interface Props {
|
||||
provider: Provider
|
||||
}
|
||||
|
||||
const ProviderSetting: FC<Props> = ({ provider }) => {
|
||||
const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const { provider } = useProvider(_provider.id)
|
||||
const [apiKey, setApiKey] = useState(provider.apiKey)
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
$background-color: #121212;
|
||||
$text-color: #ffffff;
|
||||
$heading-color: #00b96b;
|
||||
$link-color: #3498db;
|
||||
$code-background: #1e1e1e;
|
||||
$code-color: #f0e7db;
|
||||
|
||||
.markdown {
|
||||
body {
|
||||
background-color: $background-color;
|
||||
color: $text-color;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: $heading-color;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: $code-background;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: $code-background;
|
||||
color: $code-color;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid $heading-color;
|
||||
padding-left: 10px;
|
||||
margin-left: 0;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 20px;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { takeRight } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { t } from 'i18next'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
interface FetchChatCompletionParams {
|
||||
messages: Message[]
|
||||
@@ -27,6 +27,8 @@ const getOpenAiProvider = (provider: Provider) => {
|
||||
}
|
||||
|
||||
export async function fetchChatCompletion({ messages, topic, assistant, onResponse }: FetchChatCompletionParams) {
|
||||
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, false)
|
||||
|
||||
const provider = getAssistantProvider(assistant)
|
||||
const openaiProvider = getOpenAiProvider(provider)
|
||||
const defaultModel = getDefaultModel()
|
||||
@@ -34,7 +36,7 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
||||
|
||||
store.dispatch(setGenerating(true))
|
||||
|
||||
const _message: Message = {
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
@@ -45,7 +47,7 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
||||
status: 'sending'
|
||||
}
|
||||
|
||||
onResponse({ ..._message })
|
||||
onResponse({ ...message })
|
||||
|
||||
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
||||
|
||||
@@ -54,12 +56,10 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
const _messages = [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[]
|
||||
|
||||
try {
|
||||
const stream = await openaiProvider.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: _messages,
|
||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||
stream: true
|
||||
})
|
||||
|
||||
@@ -67,22 +67,27 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
||||
let usage: OpenAI.Completions.CompletionUsage | undefined = undefined
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||
break
|
||||
}
|
||||
|
||||
content = content + (chunk.choices[0]?.delta?.content || '')
|
||||
chunk.usage && (usage = chunk.usage)
|
||||
onResponse({ ..._message, content, status: 'pending' })
|
||||
onResponse({ ...message, content, status: 'pending' })
|
||||
}
|
||||
|
||||
_message.content = content
|
||||
_message.usage = usage
|
||||
message.content = content
|
||||
message.usage = usage
|
||||
} catch (error: any) {
|
||||
_message.content = `Error: ${error.message}`
|
||||
message.content = `Error: ${error.message}`
|
||||
}
|
||||
|
||||
_message.status = 'success'
|
||||
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, _message)
|
||||
const paused = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)
|
||||
message.status = paused ? 'paused' : 'success'
|
||||
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, message)
|
||||
store.dispatch(setGenerating(false))
|
||||
|
||||
return _message
|
||||
return message
|
||||
}
|
||||
|
||||
interface FetchMessagesSummaryParams {
|
||||
@@ -122,17 +127,17 @@ export async function checkApi(provider: Provider) {
|
||||
const style = { marginTop: '3vh' }
|
||||
|
||||
if (!provider.apiKey) {
|
||||
window.message.error({ content: t('error.enter.api.key'), key, style })
|
||||
window.message.error({ content: i18n.t('message.error.enter.api.key'), key, style })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!provider.apiHost) {
|
||||
window.message.error({ content: t('error.enter.api.host'), key, style })
|
||||
window.message.error({ content: i18n.t('message.error.enter.api.host'), key, style })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
window.message.error({ content: t('error.enter.model'), key, style })
|
||||
window.message.error({ content: i18n.t('message.error.enter.model'), key, style })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -142,7 +147,8 @@ export async function checkApi(provider: Provider) {
|
||||
try {
|
||||
const response = await openaiProvider.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 100,
|
||||
stream: false
|
||||
})
|
||||
|
||||
@@ -156,7 +162,9 @@ export async function checkApi(provider: Provider) {
|
||||
key: 'api-check',
|
||||
style: { marginTop: '3vh' },
|
||||
duration: valid ? 2 : 8,
|
||||
content: valid ? t('message.api.connection.successful') : t('message.api.connection.failed') + ' ' + errorMessage
|
||||
content: valid
|
||||
? i18n.t('message.api.connection.success')
|
||||
: i18n.t('message.api.connection.failed') + ' ' + errorMessage
|
||||
})
|
||||
|
||||
return valid
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Assistant, Model, Provider, Topic } from '@renderer/types'
|
||||
import store from '@renderer/store'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import i18next from 'i18next'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
export function getDefaultAssistant(): Assistant {
|
||||
return {
|
||||
id: 'default',
|
||||
name: i18next.t('assistant.default.name'),
|
||||
name: i18n.t('assistant.default.name'),
|
||||
prompt: '',
|
||||
topics: [getDefaultTopic()]
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function getDefaultAssistant(): Assistant {
|
||||
export function getDefaultTopic(): Topic {
|
||||
return {
|
||||
id: uuid(),
|
||||
name: i18next.t('assistant.default.topic.name'),
|
||||
name: i18n.t('assistant.default.topic.name'),
|
||||
messages: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ export const EVENT_NAMES = {
|
||||
CLEAR_MESSAGES: 'CLEAR_MESSAGES',
|
||||
ADD_ASSISTANT: 'ADD_ASSISTANT',
|
||||
EDIT_MESSAGE: 'EDIT_MESSAGE',
|
||||
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE'
|
||||
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE',
|
||||
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED'
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 7,
|
||||
version: 9,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -54,7 +54,7 @@ const initialState: LlmState = {
|
||||
name: 'ZhiPu',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
models: SYSTEM_MODELS.groq.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.defaultEnabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createMigrate } from 'redux-persist'
|
||||
import { RootState } from '.'
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import { isEmpty } from 'lodash'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { Assistant } from '@renderer/types'
|
||||
|
||||
const migrate = createMigrate({
|
||||
// @ts-ignore store type is unknown
|
||||
@@ -112,6 +115,47 @@ const migrate = createMigrate({
|
||||
language: navigator.language
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'8': (state: RootState) => {
|
||||
const fixAssistantName = (assistant: Assistant) => {
|
||||
if (isEmpty(assistant.name)) {
|
||||
assistant.name = i18n.t(`assistant.${assistant.id}.name`)
|
||||
}
|
||||
|
||||
assistant.topics = assistant.topics.map((topic) => {
|
||||
if (isEmpty(topic.name)) {
|
||||
topic.name = i18n.t(`assistant.${assistant.id}.topic.name`)
|
||||
}
|
||||
return topic
|
||||
})
|
||||
|
||||
return assistant
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
assistants: {
|
||||
...state.assistants,
|
||||
defaultAssistant: fixAssistantName(state.assistants.defaultAssistant),
|
||||
assistants: state.assistants.assistants.map((assistant) => fixAssistantName(assistant))
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'9': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
llm: {
|
||||
...state.llm,
|
||||
providers: state.llm.providers.map((provider) => {
|
||||
if (provider.id === 'zhipu' && provider.models[0] && provider.models[0].id === 'llama3-70b-8192') {
|
||||
provider.models = SYSTEM_MODELS.zhipu.filter((m) => m.defaultEnabled)
|
||||
}
|
||||
return provider
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export type Message = {
|
||||
topicId: string
|
||||
modelId?: string
|
||||
createdAt: string
|
||||
status: 'sending' | 'pending' | 'success' | 'error'
|
||||
status: 'sending' | 'pending' | 'success' | 'paused' | 'error'
|
||||
usage?: OpenAI.Completions.CompletionUsage
|
||||
}
|
||||
|
||||
|
||||
@@ -98,3 +98,13 @@ export const firstLetter = (str?: string) => {
|
||||
export function isFreeModel(model: Model) {
|
||||
return (model.id + model.name).toLocaleLowerCase().includes('free')
|
||||
}
|
||||
|
||||
export async function isProduction() {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
return isPackaged
|
||||
}
|
||||
|
||||
export async function isDev() {
|
||||
const isProd = await isProduction()
|
||||
return !isProd
|
||||
}
|
||||
|
||||
176
src/website/index.html
Normal file
176
src/website/index.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Cherry Studio 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
|
||||
<meta name="keywords" content="Cherry Studio, AI 助手, GPT 客户端, 多模型, iOS, macOS, Windows, LLM" />
|
||||
<meta name="author" content="kangfenmao" />
|
||||
<link rel="canonical" href="https://easys.run/cherry-studio" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://easys.run/cherry-studio" />
|
||||
<meta property="og:title" content="Cherry Studio - 多模型 AI 助手" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Cherry Studio 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
|
||||
<meta property="og:image" content="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://x.com/kangfenmao" />
|
||||
<meta property="twitter:title" content="Cherry Studio - 多模型 AI 助手" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Cherry Studio 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
|
||||
<meta
|
||||
property="twitter:image"
|
||||
content="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" />
|
||||
|
||||
<title>Cherry Studio - 多模型AI助手</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 10%;
|
||||
}
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.description {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
.download-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.download-btn {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.download-btn:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
.download-btn svg {
|
||||
margin-right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.new-app {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
.footer a {
|
||||
color: #a0a0a0;
|
||||
text-decoration: none;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img
|
||||
src="https://github.com/kangfenmao/cherry-studio/blob/main/resources/icon.png?raw=true"
|
||||
alt="Cherry Studio Logo"
|
||||
class="logo" />
|
||||
<h1>Cherry Studio</h1>
|
||||
<p class="description">Windows/macOS GPT 客户端</p>
|
||||
<div class="download-buttons">
|
||||
<a
|
||||
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.3/Cherry-Studio-0.2.3-x64.dmg"
|
||||
class="download-btn">
|
||||
<svg viewBox="0 0 384 512" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
|
||||
</svg>
|
||||
macOS Intel
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.3/Cherry-Studio-0.2.3-arm64.dmg"
|
||||
class="download-btn">
|
||||
<svg viewBox="0 0 384 512" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
|
||||
</svg>
|
||||
macOS Apple Silicon
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.3/Cherry-Studio-0.2.3-setup.exe"
|
||||
class="download-btn">
|
||||
<svg viewBox="0 0 448 512" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" />
|
||||
</svg>
|
||||
下载 Windows 版本
|
||||
</a>
|
||||
</div>
|
||||
<p class="new-app">
|
||||
🎉 Cherry Studio 最新版本
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/releases/tag/v0.2.3" target="_blank">v0.2.3</a> 发布啦!
|
||||
</p>
|
||||
<div class="footer">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">开源</a> |
|
||||
<a href="https://github.com/kangfenmao/cherry-studio/blob/main/README.md" target="_blank">帮助</a> |
|
||||
<a href="mailto:kangfenmao@qq.com" target="_blank">联系</a>
|
||||
</div>
|
||||
<!-- 结构化数据 -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Cherry Studio",
|
||||
"applicationCategory": "UtilitiesApplication",
|
||||
"operatingSystem": "iOS, macOS, Windows",
|
||||
"description": "Cherry Studio 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user