Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17c3437e02 | ||
|
|
69293846fc | ||
|
|
20a7fbfc48 | ||
|
|
64d4b8450a | ||
|
|
f080fc5048 | ||
|
|
50f08124d7 | ||
|
|
b91081ef99 | ||
|
|
d869ec9a9b | ||
|
|
70c4354d6c | ||
|
|
527c4e77dc | ||
|
|
2483ce3bb4 | ||
|
|
db3f8b8bee | ||
|
|
45bf3d4e86 | ||
|
|
59b39dc41a | ||
|
|
a267a8d4c3 | ||
|
|
5b123f2c33 | ||
|
|
fe34fb3c25 | ||
|
|
e6359d2048 | ||
|
|
aa3b2d6290 | ||
|
|
c0e51c3992 | ||
|
|
8c80cc00b3 | ||
|
|
f961accd86 | ||
|
|
7de91d236d | ||
|
|
2fdf0acec6 | ||
|
|
40e76f3e53 | ||
|
|
d7b8721848 | ||
|
|
b91b0dd8e4 | ||
|
|
bb9b053924 | ||
|
|
5743046200 | ||
|
|
a507776c1e | ||
|
|
e74c828379 | ||
|
|
4b264c6a6b | ||
|
|
d21a4dce92 | ||
|
|
74df29604b | ||
|
|
8807783aa6 | ||
|
|
f81b38a362 | ||
|
|
d0280186bc | ||
|
|
9d96b826e2 | ||
|
|
ec20750e64 | ||
|
|
51f4653cde | ||
|
|
3625eefec4 | ||
|
|
1e1414d659 | ||
|
|
b7162663f2 | ||
|
|
1dd1bb5804 | ||
|
|
4dd6c46035 | ||
|
|
4036c36753 | ||
|
|
764aadd234 | ||
|
|
3d801f1552 | ||
|
|
bd865f0270 | ||
|
|
93505a4bc6 | ||
|
|
c43be11d20 | ||
|
|
8535edbdd1 | ||
|
|
731fb7860b | ||
|
|
4a32976483 | ||
|
|
dedabe320e | ||
|
|
235b481645 | ||
|
|
58c5ace678 | ||
|
|
973d24271b | ||
|
|
f434fe1231 | ||
|
|
a0c147ae3f | ||
|
|
8d7cde1231 | ||
|
|
87c04408de | ||
|
|
2592448c74 | ||
|
|
6f054874e8 | ||
|
|
40d687104e | ||
|
|
ac3cfe2878 | ||
|
|
e9a7735fce | ||
|
|
c1a8198575 | ||
|
|
8b45548b79 | ||
|
|
3f3b930819 | ||
|
|
a5d6e2c5c5 | ||
|
|
2993ab8dc1 | ||
|
|
117069e450 | ||
|
|
c5965dc696 | ||
|
|
4169a2ef35 | ||
|
|
75c37632d4 | ||
|
|
3f5c151a11 | ||
|
|
d049e36c46 | ||
|
|
d05fc1c9be | ||
|
|
f33317a3fb | ||
|
|
f2b5ed09c0 | ||
|
|
81e66dde0e | ||
|
|
f76388d979 | ||
|
|
9e542f813c | ||
|
|
5ede95cf2e | ||
|
|
fd8b15ebbe | ||
|
|
5a636e7614 | ||
|
|
13c73a3de1 | ||
|
|
31284a6e23 | ||
|
|
c4394b925d | ||
|
|
93a5739d87 | ||
|
|
f23c4a0afa | ||
|
|
8723c251b1 | ||
|
|
a9634fd684 | ||
|
|
53757626f2 | ||
|
|
83af70e460 | ||
|
|
3377aae0ff | ||
|
|
2c4d18843b | ||
|
|
244cce0b65 | ||
|
|
af41cebe18 | ||
|
|
00cf2d6b24 | ||
|
|
2507dd1bcf | ||
|
|
a6ff6e3a4d | ||
|
|
474beca088 | ||
|
|
810c44f7fc | ||
|
|
8bff4df722 | ||
|
|
a19c69340a | ||
|
|
c250124043 | ||
|
|
7aa6d6ebeb | ||
|
|
e962351b13 | ||
|
|
80e34688b1 | ||
|
|
8c23d6ec55 | ||
|
|
2cc09a52f4 |
3
.gitignore
vendored
@@ -34,6 +34,9 @@ npm/*/*
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
|
||||
# Project
|
||||
node_modules
|
||||
dist
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Cherry Studio
|
||||
|
||||
Cherry Studio is a desktop client for multiple cutting-edge LLM models, available on Windows, Mac and Linux.
|
||||
🍒 Cherry Studio is a desktop client that supports multiple Large Language Model (LLM) providers, available on Windows, Mac and Linux.
|
||||
|
||||
# Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
# Feature
|
||||
|
||||
@@ -16,6 +18,7 @@ Cherry Studio is a desktop client for multiple cutting-edge LLM models, availabl
|
||||
4. Allows using multiple models to answer questions in the same conversation.
|
||||
5. Supports drag-and-drop sorting.
|
||||
6. Code highlighting.
|
||||
7. Mermaid chart
|
||||
|
||||
# Develop
|
||||
## Recommended IDE Setup
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -56,7 +56,5 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
- 修复多语言提示错误
|
||||
- 修复智谱AI默认模型错误问题
|
||||
- 修复 OpenRouter API 检测出错问题
|
||||
- 修复模型提供商多语言翻译错误问题
|
||||
支持主题切换
|
||||
修复一些问题
|
||||
|
||||
@@ -16,9 +16,6 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
plugins: [react()],
|
||||
assetsInclude: ['**/*.md'],
|
||||
server: {
|
||||
host: '0.0.0.0'
|
||||
}
|
||||
assetsInclude: ['**/*.md']
|
||||
}
|
||||
})
|
||||
|
||||
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.2.3",
|
||||
"version": "0.4.1",
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
@@ -18,20 +18,23 @@
|
||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||
"build:win": "dotenv npm run build && electron-builder --win --publish never",
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never"
|
||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
|
||||
"release": "node scripts/version.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@sentry/electron": "^5.2.0",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-window-state": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.3",
|
||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@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",
|
||||
@@ -54,6 +57,7 @@
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"gpt-tokens": "^1.3.6",
|
||||
"i18next": "^23.11.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -6,17 +6,21 @@ exports.default = async function notarizing(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
|
||||
console.log('Skipping notarization')
|
||||
return
|
||||
}
|
||||
|
||||
const notarized = await notarize({
|
||||
appPath: `${context.appOutDir}/${appName}.app`,
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
const appPath = `${context.appOutDir}/${appName}.app`
|
||||
|
||||
await notarize({
|
||||
appPath,
|
||||
appBundleId: 'com.kangfenmao.CherryStudio',
|
||||
appleId: process.env.APPLE_ID,
|
||||
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
||||
teamId: process.env.APPLE_TEAM_ID
|
||||
})
|
||||
|
||||
console.log('Notarized:', notarized)
|
||||
|
||||
return notarized
|
||||
console.log(' • Notarized app:', appPath)
|
||||
}
|
||||
|
||||
40
scripts/version.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { execSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
|
||||
// 执行命令并返回输出
|
||||
function exec(command) {
|
||||
return execSync(command, { encoding: 'utf8' }).trim()
|
||||
}
|
||||
|
||||
// 获取命令行参数
|
||||
const args = process.argv.slice(2)
|
||||
const versionType = args[0] || 'patch'
|
||||
const shouldPush = args.includes('push')
|
||||
|
||||
// 验证版本类型
|
||||
if (!['patch', 'minor', 'major'].includes(versionType)) {
|
||||
console.error('Invalid version type. Use patch, minor, or major.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 更新版本
|
||||
exec(`yarn version ${versionType} --immediate`)
|
||||
|
||||
// 读取更新后的 package.json 获取新版本号
|
||||
const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||
const newVersion = updatedPackageJson.version
|
||||
|
||||
// Git 操作
|
||||
exec('git add .')
|
||||
exec(`git commit -m "chore(version): ${newVersion}"`)
|
||||
exec(`git tag -a v${newVersion} -m "Version ${newVersion}"`)
|
||||
|
||||
console.log(`Version bumped to ${newVersion}`)
|
||||
|
||||
if (shouldPush) {
|
||||
console.log('Pushing to remote...')
|
||||
exec('git push && git push --tags')
|
||||
console.log('Pushed to remote.')
|
||||
} else {
|
||||
console.log('Changes are committed locally. Use "git push && git push --tags" to push to remote.')
|
||||
}
|
||||
15
src/main/config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Store from 'electron-store'
|
||||
|
||||
export const appConfig = new Store()
|
||||
|
||||
export const titleBarOverlayDark = {
|
||||
height: 41,
|
||||
color: '#1f1f1f',
|
||||
symbolColor: '#ffffff'
|
||||
}
|
||||
|
||||
export const titleBarOverlayLight = {
|
||||
height: 41,
|
||||
color: '#f8f8f8',
|
||||
symbolColor: '#000000'
|
||||
}
|
||||
24
src/main/event.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||
import { writeFile } from 'fs'
|
||||
import logger from 'electron-log'
|
||||
|
||||
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
|
||||
try {
|
||||
const options: SaveDialogOptions = {
|
||||
title: '保存文件',
|
||||
defaultPath: fileName
|
||||
}
|
||||
|
||||
const result: SaveDialogReturnValue = await dialog.showSaveDialog(options)
|
||||
|
||||
if (!result.canceled && result.filePath) {
|
||||
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
||||
if (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import * as Sentry from '@sentry/electron/main'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, 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 { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { saveFile } from './event'
|
||||
import AppUpdater from './updater'
|
||||
|
||||
function createWindow(): void {
|
||||
function createWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = appConfig.get('theme') || 'light'
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
@@ -23,13 +28,15 @@ function createWindow(): void {
|
||||
minHeight: 500,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
devTools: !app.isPackaged
|
||||
webSecurity: false
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -45,6 +52,11 @@ function createWindow(): void {
|
||||
menu.popup()
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
@@ -61,6 +73,8 @@ function createWindow(): void {
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -77,26 +91,48 @@ 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()
|
||||
|
||||
if (app.isPackaged) {
|
||||
setTimeout(() => new AppUpdater(), 3000)
|
||||
}
|
||||
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('set-proxy', (_, proxy: string) => {
|
||||
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
||||
})
|
||||
|
||||
ipcMain.handle('save-file', saveFile)
|
||||
|
||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||
appConfig.set('theme', theme)
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
})
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
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
|
||||
@@ -110,3 +146,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.checkForUpdates()
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
ipcMain.on('check-for-update', () => {
|
||||
logger.info('触发检查更新')
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
6
src/preload/index.d.ts
vendored
@@ -6,7 +6,13 @@ declare global {
|
||||
api: {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import { electronAPI } from '@electron-toolkit/preload'
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update')
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<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' *; img-src 'self' data:" />
|
||||
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:" />
|
||||
</head>
|
||||
|
||||
<body theme-mode="dark">
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
import '@fontsource/inter'
|
||||
import store, { persistor } from '@renderer/store'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
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 AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
import AntdProvider from './providers/AntdProvider'
|
||||
import { ThemeProvider } from './providers/ThemeProvider'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<ConfigProvider theme={AntdThemeConfig} locale={getAntdLocale()}>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</ConfigProvider>
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# CHANGES LOG
|
||||
|
||||
### v0.2.3 - 2024-07-16
|
||||
|
||||
1. Fixed multi-language prompt errors
|
||||
2. Fixed default model error issues with ZHIPU AI
|
||||
3. Fixed OpenRouter API detection error issues
|
||||
4. Fixed multi-language translation errors with model providers
|
||||
|
||||
### v0.2.2 - 2024-07-15
|
||||
|
||||
1. Fix the issue where the default assistant name is empty.
|
||||
2. Fix the problem with default language detection during the first installation.
|
||||
3. Adjust the changelog style.
|
||||
|
||||
### v0.2.1 - 2024-07-15
|
||||
|
||||
1. **Feature**: Add new feature for pausing message sending
|
||||
2. **Fix**: Resolve incomplete translation issue upon language switch
|
||||
3. **Build**: Support for macOS Intel architecture
|
||||
@@ -1,21 +0,0 @@
|
||||
# 更新日志
|
||||
|
||||
### v0.2.3 - 2024-07-16
|
||||
|
||||
1. 修复多语言提示错误
|
||||
2. 修复智谱AI默认模型错误问题
|
||||
3. 修复 OpenRouter API 检测出错问题
|
||||
4. 修复模型提供商多语言翻译错误问题
|
||||
|
||||
### v0.2.2 - 2024-07-15
|
||||
|
||||
1. 修复默认助理名称为空的问题
|
||||
2. 修复首次安装默认语言检测问题
|
||||
3. 更新日志样式微调
|
||||
|
||||
### v0.2.1 - 2024-07-15
|
||||
|
||||
1. 【功能】新增消息暂停发送功能
|
||||
2. 【修复】修复多语言切换不彻底问题
|
||||
3. 【构建】支持 macOS Intel 架构
|
||||
|
||||
55
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal file
@@ -0,0 +1,55 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
|
||||
url('iconfont.woff?t=1722242729348') format('woff'),
|
||||
url('iconfont.ttf?t=1722242729348') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
}
|
||||
|
||||
.icon-hidesidebarhoriz:before {
|
||||
content: "\e8eb";
|
||||
}
|
||||
|
||||
.icon-showsidebarhoriz:before {
|
||||
content: "\e944";
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: "\e658";
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
BIN
src/renderer/src/assets/images/models/baichuan.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/models/claude.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
BIN
src/renderer/src/assets/images/models/qwen.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/renderer/src/assets/images/providers/aihubmix.jpg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src/renderer/src/assets/images/providers/anthropic.jpeg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/renderer/src/assets/images/providers/baichuan.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/renderer/src/assets/images/providers/dashscope.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -1,78 +0,0 @@
|
||||
$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: 30px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
|
||||
@import '../fonts/icon-fonts/iconfont.css';
|
||||
@import './markdown.scss';
|
||||
|
||||
// @font-face {
|
||||
// font-family: 'Playwrite';
|
||||
// src: url(../fonts/Playwrite.ttf) format('truetype');
|
||||
// }
|
||||
@import './scrollbar.scss';
|
||||
|
||||
:root {
|
||||
--color-white: #ffffff;
|
||||
@@ -12,7 +8,7 @@
|
||||
--color-white-mute: #f2f2f2;
|
||||
|
||||
--color-black: #1b1b1f;
|
||||
--color-black-soft: #303030;
|
||||
--color-black-soft: #262626;
|
||||
--color-black-mute: #363636;
|
||||
|
||||
--color-gray-1: #515c67;
|
||||
@@ -27,18 +23,67 @@
|
||||
--color-background-soft: var(--color-black-soft);
|
||||
--color-background-mute: var(--color-black-mute);
|
||||
|
||||
--color-primary: #135200;
|
||||
--color-primary-soft: #13520099;
|
||||
--color-primary-mute: #13520033;
|
||||
|
||||
--color-text: var(--color-text-1);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff20;
|
||||
--color-error: #f44336;
|
||||
--color-code-background: #323232;
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
|
||||
|
||||
--navbar-background: #1f1f1f;
|
||||
--sidebar-background: #1f1f1f;
|
||||
|
||||
--navbar-height: 42px;
|
||||
--sidebar-width: 55px;
|
||||
--assistants-width: 235px;
|
||||
--topic-list-width: 250px;
|
||||
--assistants-width: 245px;
|
||||
--topic-list-width: 260px;
|
||||
--settings-width: var(--assistants-width);
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 120px;
|
||||
--input-bar-height: 125px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
--color-white: #ffffff;
|
||||
--color-white-soft: #f8f8f8;
|
||||
--color-white-mute: #efefef;
|
||||
|
||||
--color-black: #1b1b1f;
|
||||
--color-black-soft: #262626;
|
||||
--color-black-mute: #363636;
|
||||
|
||||
--color-gray-1: #8e8e93;
|
||||
--color-gray-2: #aeaeb2;
|
||||
--color-gray-3: #c7c7cc;
|
||||
|
||||
--color-text-1: rgba(0, 0, 0, 1);
|
||||
--color-text-2: rgba(0, 0, 0, 0.6);
|
||||
--color-text-3: rgba(0, 0, 0, 0.38);
|
||||
|
||||
--color-background: #ffffff;
|
||||
--color-background-soft: var(--color-white-soft);
|
||||
--color-background-mute: var(--color-white-mute);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
--color-primary-mute: #00b96b33;
|
||||
|
||||
--color-text: var(--color-text-1);
|
||||
--color-icon: #00000099;
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000028;
|
||||
--color-error: #f44336;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
|
||||
|
||||
--navbar-background: #f8f8f8;
|
||||
--sidebar-background: #f8f8f8;
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -62,9 +107,9 @@ body {
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Microsoft YaHei',
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
@@ -72,8 +117,7 @@ body {
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
'Helvetica Neue' sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -102,29 +146,8 @@ body,
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* 全局初始化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Safari 和 Chrome */
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
body {
|
||||
scrollbar-width: thin; /* 告诉 FF 用细滚动条 */
|
||||
scrollbar-color: rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.1); /* FF 前面色后面色 */
|
||||
.chat-nav-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
.markdown {
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
user-select: text;
|
||||
margin-top: 4px;
|
||||
|
||||
p:first-of-type {
|
||||
p:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1:first-of-type,
|
||||
h2:first-of-type,
|
||||
h3:first-of-type,
|
||||
h4:first-of-type,
|
||||
h5:first-of-type,
|
||||
h6:first-of-type {
|
||||
h1:first-child,
|
||||
h2:first-child,
|
||||
h3:first-child,
|
||||
h4:first-child,
|
||||
h5:first-child,
|
||||
h6:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -30,44 +33,36 @@
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.5em;
|
||||
margin: 1em 0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
li {
|
||||
@@ -84,4 +79,8 @@
|
||||
span {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
15
src/renderer/src/assets/styles/scrollbar.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
/* 全局初始化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar-thumb);
|
||||
&:hover {
|
||||
background: var(--color-scrollbar-thumb-hover);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAppInitEffect } from '@renderer/hooks/useAppInitEffect'
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { message, Modal } from 'antd'
|
||||
import { findIndex, pullAt } from 'lodash'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
@@ -29,7 +29,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
const [messageApi, messageContextHolder] = message.useMessage()
|
||||
const [modal, modalContextHolder] = Modal.useModal()
|
||||
|
||||
useAppInitEffect()
|
||||
useAppInit()
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elements]
|
||||
@@ -61,7 +61,11 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
|
||||
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
|
||||
{elements.map(({ element: Element, key }) =>
|
||||
typeof Element === 'function' ? <Element key={`TOPVIEW_${key}`} /> : Element
|
||||
typeof Element === 'function' ? (
|
||||
<Element key={`TOPVIEW_${key}`} />
|
||||
) : (
|
||||
<div key={`TOPVIEW_${key}`}>{Element}</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -25,32 +25,32 @@ const NavbarContainer = styled.div`
|
||||
flex-direction: row;
|
||||
min-height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
-webkit-app-region: drag;
|
||||
background-color: #1f1f1f;
|
||||
background-color: var(--navbar-background);
|
||||
margin-left: calc(var(--sidebar-width) * -1);
|
||||
padding-left: var(--sidebar-width);
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const NavbarLeftContainer = styled.div`
|
||||
min-width: var(--assistants-width);
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
const NavbarCenterContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
text-align: center;
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
const NavbarRightContainer = styled.div`
|
||||
|
||||
@@ -3,6 +3,8 @@ import Logo from '@renderer/assets/images/logo.png'
|
||||
import styled from 'styled-components'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { TranslationOutlined } from '@ant-design/icons'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
@@ -11,7 +13,8 @@ const Sidebar: FC = () => {
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container style={isWindows ? { paddingTop: 0 } : {}}>
|
||||
{isMac ? <PlaceholderBorderMac /> : <PlaceholderBorderWin />}
|
||||
<StyledLink to="/">
|
||||
<AvatarImg src={avatar || Logo} draggable={false} />
|
||||
</StyledLink>
|
||||
@@ -27,6 +30,11 @@ const Sidebar: FC = () => {
|
||||
<i className="iconfont icon-appstore"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/translate">
|
||||
<Icon className={isRoute('/translate')}>
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus>
|
||||
@@ -44,14 +52,14 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
padding: 8px 0;
|
||||
min-width: var(--sidebar-width);
|
||||
min-height: 100%;
|
||||
-webkit-app-region: drag !important;
|
||||
background-color: #1f1f1f;
|
||||
background-color: var(--sidebar-background);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
margin-top: var(--navbar-height);
|
||||
padding-bottom: calc(var(--navbar-height) + 6px);
|
||||
padding-top: var(--navbar-height);
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const AvatarImg = styled.img`
|
||||
@@ -60,6 +68,7 @@ const AvatarImg = styled.img`
|
||||
height: 28px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin: 5px 0;
|
||||
margin-top: ${isMac ? '16px' : '9px'};
|
||||
`
|
||||
const MainMenus = styled.div`
|
||||
display: flex;
|
||||
@@ -82,22 +91,28 @@ const Icon = styled.div`
|
||||
margin-bottom: 5px;
|
||||
transition: background-color 0.2s ease;
|
||||
-webkit-app-region: none;
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 20px;
|
||||
transition: color 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 17px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #ffffff30;
|
||||
background-color: var(--color-background-soft);
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: #ffffff20;
|
||||
.iconfont {
|
||||
background-color: var(--color-background-mute);
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
@@ -111,4 +126,24 @@ const StyledLink = styled(Link)`
|
||||
}
|
||||
`
|
||||
|
||||
const PlaceholderBorderMac = styled.div`
|
||||
width: var(--sidebar-width);
|
||||
height: var(--navbar-height);
|
||||
background: var(--navbar-background);
|
||||
border-right: 1px solid var(--navbar-background);
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
const PlaceholderBorderWin = styled.div`
|
||||
width: var(--sidebar-width);
|
||||
height: var(--navbar-height);
|
||||
position: absolute;
|
||||
border-right: 1px solid var(--navbar-background);
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
`
|
||||
|
||||
export default Sidebar
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import store from '@renderer/store'
|
||||
import { theme, ThemeConfig } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
|
||||
export const colorPrimary = '#00b96b'
|
||||
|
||||
export const AntdThemeConfig: ThemeConfig = {
|
||||
token: {
|
||||
colorPrimary,
|
||||
borderRadius: 5
|
||||
},
|
||||
algorithm: [theme.darkAlgorithm]
|
||||
}
|
||||
|
||||
export function getAntdLocale() {
|
||||
const language = store.getState().settings.language
|
||||
|
||||
switch (language) {
|
||||
case 'zh-CN':
|
||||
return zhCN
|
||||
case 'en-US':
|
||||
return undefined
|
||||
default:
|
||||
return zhCN
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { SystemAssistant } from '@renderer/types'
|
||||
|
||||
export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D29',
|
||||
name: '文章总结',
|
||||
prompt: '总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复',
|
||||
group: '文章'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
|
||||
name: '论文',
|
||||
prompt:
|
||||
'我希望你能作为一名学者行事。你将负责研究一个你选择的主题,并将研究结果以论文或文章的形式呈现出来。你的任务是确定可靠的来源,以结构良好的方式组织材料,并以引用的方式准确记录。',
|
||||
group: '写作'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D40',
|
||||
name: '翻译成中文',
|
||||
prompt:
|
||||
'你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。',
|
||||
group: '翻译'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D41',
|
||||
name: '翻译成英文',
|
||||
prompt:
|
||||
'你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。',
|
||||
group: '翻译'
|
||||
},
|
||||
{
|
||||
id: '43CEDACF-C9EB-431B-848C-4D08EC26EB90',
|
||||
name: '软件工程师',
|
||||
prompt:
|
||||
'你是一个高级软件工程师,你需要帮我解答各种技术难题、设计技术方案以及编写代码。你编写的代码必须可以正常运行,而且没有任何 Bug 和其他问题。',
|
||||
group: '软件工程师'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2A',
|
||||
name: '前端工程师',
|
||||
prompt:
|
||||
'你擅长使用 TypeScript, JavaScript, HMLT, CSS 等编程语言。同时你还会使用 Node.js 及各种包来解决开发中遇到的问题。你还会使用 React, Vue 等前端框架。对于我的问题希望你能给出具体的代码示例,最好能够封装成一个函数方便我复制运行测试。',
|
||||
group: '软件工程师'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2B',
|
||||
name: '后端工程师',
|
||||
prompt:
|
||||
'高级后端工程师,技术难题解答,服务器架构,数据库优化,API设计,网络安全,代码审查,性能调优,微服务,分布式系统,容器技术,持续集成/持续部署(CI/CD)。',
|
||||
group: '软件工程师'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2D',
|
||||
name: '测试工程师',
|
||||
prompt: '你是一个高级测试工程师,你需要帮我解答各种技术难题',
|
||||
group: '软件工程师'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2E',
|
||||
name: 'Python 工程师',
|
||||
prompt: '你是一个高级Python工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2F',
|
||||
name: 'Java 工程师',
|
||||
prompt: '你是一个高级Java工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
|
||||
name: 'C# 工程师',
|
||||
prompt: '你是一个高级C#工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D31',
|
||||
name: 'C++ 工程师',
|
||||
prompt: '你是一个高级C++工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D32',
|
||||
name: 'C 工程师',
|
||||
prompt: '你是一个高级C工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D33',
|
||||
name: 'Go 工程师',
|
||||
prompt: '你是一个高级Go工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D34',
|
||||
name: 'Rust 工程师',
|
||||
prompt: '你是一个高级Rust工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D35',
|
||||
name: 'PHP 工程师',
|
||||
prompt: '你是一个高级PHP工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D36',
|
||||
name: 'Ruby 工程师',
|
||||
prompt: '你是一个高级Ruby工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D37',
|
||||
name: 'Swift 工程师',
|
||||
prompt: '你是一个高级Swift工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D38',
|
||||
name: 'Kotlin 工程师',
|
||||
prompt: '你是一个高级Kotlin工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D39',
|
||||
name: 'Dart 工程师',
|
||||
prompt: '你是一个高级Dart工程师,你需要帮我解答各种技术难题',
|
||||
group: '编程语言'
|
||||
}
|
||||
]
|
||||
242
src/renderer/src/config/assistants.json
Normal file
@@ -0,0 +1,242 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "🎯 产品经理 - Product Manager",
|
||||
"emoji": "🎯",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "🎯 策略产品经理 - Strategy Product Manager",
|
||||
"emoji": "🎯 ",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "👥 社群运营 - Community Operations",
|
||||
"emoji": "👥",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "✍️ 内容运营 - Content Operations",
|
||||
"emoji": "✍️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "🛍️ 商家运营 - Merchant Operations",
|
||||
"emoji": "🛍️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "🚀 产品运营 - Product Operations",
|
||||
"emoji": "🚀",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "💼 销售运营 - Sales Operations",
|
||||
"emoji": "🎓",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "👨💻 用户运营 - User Operations",
|
||||
"emoji": "👨💻",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "📢 市场营销 - Marketing",
|
||||
"emoji": "📢",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "📈 商业数据分析 - Business Data Analysis",
|
||||
"emoji": "📈",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "🗂️ 项目管理 - Project Management",
|
||||
"emoji": "🗂️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "🔎 SEO专家 - SEO Expert",
|
||||
"emoji": "🔎",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名知识丰富的SEO专家,你了解搜索引擎的工作原理,熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名知识丰富的SEO专家,你了解搜索引擎的工作原理,熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "💻 网站运营数据分析 - Website Operations Data Analysis",
|
||||
"emoji": "💻",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "📊 数据分析师 - Data Analyst",
|
||||
"emoji": "📊",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "🖥️ 前端工程师 - Frontend Engineer",
|
||||
"emoji": "🖥️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的前端工程师,你对HTML、CSS、JavaScript等前端技术有深入的了解,能够制作和优化用户界面。你能够解决浏览器兼容性问题,提升网页性能,并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名专业的前端工程师,你对HTML、CSS、JavaScript等前端技术有深入的了解,能够制作和优化用户界面。你能够解决浏览器兼容性问题,提升网页性能,并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "🛠️ 运维工程师 - Operations Engineer",
|
||||
"emoji": "🛠️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "💻 开发工程师 - Software Engineer",
|
||||
"emoji": "💻",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "🧪 测试工程师 - Test Engineer",
|
||||
"emoji": "🧪",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "👥 HR人力资源管理 - Human Resources Management",
|
||||
"emoji": "👥",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "📋 行政 - Administration",
|
||||
"emoji": "📋",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "💰 财务顾问 - Financial Advisor",
|
||||
"emoji": "💰",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "🩺 医生 - Doctor",
|
||||
"emoji": "🩺",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "✒️ 编辑 - Editor",
|
||||
"emoji": "✒️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "🧠 哲学家 - Philosopher",
|
||||
"emoji": "🧠",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "🛒 采购 - Procurement",
|
||||
"emoji": "🛒",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "⚖️ 法务 - Legal Affairs",
|
||||
"emoji": "⚖️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "🇨🇳 翻译成中文 - Chinese",
|
||||
"emoji": "🇨🇳",
|
||||
"group": "语言",
|
||||
"prompt": "你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "🌐 翻译成英文 - English",
|
||||
"emoji": "🌐",
|
||||
"group": "语言",
|
||||
"prompt": "你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "📕 英语单词背诵助手",
|
||||
"emoji": "📕",
|
||||
"group": "语言",
|
||||
"prompt": "- 版本:0.1\n- 语言:中文\n- 描述:您是一位语言专家,擅长阐释英语词汇的复杂性。您的角色是将复杂的英语单词分解为简单的概念,提供易懂的英语解释,提供中文翻译,并提供助记设备以帮助记忆。\n\n技能\n1. 分析高级英语单词的拼写、发音和含义。\n2. 使用简单的英语词汇进行解释,然后提供中文翻译。\n3. 使用音标联想、形象联想和词源等记忆技巧。\n4. 创作高质量的句子,以示范单词在语境中的使用。\n\n规则\n1. 总是以使用简单的英语词汇进行解释为开头。\n2. 在适当的时候,保持解释和例句的清晰、准确和幽默。\n3. 确保助记设备与记忆相关且有效。\n\n工作流程\n1. 问候用户并询问他们感兴趣的英语单词。\n2. 分解单词,分析其拼写、发音和复杂含义。\n3. 用简单的英语词汇解释,使含义更易理解。\n4. 提供单词的中文翻译和简单的英语解释。\n5. 针对单词的特点提供个性化的助记策略。\n6. 使用单词构建高质量、信息丰富且引人入胜的句子。\n\n初始化\n作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "📖 文章总结 - Summarize",
|
||||
"emoji": "📖",
|
||||
"group": "阅读",
|
||||
"prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复",
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
export const DEFAULT_TEMPERATURE = 0.7
|
||||
export const DEFAULT_CONEXTCOUNT = 5
|
||||
export const platform = window.electron?.process?.platform === 'darwin' ? 'macos' : 'windows'
|
||||
export const isMac = platform === 'macos'
|
||||
export const isWindows = platform === 'windows'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Model } from '@renderer/types'
|
||||
|
||||
type SystemModel = Model & { defaultEnabled: boolean }
|
||||
type SystemModel = Model & { enabled: boolean }
|
||||
|
||||
export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
openai: [
|
||||
@@ -9,28 +9,28 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'openai',
|
||||
name: 'GPT-3.5 Turbo',
|
||||
group: 'GPT 3.5',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-turbo',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4 Turbo',
|
||||
group: 'GPT 4',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'gpt-4',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4',
|
||||
group: 'GPT 4',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4o',
|
||||
group: 'GPT 4o',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
silicon: [
|
||||
@@ -39,112 +39,112 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-7B-Instruct',
|
||||
group: 'Qwen2',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-1.5B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-1.5B-Instruct',
|
||||
group: 'Qwen2',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-7B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-7B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-72B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-72B-Instruct',
|
||||
group: 'Qwen2',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-57B-A14B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-57B-A14B-Instruct',
|
||||
group: 'Qwen2',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-110B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-110B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-32B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-32B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-14B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-14B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'DeepSeek-V2-Chat',
|
||||
group: 'DeepSeek',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'DeepSeek-Coder-V2-Instruct',
|
||||
group: 'DeepSeek',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/deepseek-llm-67b-chat',
|
||||
provider: 'silicon',
|
||||
name: 'Deepseek-LLM-67B-Chat',
|
||||
group: 'DeepSeek',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
provider: 'silicon',
|
||||
name: 'GLM-4-9B-Chat',
|
||||
group: 'GLM',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'THUDM/chatglm3-6b',
|
||||
provider: 'silicon',
|
||||
name: 'GhatGLM3-6B',
|
||||
group: 'GLM',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-9B-Chat-16K',
|
||||
provider: 'silicon',
|
||||
name: 'Yi-1.5-9B-Chat-16K',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-6B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Yi-1.5-6B-Chat',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-34B-Chat-16K',
|
||||
provider: 'silicon',
|
||||
name: 'Yi-1.5-34B-Chat-16K',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
deepseek: [
|
||||
@@ -153,14 +153,14 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'deepseek',
|
||||
name: 'DeepSeek Chat',
|
||||
group: 'DeepSeek Chat',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'deepseek-coder',
|
||||
provider: 'deepseek',
|
||||
name: 'DeepSeek Coder',
|
||||
group: 'DeepSeek Coder',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
yi: [
|
||||
@@ -169,42 +169,42 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'yi',
|
||||
name: 'Yi-Large',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'yi-large-turbo',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Large-Turbo',
|
||||
group: 'Yi',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'yi-large-rag',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Large-Rag',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'yi-medium',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Medium',
|
||||
group: 'Yi',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'yi-medium-200k',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Medium-200k',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'yi-spark',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Spark',
|
||||
group: 'Yi',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
zhipu: [
|
||||
@@ -213,42 +213,42 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-0520',
|
||||
group: 'GLM',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'glm-4',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4',
|
||||
group: 'GLM',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'glm-4-airx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AirX',
|
||||
group: 'GLM',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'glm-4-air',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Air',
|
||||
group: 'GLM',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'glm-4v',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4V',
|
||||
group: 'GLM',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'glm-4-alltools',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AllTools',
|
||||
group: 'GLM',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
moonshot: [
|
||||
@@ -257,21 +257,83 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'moonshot',
|
||||
name: 'Moonshot V1 8k',
|
||||
group: 'Moonshot V1',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'moonshot-v1-32k',
|
||||
provider: 'moonshot',
|
||||
name: 'Moonshot V1 32k',
|
||||
group: 'Moonshot V1',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'moonshot-v1-128k',
|
||||
provider: 'moonshot',
|
||||
name: 'Moonshot V1 128k',
|
||||
group: 'Moonshot V1',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
baichuan: [
|
||||
{
|
||||
id: 'Baichuan4',
|
||||
provider: 'baichuan',
|
||||
name: 'Baichuan4',
|
||||
group: 'Baichuan4',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'Baichuan3-Turbo',
|
||||
provider: 'baichuan',
|
||||
name: 'Baichuan3 Turbo',
|
||||
group: 'Baichuan3',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'Baichuan3-Turbo-128k',
|
||||
provider: 'baichuan',
|
||||
name: 'Baichuan3 Turbo 128k',
|
||||
group: 'Baichuan3',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
dashscope: [
|
||||
{
|
||||
id: 'qwen-turbo',
|
||||
provider: 'dashscope',
|
||||
name: 'Qwen Turbo',
|
||||
group: 'Qwen',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'qwen-plus',
|
||||
provider: 'dashscope',
|
||||
name: 'Qwen Plus',
|
||||
group: 'Qwen',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'qwen-max',
|
||||
provider: 'dashscope',
|
||||
name: 'Qwen Max',
|
||||
group: 'Qwen',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
aihubmix: [
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'aihubmix',
|
||||
name: 'GPT-4o Mini',
|
||||
group: 'GPT-4o',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'aihubmix-Llama-3-70B-Instruct',
|
||||
provider: 'aihubmix',
|
||||
name: 'Llama 3 70B Instruct',
|
||||
group: 'Llama3',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
openrouter: [
|
||||
@@ -280,35 +342,35 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'openrouter',
|
||||
name: 'Google: Gemma 2 9B',
|
||||
group: 'Gemma',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'microsoft/phi-3-mini-128k-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Phi-3 Mini 128K Instruct',
|
||||
group: 'Phi',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'microsoft/phi-3-medium-128k-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Phi-3 Medium 128K Instruct',
|
||||
group: 'Phi',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/llama-3-8b-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Meta: Llama 3 8B Instruct',
|
||||
group: 'Llama3',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'mistralai/mistral-7b-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Mistral: Mistral 7B Instruct',
|
||||
group: 'Mistral',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
groq: [
|
||||
@@ -317,28 +379,58 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
provider: 'groq',
|
||||
name: 'LLaMA3 8B',
|
||||
group: 'Llama3',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'llama3-70b-8192',
|
||||
provider: 'groq',
|
||||
name: 'LLaMA3 70B',
|
||||
group: 'Llama3',
|
||||
defaultEnabled: true
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'mixtral-8x7b-32768',
|
||||
provider: 'groq',
|
||||
name: 'Mixtral 8x7B',
|
||||
group: 'Mixtral',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'gemma-7b-it',
|
||||
provider: 'groq',
|
||||
name: 'Gemma 7B',
|
||||
group: 'Gemma',
|
||||
defaultEnabled: false
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
anthropic: [
|
||||
{
|
||||
id: 'claude-3-5-sonnet-20240620',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
group: 'Claude 3.5',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'claude-3-opus-20240229',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3 Opus',
|
||||
group: 'Claude 3',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'claude-3-sonnet-20240229',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3 Sonnet',
|
||||
group: 'Claude 3',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'claude-3-haiku-20240307',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3 Haiku',
|
||||
group: 'Claude 3',
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,94 @@
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
|
||||
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
|
||||
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
|
||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
|
||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
|
||||
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
||||
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
|
||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
switch (providerId) {
|
||||
case 'openai':
|
||||
return OpenAiProviderLogo
|
||||
case 'silicon':
|
||||
return SiliconFlowProviderLogo
|
||||
case 'deepseek':
|
||||
return DeepSeekProviderLogo
|
||||
case 'yi':
|
||||
return YiProviderLogo
|
||||
case 'groq':
|
||||
return GroqProviderLogo
|
||||
case 'zhipu':
|
||||
return ZhipuProviderLogo
|
||||
case 'ollama':
|
||||
return OllamaProviderLogo
|
||||
case 'moonshot':
|
||||
return MoonshotProviderLogo
|
||||
case 'openrouter':
|
||||
return OpenRouterProviderLogo
|
||||
case 'baichuan':
|
||||
return BaichuanProviderLogo
|
||||
case 'dashscope':
|
||||
return DashScopeProviderLogo
|
||||
case 'anthropic':
|
||||
return AnthropicProviderLogo
|
||||
case 'aihubmix':
|
||||
return AiHubMixProviderLogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getModelLogo(modelId: string) {
|
||||
const logoMap = {
|
||||
gpt: ChatGPTModelLogo,
|
||||
glm: ChatGLMModelLogo,
|
||||
deepseek: DeepSeekModelLogo,
|
||||
qwen: QwenModelLogo,
|
||||
gemma: GemmaModelLogo,
|
||||
'yi-': YiModelLogo,
|
||||
llama: LlamaModelLogo,
|
||||
mixtral: MixtralModelLogo,
|
||||
mistral: MixtralModelLogo,
|
||||
moonshot: MoonshotModelLogo,
|
||||
phi: MicrosoftModelLogo,
|
||||
baichuan: BaichuanModelLogo,
|
||||
claude: ClaudeModelLogo
|
||||
}
|
||||
|
||||
for (const key in logoMap) {
|
||||
if (modelId.toLowerCase().includes(key)) {
|
||||
return logoMap[key]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const PROVIDER_CONFIG = {
|
||||
openai: {
|
||||
api: {
|
||||
url: 'https://api.openai.com',
|
||||
editable: true
|
||||
},
|
||||
websites: {
|
||||
official: 'https://openai.com/',
|
||||
apiKey: 'https://platform.openai.com/api-keys',
|
||||
@@ -8,6 +97,10 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
silicon: {
|
||||
api: {
|
||||
url: 'https://cloud.siliconflow.cn',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://www.siliconflow.cn/',
|
||||
apiKey: 'https://cloud.siliconflow.cn/account/ak',
|
||||
@@ -16,6 +109,10 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
deepseek: {
|
||||
api: {
|
||||
url: 'https://api.deepseek.com',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://deepseek.com/',
|
||||
apiKey: 'https://platform.deepseek.com/api_keys',
|
||||
@@ -24,6 +121,10 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
yi: {
|
||||
api: {
|
||||
url: 'https://api.lingyiwanwu.com',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://platform.lingyiwanwu.com/',
|
||||
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
|
||||
@@ -32,6 +133,10 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
zhipu: {
|
||||
api: {
|
||||
url: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://open.bigmodel.cn/',
|
||||
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
|
||||
@@ -40,6 +145,10 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
moonshot: {
|
||||
api: {
|
||||
url: 'https://api.moonshot.cn',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://moonshot.ai/',
|
||||
apiKey: 'https://platform.moonshot.cn/console/api-keys',
|
||||
@@ -47,7 +156,35 @@ export const PROVIDER_CONFIG = {
|
||||
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
|
||||
}
|
||||
},
|
||||
baichuan: {
|
||||
api: {
|
||||
url: 'https://api.baichuan-ai.com',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://www.baichuan-ai.com/',
|
||||
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
|
||||
docs: 'https://platform.baichuan-ai.com/docs',
|
||||
models: 'https://platform.baichuan-ai.com/price'
|
||||
}
|
||||
},
|
||||
dashscope: {
|
||||
api: {
|
||||
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
editable: false
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
openrouter: {
|
||||
api: {
|
||||
url: 'https://openrouter.ai/api/v1/',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://openrouter.ai/',
|
||||
apiKey: 'https://openrouter.ai/settings/keys',
|
||||
@@ -56,6 +193,10 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
groq: {
|
||||
api: {
|
||||
url: 'https://api.groq.com/openai',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://groq.com/',
|
||||
apiKey: 'https://console.groq.com/keys',
|
||||
@@ -64,10 +205,38 @@ export const PROVIDER_CONFIG = {
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
api: {
|
||||
url: 'http://localhost:11434/v1/',
|
||||
editable: true
|
||||
},
|
||||
websites: {
|
||||
official: 'https://ollama.com/',
|
||||
docs: 'https://github.com/ollama/ollama/tree/main/docs',
|
||||
models: 'https://ollama.com/library'
|
||||
}
|
||||
},
|
||||
anthropic: {
|
||||
api: {
|
||||
url: 'https://api.anthropic.com/',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://anthropic.com/',
|
||||
apiKey: 'https://console.anthropic.com/settings/keys',
|
||||
docs: 'https://docs.anthropic.com/en/docs',
|
||||
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
|
||||
}
|
||||
},
|
||||
aihubmix: {
|
||||
api: {
|
||||
url: 'https://aihubmix.com',
|
||||
editable: false
|
||||
},
|
||||
websites: {
|
||||
official: 'https://aihubmix.com/',
|
||||
apiKey: 'https://aihubmix.com/token',
|
||||
docs: 'https://doc.aihubmix.com/',
|
||||
models: 'https://aihubmix.com/models'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/renderer/src/env.d.ts
vendored
@@ -9,5 +9,6 @@ declare global {
|
||||
message: MessageInstance
|
||||
modal: HookAPI
|
||||
keyv: KeyvStorage
|
||||
mermaid: any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useSettings } from './useSettings'
|
||||
|
||||
export function useAppInitEffect() {
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl } = useSettings()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@@ -15,4 +17,15 @@ export function useAppInitEffect() {
|
||||
})
|
||||
i18nInit()
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
proxyUrl && window.api.setProxy(proxyUrl)
|
||||
}, [proxyUrl])
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addTopic as _addTopic,
|
||||
removeAllTopics as _removeAllTopics,
|
||||
removeTopic as _removeTopic,
|
||||
setModel as _setModel,
|
||||
updateAssistants as _updateAssistants,
|
||||
updateDefaultAssistant as _updateDefaultAssistant,
|
||||
updateTopic as _updateTopic,
|
||||
updateTopics as _updateTopics,
|
||||
addAssistant,
|
||||
addTopic,
|
||||
removeAllTopics,
|
||||
removeAssistant,
|
||||
updateAssistant
|
||||
removeTopic,
|
||||
setModel,
|
||||
updateAssistant,
|
||||
updateAssistants,
|
||||
updateAssistantSettings,
|
||||
updateDefaultAssistant,
|
||||
updateTopic,
|
||||
updateTopics
|
||||
} from '@renderer/store/assistants'
|
||||
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm'
|
||||
import { Assistant, Model, Topic } from '@renderer/types'
|
||||
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
|
||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||
import localforage from 'localforage'
|
||||
|
||||
export function useAssistants() {
|
||||
@@ -23,9 +24,8 @@ export function useAssistants() {
|
||||
|
||||
return {
|
||||
assistants,
|
||||
updateAssistants: (assistants: Assistant[]) => dispatch(_updateAssistants(assistants)),
|
||||
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
|
||||
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
|
||||
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
|
||||
removeAssistant: (id: string) => {
|
||||
dispatch(removeAssistant({ id }))
|
||||
const assistant = assistants.find((a) => a.id === id)
|
||||
@@ -44,17 +44,21 @@ export function useAssistant(id: string) {
|
||||
return {
|
||||
assistant,
|
||||
model: assistant?.model ?? defaultModel,
|
||||
addTopic: (topic: Topic) => dispatch(_addTopic({ assistantId: assistant.id, topic })),
|
||||
removeTopic: (topic: Topic) => dispatch(_removeTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopic: (topic: Topic) => dispatch(_updateTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopics: (topics: Topic[]) => dispatch(_updateTopics({ assistantId: assistant.id, topics })),
|
||||
removeAllTopics: () => dispatch(_removeAllTopics({ assistantId: assistant.id })),
|
||||
setModel: (model: Model) => dispatch(_setModel({ assistantId: assistant.id, model }))
|
||||
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
||||
removeTopic: (topic: Topic) => dispatch(removeTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
||||
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
||||
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
|
||||
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
|
||||
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
|
||||
updateAssistantSettings: (settings: AssistantSettings) => {
|
||||
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefaultAssistant() {
|
||||
const { defaultAssistant } = useAppSelector((state) => state.assistants)
|
||||
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
@@ -62,18 +66,20 @@ export function useDefaultAssistant() {
|
||||
...defaultAssistant,
|
||||
topics: [getDefaultTopic()]
|
||||
},
|
||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(_updateDefaultAssistant({ assistant }))
|
||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefaultModel() {
|
||||
const { defaultModel, topicNamingModel } = useAppSelector((state) => state.llm)
|
||||
const { defaultModel, topicNamingModel, translateModel } = useAppSelector((state) => state.llm)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
defaultModel,
|
||||
topicNamingModel,
|
||||
setDefaultModel: (model: Model) => dispatch(_setDefaultModel({ model })),
|
||||
setTopicNamingModel: (model: Model) => dispatch(_setTopicNamingModel({ model }))
|
||||
translateModel,
|
||||
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
|
||||
setTopicNamingModel: (model: Model) => dispatch(setTopicNamingModel({ model })),
|
||||
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addModel as _addModel,
|
||||
removeModel as _removeModel,
|
||||
updateProvider as _updateProvider,
|
||||
updateProviders as _updateProviders
|
||||
addModel,
|
||||
addProvider,
|
||||
removeModel,
|
||||
removeProvider,
|
||||
updateProvider,
|
||||
updateProviders
|
||||
} from '@renderer/store/llm'
|
||||
import { Assistant, Model, Provider } from '@renderer/types'
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
|
||||
const selectEnabledProviders = createSelector(
|
||||
(state) => state.llm.providers,
|
||||
(providers) => providers.filter((p) => p.enabled)
|
||||
)
|
||||
|
||||
export function useProviders() {
|
||||
const providers = useAppSelector((state) => state.llm.providers.filter((p) => p.enabled))
|
||||
const providers = useAppSelector(selectEnabledProviders)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
providers,
|
||||
updateProviders: (providers: Provider[]) => dispatch(_updateProviders(providers))
|
||||
addProvider: (provider: Provider) => dispatch(addProvider(provider)),
|
||||
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
|
||||
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
|
||||
updateProviders: (providers: Provider[]) => dispatch(updateProviders(providers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +33,14 @@ export function useSystemProviders() {
|
||||
return useAppSelector((state) => state.llm.providers.filter((p) => p.isSystem))
|
||||
}
|
||||
|
||||
export function useUserProviders() {
|
||||
return useAppSelector((state) => state.llm.providers.filter((p) => !p.isSystem))
|
||||
}
|
||||
|
||||
export function useAllProviders() {
|
||||
return useAppSelector((state) => state.llm.providers)
|
||||
}
|
||||
|
||||
export function useProvider(id: string) {
|
||||
const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === id) as Provider)
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -29,9 +48,9 @@ export function useProvider(id: string) {
|
||||
return {
|
||||
provider,
|
||||
models: provider.models,
|
||||
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
|
||||
addModel: (model: Model) => dispatch(_addModel({ providerId: id, model })),
|
||||
removeModel: (model: Model) => dispatch(_removeModel({ providerId: id, model }))
|
||||
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
|
||||
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
|
||||
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setSendMessageShortcut as _setSendMessageShortcut, SendMessageShortcut } from '@renderer/store/settings'
|
||||
import {
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
SendMessageShortcut,
|
||||
setTheme,
|
||||
ThemeMode
|
||||
} from '@renderer/store/settings'
|
||||
|
||||
export function useSettings() {
|
||||
const settings = useAppSelector((state) => state.settings)
|
||||
@@ -9,6 +14,9 @@ export function useSettings() {
|
||||
...settings,
|
||||
setSendMessageShortcut(shortcut: SendMessageShortcut) {
|
||||
dispatch(_setSendMessageShortcut(shortcut))
|
||||
},
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { toggleRightSidebar } from '@renderer/store/settings'
|
||||
import { setShowRightSidebar, toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
|
||||
|
||||
export function useShowRightSidebar() {
|
||||
const showRightSidebar = useAppSelector((state) => state.settings.showRightSidebar)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
showRightSidebar,
|
||||
setShowRightSidebar: () => dispatch(toggleRightSidebar())
|
||||
rightSidebarShown: showRightSidebar,
|
||||
toggleRightSidebar: () => dispatch(toggleRightSidebar()),
|
||||
showRightSidebar: () => dispatch(setShowRightSidebar(true)),
|
||||
hideRightSidebar: () => dispatch(setShowRightSidebar(false))
|
||||
}
|
||||
}
|
||||
|
||||
export function useShowAssistants() {
|
||||
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
showAssistants,
|
||||
toggleShowAssistants: () => dispatch(toggleShowAssistants())
|
||||
}
|
||||
}
|
||||
|
||||
export function useRuntime() {
|
||||
return useAppSelector((state) => state.runtime)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { find } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
let _activeTopic: Topic
|
||||
|
||||
export function useActiveTopic(assistant: Assistant) {
|
||||
const [activeTopic, setActiveTopic] = useState(assistant?.topics[0])
|
||||
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
||||
|
||||
_activeTopic = activeTopic
|
||||
|
||||
useEffect(() => {
|
||||
// activeTopic not in assistant.topics
|
||||
|
||||
@@ -23,10 +23,13 @@ const resources = {
|
||||
duplicate: 'Duplicate',
|
||||
copy: 'Copy',
|
||||
regenerate: 'Regenerate',
|
||||
provider: 'Provider'
|
||||
provider: 'Provider',
|
||||
you: 'You',
|
||||
save: 'Save'
|
||||
},
|
||||
button: {
|
||||
add: 'Add',
|
||||
added: 'Added',
|
||||
manage: 'Manage',
|
||||
select_model: 'Select Model'
|
||||
},
|
||||
@@ -38,17 +41,20 @@ const resources = {
|
||||
'error.enter.api.key': 'Please enter your API key first',
|
||||
'error.enter.api.host': 'Please enter your API host first',
|
||||
'error.enter.model': 'Please select a model first',
|
||||
'error.invalid.proxy.url': 'Invalid proxy URL',
|
||||
'api.connection.failed': 'Connection failed',
|
||||
'api.connection.success': 'Connection successful',
|
||||
'chat.completion.paused': 'Chat completion paused'
|
||||
'chat.completion.paused': 'Chat completion paused',
|
||||
'switch.disabled': 'Switching is disabled while the assistant is generating'
|
||||
},
|
||||
chat: {
|
||||
save: 'Save'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': 'Default Assistant',
|
||||
'default.name': '😀 Default Assistant',
|
||||
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||
'default.topic.name': 'Default Topic',
|
||||
'topics.title': 'Topics',
|
||||
'topics.hide_topics': 'Hide Topics',
|
||||
'topics.show_topics': 'Show Topics',
|
||||
'topics.auto_rename': 'Auto Rename',
|
||||
'topics.edit.title': 'Rename',
|
||||
'topics.edit.placeholder': 'Enter new name',
|
||||
@@ -63,7 +69,16 @@ const resources = {
|
||||
'input.clear.content': 'Are you sure to clear all messages?',
|
||||
'input.placeholder': 'Type your message here...',
|
||||
'input.send': 'Send',
|
||||
'input.pause': 'Pause'
|
||||
'input.pause': 'Pause',
|
||||
'input.settings': 'Settings',
|
||||
'settings.temperature': 'Temperature',
|
||||
'settings.temperature.tip':
|
||||
'Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.',
|
||||
'settings.conext_count': 'Context',
|
||||
'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
|
||||
'settings.reset': 'Reset',
|
||||
'settings.set_as_default': 'Apply to default assistant',
|
||||
'settings.max': 'Max'
|
||||
},
|
||||
apps: {
|
||||
title: 'Agents'
|
||||
@@ -77,16 +92,28 @@ const resources = {
|
||||
yi: 'Yi',
|
||||
zhipu: 'ZHIPU AI',
|
||||
groq: 'Groq',
|
||||
ollama: 'Ollama'
|
||||
ollama: 'Ollama',
|
||||
baichuan: 'Baichuan',
|
||||
dashscope: 'DashScope',
|
||||
anthropic: 'Anthropic',
|
||||
aihubmix: 'AiHubMix'
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
general: 'General',
|
||||
general: 'General Settings',
|
||||
provider: 'Model Provider',
|
||||
model: 'Model Settings',
|
||||
model: 'Default Model',
|
||||
assistant: 'Default Assistant',
|
||||
about: 'About',
|
||||
about: 'About & Feedback',
|
||||
'messages.model.title': 'Model Settings',
|
||||
'messages.title': 'Message Settings',
|
||||
'messages.divider': 'Show divider between messages',
|
||||
'messages.use_serif_font': 'Use serif font',
|
||||
'messages.input.title': 'Input Settings',
|
||||
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
|
||||
'general.title': 'General Settings',
|
||||
'general.user_name': 'User Name',
|
||||
'general.user_name.placeholder': 'Enter your name',
|
||||
'provider.api_key': 'API Key',
|
||||
'provider.check': 'Check',
|
||||
'provider.get_api_key': 'Get API Key',
|
||||
@@ -94,10 +121,11 @@ const resources = {
|
||||
'provider.docs_check': 'Check',
|
||||
'provider.docs_more_details': 'for more details',
|
||||
'provider.search_placeholder': 'Search model id or name',
|
||||
'provider.api.url.reset': 'Reset',
|
||||
'models.default_assistant_model': 'Default Assistant Model',
|
||||
'models.topic_naming_model': 'Topic Naming Model',
|
||||
'models.translate_model': 'Translate Model',
|
||||
'models.add.add_model': 'Add Model',
|
||||
'models.add.provider_name.placeholder': 'Provider Name',
|
||||
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
|
||||
'models.add.model_id': 'Model ID',
|
||||
'models.add.model_id.tooltip': 'Example: gpt-3.5-turbo',
|
||||
@@ -108,7 +136,52 @@ 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'
|
||||
'assistant.model_params': 'Model Parameters',
|
||||
'about.description': 'A powerful AI assistant for producer',
|
||||
'about.updateNotAvailable': 'You are using the latest version',
|
||||
'about.checkingUpdate': 'Checking for updates...',
|
||||
'about.updateError': 'Update error',
|
||||
'about.checkUpdate': 'Check Update',
|
||||
'about.downloading': 'Downloading...',
|
||||
'provider.delete.title': 'Delete Provider',
|
||||
'provider.delete.content': 'Are you sure you want to delete this provider?',
|
||||
'provider.edit.name': 'Provider Name',
|
||||
'provider.edit.name.placeholder': 'Example: OpenAI',
|
||||
'about.title': 'About',
|
||||
'about.releases.title': '📔 Release Notes',
|
||||
'about.releases.button': 'Releases',
|
||||
'about.website.title': '🌐 Official Website',
|
||||
'about.website.button': 'Website',
|
||||
'about.feedback.title': '📝 Feedback',
|
||||
'about.feedback.button': 'Feedback',
|
||||
'about.contact.title': '📧 Contact',
|
||||
'about.contact.button': 'Email',
|
||||
'proxy.title': 'Proxy Address',
|
||||
'theme.title': 'Theme',
|
||||
'theme.dark': 'Dark',
|
||||
'theme.light': 'Light',
|
||||
'theme.auto': 'Auto'
|
||||
},
|
||||
translate: {
|
||||
title: 'Translation',
|
||||
'any.language': 'Any language',
|
||||
'button.translate': 'Translate',
|
||||
'error.not_configured': 'Translation model is not configured',
|
||||
'input.placeholder': 'Enter text to translate',
|
||||
'output.placeholder': 'Translation'
|
||||
},
|
||||
languages: {
|
||||
english: 'English',
|
||||
chinese: 'Chinese',
|
||||
'chinese-traditional': 'Traditional Chinese',
|
||||
japanese: 'Japanese',
|
||||
korean: 'Korean',
|
||||
russian: 'Russian',
|
||||
spanish: 'Spanish',
|
||||
french: 'French',
|
||||
italian: 'Italian',
|
||||
portuguese: 'Portuguese',
|
||||
arabic: 'Arabic'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -132,32 +205,37 @@ const resources = {
|
||||
duplicate: '复制',
|
||||
copy: '复制',
|
||||
regenerate: '重新生成',
|
||||
provider: '提供商'
|
||||
provider: '提供商',
|
||||
you: '用户'
|
||||
},
|
||||
button: {
|
||||
add: '添加',
|
||||
added: '已添加',
|
||||
manage: '管理',
|
||||
select_model: '选择模型'
|
||||
},
|
||||
message: {
|
||||
copied: '已复制!',
|
||||
copied: '已复制',
|
||||
'assistant.added.content': '智能体添加成功',
|
||||
'message.delete.title': '删除消息',
|
||||
'message.delete.content': '确定要删除此消息吗?',
|
||||
'error.enter.api.key': '请输入您的 API 密钥',
|
||||
'error.enter.api.host': '请输入您的 API 地址',
|
||||
'error.enter.model': '请选择一个模型',
|
||||
'error.invalid.proxy.url': '无效的代理地址',
|
||||
'api.connection.failed': '连接失败',
|
||||
'api.connection.successful': '连接成功',
|
||||
'chat.completion.paused': '会话已停止'
|
||||
'api.connection.success': '连接成功',
|
||||
'chat.completion.paused': '会话已停止',
|
||||
'switch.disabled': '模型回复完成后才能切换'
|
||||
},
|
||||
chat: {
|
||||
save: '保存'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': '默认助手',
|
||||
'default.name': '😃 默认助手 - Assistant',
|
||||
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
|
||||
'default.topic.name': '默认话题',
|
||||
'topics.title': '话题',
|
||||
'topics.hide_topics': '隐藏话题',
|
||||
'topics.show_topics': '显示话题',
|
||||
'topics.auto_rename': 'AI 重命名',
|
||||
'topics.edit.title': '重命名',
|
||||
'topics.edit.placeholder': '输入新名称',
|
||||
@@ -172,7 +250,17 @@ const resources = {
|
||||
'input.clear.content': '确定要清除所有消息吗?',
|
||||
'input.placeholder': '在这里输入消息...',
|
||||
'input.send': '发送',
|
||||
'input.pause': '暂停'
|
||||
'input.pause': '暂停',
|
||||
'input.settings': '设置',
|
||||
'settings.temperature': '模型温度',
|
||||
'settings.temperature.tip':
|
||||
'模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7',
|
||||
'settings.conext_count': '上下文数',
|
||||
'settings.conext_count.tip':
|
||||
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10,代码生成建议 5-10',
|
||||
'settings.reset': '重置',
|
||||
'settings.set_as_default': '应用到默认助手',
|
||||
'settings.max': '不限'
|
||||
},
|
||||
apps: {
|
||||
title: '智能体'
|
||||
@@ -186,16 +274,28 @@ const resources = {
|
||||
yi: '零一万物',
|
||||
zhipu: '智谱AI',
|
||||
groq: 'Groq',
|
||||
ollama: 'Ollama'
|
||||
ollama: 'Ollama',
|
||||
baichuan: '百川',
|
||||
dashscope: '阿里云灵积',
|
||||
anthropic: 'Anthropic',
|
||||
aihubmix: 'AiHubMix'
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
general: '常规',
|
||||
general: '常规设置',
|
||||
provider: '模型提供商',
|
||||
model: '模型设置',
|
||||
model: '默认模型',
|
||||
assistant: '默认助手',
|
||||
about: '关于',
|
||||
about: '关于我们',
|
||||
'messages.model.title': '模型设置',
|
||||
'messages.title': '消息设置',
|
||||
'messages.divider': '消息分割线',
|
||||
'messages.use_serif_font': '使用衬线字体',
|
||||
'messages.input.title': '输入设置',
|
||||
'messages.input.show_estimated_tokens': '状态显示',
|
||||
'general.title': '常规设置',
|
||||
'general.user_name': '用户名',
|
||||
'general.user_name.placeholder': '请输入用户名',
|
||||
'provider.api_key': 'API 密钥',
|
||||
'provider.check': '检查',
|
||||
'provider.get_api_key': '点击这里获取密钥',
|
||||
@@ -203,10 +303,11 @@ const resources = {
|
||||
'provider.docs_check': '查看',
|
||||
'provider.docs_more_details': '获取更多详情',
|
||||
'provider.search_placeholder': '搜索模型 ID 或名称',
|
||||
'provider.api.url.reset': '重置',
|
||||
'models.default_assistant_model': '默认助手模型',
|
||||
'models.topic_naming_model': '话题命名模型',
|
||||
'models.translate_model': '翻译模型',
|
||||
'models.add.add_model': '添加模型',
|
||||
'models.add.provider_name.placeholder': '必填 例如 OpenAI',
|
||||
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
|
||||
'models.add.model_id': '模型 ID',
|
||||
'models.add.model_id.tooltip': '例如 gpt-3.5-turbo',
|
||||
@@ -217,7 +318,52 @@ const resources = {
|
||||
'models.add.group_name.placeholder': '例如 ChatGPT',
|
||||
'models.empty': '没有模型',
|
||||
'assistant.title': '默认助手',
|
||||
'about.description': '一个为创造者而生的 AI 助手'
|
||||
'assistant.model_params': '模型参数',
|
||||
'about.description': '一款为创造者而生的 AI 助手',
|
||||
'about.updateNotAvailable': '你的软件已是最新版本',
|
||||
'about.checkingUpdate': '正在检查更新...',
|
||||
'about.updateError': '更新出错',
|
||||
'about.checkUpdate': '检查更新',
|
||||
'about.downloading': '正在下载更新...',
|
||||
'provider.delete.title': '删除提供商',
|
||||
'provider.delete.content': '确定要删除此模型提供商吗?',
|
||||
'provider.edit.name': '模型提供商名称',
|
||||
'provider.edit.name.placeholder': '例如 OpenAI',
|
||||
'about.title': '关于我们',
|
||||
'about.releases.title': '📔 更新日志',
|
||||
'about.releases.button': '查看',
|
||||
'about.website.title': '🌐 官方网站',
|
||||
'about.website.button': '查看',
|
||||
'about.feedback.title': '📝 意见反馈',
|
||||
'about.feedback.button': '反馈',
|
||||
'about.contact.title': '📧 邮件联系',
|
||||
'about.contact.button': '邮件',
|
||||
'proxy.title': '代理地址',
|
||||
'theme.title': '主题',
|
||||
'theme.dark': '深色主题',
|
||||
'theme.light': '浅色主题',
|
||||
'theme.auto': '跟随系统'
|
||||
},
|
||||
translate: {
|
||||
title: '翻译',
|
||||
'any.language': '任意语言',
|
||||
'button.translate': '翻译',
|
||||
'error.not_configured': '翻译模型未配置',
|
||||
'input.placeholder': '输入文本进行翻译',
|
||||
'output.placeholder': '翻译'
|
||||
},
|
||||
languages: {
|
||||
english: '英文',
|
||||
chinese: '简体中文',
|
||||
'chinese-traditional': '繁体中文',
|
||||
japanese: '日文',
|
||||
korean: '韩文',
|
||||
russian: '俄文',
|
||||
spanish: '西班牙文',
|
||||
french: '法文',
|
||||
italian: '意大利文',
|
||||
portuguese: '葡萄牙文',
|
||||
arabic: '阿拉伯文'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
import localforage from 'localforage'
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import * as Sentry from '@sentry/electron/renderer'
|
||||
import { isProduction, loadScript } from './utils'
|
||||
import { ThemeMode } from './store/settings'
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function initMermaid(theme: ThemeMode) {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
localforage.config({
|
||||
@@ -9,8 +42,11 @@ function init() {
|
||||
storeName: 'cherryai',
|
||||
description: 'Cherry Studio Storage'
|
||||
})
|
||||
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
|
||||
initSentry()
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
@@ -1,27 +1,44 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { SYSTEM_ASSISTANTS } from '@renderer/config/assistant'
|
||||
import { Button, Col, Row, Tooltip, Typography } from 'antd'
|
||||
import { Col, Row, Typography } from 'antd'
|
||||
import { find, groupBy } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { SystemAssistant } from '@renderer/types'
|
||||
import { getDefaultAssistant } from '@renderer/services/assistant'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { colorPrimary } from '@renderer/config/antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SYSTEM_ASSISTANTS from '@renderer/config/assistants.json'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const assistantGroups = groupBy(SYSTEM_ASSISTANTS, 'group')
|
||||
const assistantGroups = groupBy(
|
||||
SYSTEM_ASSISTANTS.map((a) => ({ ...a, id: String(a.id) })),
|
||||
'group'
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onAddAssistantConfirm = (assistant: SystemAssistant) => {
|
||||
const added = find(assistants, { id: assistant.id })
|
||||
|
||||
window.modal.confirm({
|
||||
title: assistant.name,
|
||||
content: assistant.description || assistant.prompt,
|
||||
icon: null,
|
||||
closable: true,
|
||||
maskClosable: true,
|
||||
okButtonProps: { type: 'primary', disabled: Boolean(added) },
|
||||
okText: added ? t('button.added') : t('button.add'),
|
||||
onOk: () => onAddAssistant(assistant)
|
||||
})
|
||||
}
|
||||
|
||||
const onAddAssistant = (assistant: SystemAssistant) => {
|
||||
addAssistant({
|
||||
...getDefaultAssistant(),
|
||||
...assistant
|
||||
...assistant,
|
||||
id: String(assistant.id)
|
||||
})
|
||||
window.message.success({
|
||||
content: t('message.assistant.added.content'),
|
||||
@@ -36,51 +53,35 @@ const AppsPage: FC = () => {
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('apps.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
{Object.keys(assistantGroups).map((group) => (
|
||||
<div key={group}>
|
||||
<Title level={3} key={group} style={{ marginBottom: 16 }}>
|
||||
{group}
|
||||
</Title>
|
||||
<Row gutter={16}>
|
||||
{assistantGroups[group].map((assistant, index) => {
|
||||
const added = find(assistants, { id: assistant.id })
|
||||
return (
|
||||
<Col span={6} key={group + index} style={{ marginBottom: 16 }}>
|
||||
<AssistantCard>
|
||||
<AssistantHeader>
|
||||
<Title level={5} style={{ marginBottom: 0, color: colorPrimary }}>
|
||||
{assistant.name}
|
||||
</Title>
|
||||
{added && (
|
||||
<Button
|
||||
type="primary"
|
||||
shape="circle"
|
||||
size="small"
|
||||
ghost
|
||||
icon={<CheckOutlined style={{ fontSize: 12 }} />}
|
||||
/>
|
||||
)}
|
||||
{!added && (
|
||||
<Tooltip placement="top" title=" Add to assistant list " arrow>
|
||||
<Button
|
||||
type="default"
|
||||
shape="circle"
|
||||
size="small"
|
||||
style={{ padding: 0 }}
|
||||
icon={<PlusOutlined style={{ fontSize: 12 }} />}
|
||||
onClick={() => onAddAssistant(assistant)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AssistantHeader>
|
||||
<AssistantCardPrompt>{assistant.prompt}</AssistantCardPrompt>
|
||||
</AssistantCard>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
<AssistantsContainer>
|
||||
{Object.keys(assistantGroups).map((group) => (
|
||||
<div key={group}>
|
||||
<Title level={3} key={group} style={{ marginBottom: 16 }}>
|
||||
{group}
|
||||
</Title>
|
||||
<Row gutter={16}>
|
||||
{assistantGroups[group].map((assistant, index) => {
|
||||
return (
|
||||
<Col span={8} key={group + index}>
|
||||
<AssistantCard onClick={() => onAddAssistantConfirm(assistant)}>
|
||||
<EmojiHeader>{assistant.emoji}</EmojiHeader>
|
||||
<Col>
|
||||
<AssistantHeader>
|
||||
<AssistantName level={5} style={{ marginBottom: 0 }}>
|
||||
{assistant.name.replace(assistant.emoji + ' ', '')}
|
||||
</AssistantName>
|
||||
</AssistantHeader>
|
||||
<AssistantCardPrompt>{assistant.prompt}</AssistantCardPrompt>
|
||||
</Col>
|
||||
</AssistantCard>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ minHeight: 20 }} />
|
||||
</AssistantsContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
@@ -96,17 +97,45 @@ const Container = styled.div`
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const AssistantCard = styled.div`
|
||||
margin-bottom: 16px;
|
||||
background-color: #141414;
|
||||
border-radius: 10px;
|
||||
const AssistantsContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
max-width: 1000px;
|
||||
`
|
||||
|
||||
const AssistantCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
const EmojiHeader = styled.div`
|
||||
width: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
font-size: 25px;
|
||||
line-height: 25px;
|
||||
`
|
||||
|
||||
const AssistantHeader = styled.div`
|
||||
@@ -116,11 +145,24 @@ const AssistantHeader = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const AssistantName = styled(Title)`
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: var(--color-white);
|
||||
font-weight: 900;
|
||||
`
|
||||
|
||||
const AssistantCardPrompt = styled.div`
|
||||
color: white;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
margin-top: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
|
||||
@@ -5,17 +5,24 @@ import styled from 'styled-components'
|
||||
import Chat from './components/Chat'
|
||||
import Assistants from './components/Assistants'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { Tooltip } from 'antd'
|
||||
import Navigation from './components/Navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import Navigation from './components/NavigationCenter'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { Switch } from 'antd'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const [activeAssistant, setActiveAssistant] = useState(assistants[0])
|
||||
const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar()
|
||||
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
|
||||
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { t } = useTranslation()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
const onCreateAssistant = () => {
|
||||
const assistant = { ...defaultAssistant, id: uuid() }
|
||||
@@ -23,32 +30,42 @@ const HomePage: FC = () => {
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
console.debug('theme', theme)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
|
||||
<NewButton onClick={onCreateAssistant}>
|
||||
<i className="iconfont icon-a-addchat"></i>
|
||||
</NewButton>
|
||||
</NavbarLeft>
|
||||
<Navigation activeAssistant={activeAssistant} />
|
||||
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
|
||||
<Tooltip
|
||||
placement="left"
|
||||
title={showRightSidebar ? t('assistant.topics.hide_topics') : t('assistant.topics.show_topics')}
|
||||
arrow>
|
||||
<NewButton onClick={setShowRightSidebar}>
|
||||
<i className={`iconfont ${showRightSidebar ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
|
||||
{showAssistants && (
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
||||
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
||||
<i className="iconfont icon-hidesidebarhoriz" />
|
||||
</NewButton>
|
||||
</Tooltip>
|
||||
<NewButton onClick={onCreateAssistant}>
|
||||
<i className="iconfont icon-a-addchat"></i>
|
||||
</NewButton>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<Navigation activeAssistant={activeAssistant} />
|
||||
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}>
|
||||
<ThemeSwitch
|
||||
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
|
||||
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
|
||||
checked={theme === 'dark'}
|
||||
onChange={toggleTheme}
|
||||
/>
|
||||
<NewButton onClick={toggleRightSidebar}>
|
||||
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
|
||||
</NewButton>
|
||||
</NavbarRight>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<Assistants
|
||||
activeAssistant={activeAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
/>
|
||||
{showAssistants && (
|
||||
<Assistants
|
||||
activeAssistant={activeAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
/>
|
||||
)}
|
||||
<Chat assistant={activeAssistant} />
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
@@ -59,33 +76,34 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const NewButton = styled.div`
|
||||
export const NewButton = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 4px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
color: var(--color-icon);
|
||||
.iconfont {
|
||||
font-size: 22px;
|
||||
.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 19px;
|
||||
}
|
||||
.icon-showsidebarhoriz,
|
||||
.icon-hidesidebarhoriz {
|
||||
font-size: 18px;
|
||||
font-size: 17px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
@@ -94,4 +112,12 @@ const NewButton = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const ThemeSwitch = styled(Switch)`
|
||||
-webkit-app-region: none;
|
||||
margin-right: 8px;
|
||||
.icon-theme {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default HomePage
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { droppableReorder, uuid } from '@renderer/utils'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
import { ItemType } from 'antd/es/menu/interface'
|
||||
import { last } from 'lodash'
|
||||
import { FC, useRef } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -18,52 +21,48 @@ interface Props {
|
||||
}
|
||||
|
||||
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
||||
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const targetAssistant = useRef<Assistant | null>(null)
|
||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onDelete = (assistant: Assistant) => {
|
||||
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
|
||||
removeAssistant(assistant.id)
|
||||
setTimeout(() => {
|
||||
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
if (targetAssistant.current) {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant: targetAssistant.current })
|
||||
const getMenuItems = (assistant: Assistant) =>
|
||||
[
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||
updateAssistant(_assistant)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.duplicate'),
|
||||
key: 'duplicate',
|
||||
icon: <CopyOutlined />,
|
||||
onClick: async () => {
|
||||
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
||||
addAssistant(_assistant)
|
||||
setActiveAssistant(_assistant)
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => onDelete(assistant)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.duplicate'),
|
||||
key: 'duplicate',
|
||||
icon: <CopyOutlined />,
|
||||
async onClick() {
|
||||
const assistant: Assistant = { ...activeAssistant, id: uuid(), topics: [getDefaultTopic()] }
|
||||
addAssistant(assistant)
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
targetAssistant.current && onDelete(targetAssistant.current)
|
||||
}
|
||||
}
|
||||
]
|
||||
] as ItemType[]
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (result.destination) {
|
||||
@@ -74,6 +73,15 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchAssistant = (assistant: Assistant) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
@@ -84,15 +92,13 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Dropdown
|
||||
key={assistant.id}
|
||||
menu={{ items }}
|
||||
trigger={['contextMenu']}
|
||||
onOpenChange={() => (targetAssistant.current = assistant)}>
|
||||
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
||||
<AssistantItem
|
||||
onClick={() => setActiveAssistant(assistant)}
|
||||
onClick={() => onSwitchAssistant(assistant)}
|
||||
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
|
||||
<AssistantName>{assistant.name}</AssistantName>
|
||||
<AssistantName className="name">
|
||||
{assistant.name || t('assistant.default.name')}
|
||||
</AssistantName>
|
||||
</AssistantItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@@ -139,13 +145,19 @@ const AssistantItem = styled.div`
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
cursor: pointer;
|
||||
.name {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const AssistantName = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: bold;
|
||||
color: var(--color-text);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default Assistants
|
||||
|
||||
@@ -4,7 +4,7 @@ import styled from 'styled-components'
|
||||
import Inputbar from './Inputbar'
|
||||
import Messages from './Messages'
|
||||
import { Flex } from 'antd'
|
||||
import Topics from './Topics'
|
||||
import RightSidebar from './RightSidebar'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
|
||||
@@ -16,17 +16,13 @@ const Chat: FC<Props> = (props) => {
|
||||
const { assistant } = useAssistant(props.assistant.id)
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
|
||||
|
||||
if (!assistant) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container id="chat">
|
||||
<Flex vertical flex={1} justify="space-between">
|
||||
<Messages assistant={assistant} topic={activeTopic} />
|
||||
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
|
||||
</Flex>
|
||||
<Topics assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import React from 'react'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import styled from 'styled-components'
|
||||
import { CopyOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { initMermaid } from '@renderer/init'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { ThemeMode } from '@renderer/store/settings'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 Mermaid from './Mermaid'
|
||||
|
||||
interface CodeBlockProps {
|
||||
children: string
|
||||
@@ -13,24 +17,34 @@ interface CodeBlockProps {
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(children)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
if (match && match[1] === 'mermaid') {
|
||||
initMermaid(theme)
|
||||
return <Mermaid chart={children} />
|
||||
}
|
||||
|
||||
return match ? (
|
||||
<div>
|
||||
<CodeHeader>
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
<CopyOutlined className="copy" onClick={onCopy} />
|
||||
{!copied && <CopyOutlined className="copy" onClick={onCopy} />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</CodeHeader>
|
||||
<SyntaxHighlighter
|
||||
{...rest}
|
||||
language={match[1]}
|
||||
style={atomDark}
|
||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||
wrapLongLines={true}
|
||||
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
@@ -47,10 +61,10 @@ const CodeHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
background-color: #323232;
|
||||
background-color: var(--color-code-background);
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
border-top-left-radius: 8px;
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { MoreOutlined } from '@ant-design/icons'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import {
|
||||
ClearOutlined,
|
||||
ControlOutlined,
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
HistoryOutlined,
|
||||
MoreOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlusCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
import SendMessageSetting from './SendMessageSetting'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import dayjs from 'dayjs'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { estimateInputTokenCount, uuid } from '@renderer/utils'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
import { debounce, isEmpty } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import SendMessageSetting from './SendMessageSetting'
|
||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -32,13 +33,12 @@ interface Props {
|
||||
|
||||
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const [text, setText] = useState('')
|
||||
const { setShowRightSidebar } = useShowRightSidebar()
|
||||
const { addTopic } = useAssistant(assistant.id)
|
||||
const { sendMessageShortcut } = useSettings()
|
||||
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const inputRef = useRef<TextAreaRef>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sendMessage = () => {
|
||||
@@ -65,6 +65,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
setText('')
|
||||
}
|
||||
|
||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
|
||||
if (event.shiftKey) {
|
||||
@@ -81,7 +83,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(() => {
|
||||
const topic: Topic = getDefaultTopic()
|
||||
const topic = getDefaultTopic()
|
||||
addTopic(topic)
|
||||
setActiveTopic(topic)
|
||||
}, [addTopic, setActiveTopic])
|
||||
@@ -99,6 +101,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
if (!generating) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||
addNewTopic()
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
@@ -108,11 +111,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
}, [addNewTopic, generating])
|
||||
|
||||
useEffect(() => {
|
||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
||||
setText(message.content)
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [])
|
||||
@@ -130,11 +135,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
<PlusCircleOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
|
||||
<ToolbarButton type="text" onClick={setShowRightSidebar}>
|
||||
<HistoryOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
|
||||
<Popconfirm
|
||||
icon={false}
|
||||
@@ -148,6 +148,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
</ToolbarButton>
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
|
||||
<HistoryOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('assistant.input.settings')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
|
||||
<ControlOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
@@ -158,7 +168,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
{generating && (
|
||||
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause}>
|
||||
<PauseCircleOutlined />
|
||||
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -177,9 +187,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
showCount
|
||||
ref={inputRef}
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
/>
|
||||
{showInputEstimatedTokens && (
|
||||
<TextCount>
|
||||
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T↑
|
||||
{`${inputTokenCount}/${estimateTokenCount}`}
|
||||
</TextCount>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -192,6 +209,7 @@ const Container = styled.div`
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
padding: 5px 15px;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
@@ -230,9 +248,22 @@ const ToolbarButton = styled(Button)`
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon {
|
||||
color: white;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TextCount = styled.div`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-3);
|
||||
z-index: 10;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 2px 8px;
|
||||
border-top-left-radius: 7px;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
|
||||
15
src/renderer/src/pages/home/components/Mermaid.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
interface Props {
|
||||
chart: string
|
||||
}
|
||||
|
||||
const Mermaid: React.FC<Props> = ({ chart }) => {
|
||||
useEffect(() => {
|
||||
window?.mermaid?.contentLoaded()
|
||||
}, [])
|
||||
|
||||
return <div className="mermaid">{chart}</div>
|
||||
}
|
||||
|
||||
export default Mermaid
|
||||
@@ -1,18 +1,28 @@
|
||||
import { Message } from '@renderer/types'
|
||||
import { Avatar, Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
MenuOutlined,
|
||||
SaveOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import Markdown from 'react-markdown'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { getModelLogo } from '@renderer/services/provider'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { SyncOutlined } from '@ant-design/icons'
|
||||
import { firstLetter } from '@renderer/utils'
|
||||
import { Message } from '@renderer/types'
|
||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty, upperFirst } from 'lodash'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isEmpty } from 'lodash'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import { useRuntime } from '@renderer/hooks/useStore'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -25,13 +35,20 @@ interface Props {
|
||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||
const avatar = useAvatar()
|
||||
const { t } = useTranslation()
|
||||
const { assistant } = useAssistant(message.assistantId)
|
||||
const { userName, showMessageDivider, messageFont } = useSettings()
|
||||
const { generating } = useRuntime()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const isLastMessage = index === 0
|
||||
const isUserMessage = message.role === 'user'
|
||||
const canRegenerate = isLastMessage && message.role === 'assistant'
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(message.content)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
@@ -61,18 +78,56 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
return message.content
|
||||
}
|
||||
|
||||
const getUserName = useCallback(() => {
|
||||
if (message.id === 'assistant') {
|
||||
return assistant?.name
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
return upperFirst(message.modelId)
|
||||
}
|
||||
|
||||
return userName || t('common.you')
|
||||
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
|
||||
|
||||
const getDropdownMenus = useCallback(
|
||||
(message: Message) => {
|
||||
return [
|
||||
{
|
||||
label: t('chat.save'),
|
||||
key: 'save',
|
||||
icon: <SaveOutlined />,
|
||||
onClick: () => {
|
||||
const fileName = message.createdAt + '.md'
|
||||
window.api.saveFile(fileName, message.content)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
[t]
|
||||
)
|
||||
|
||||
const fontFamily = messageFont === 'serif' ? "Georgia, Cambria, 'Times New Roman', Times, serif" : undefined
|
||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||
|
||||
return (
|
||||
<MessageContainer key={message.id}>
|
||||
<AvatarWrapper>
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
|
||||
{firstLetter(message.modelId).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} />
|
||||
)}
|
||||
</AvatarWrapper>
|
||||
<MessageContent>
|
||||
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}>
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : undefined} size={35}>
|
||||
{firstLetter(assistant?.name).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} />
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{removeLeadingEmoji(getUserName())}</UserName>
|
||||
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||
</UserWrap>
|
||||
</AvatarWrapper>
|
||||
</MessageHeader>
|
||||
<MessageContent style={{ fontFamily }}>
|
||||
{message.status === 'sending' && (
|
||||
<MessageContentLoading>
|
||||
<SyncOutlined spin size={24} />
|
||||
@@ -83,32 +138,44 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
{getMessageContent(message)}
|
||||
</Markdown>
|
||||
)}
|
||||
{message.usage && !generating && (
|
||||
<MessageMetadata>
|
||||
Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens}
|
||||
</MessageMetadata>
|
||||
)}
|
||||
{showMenu && (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<EditOutlined onClick={onEdit} />
|
||||
<ActionButton onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<CopyOutlined onClick={onCopy} />
|
||||
<ActionButton onClick={onCopy}>
|
||||
{!copied && <CopyOutlined />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
||||
<DeleteOutlined onClick={onDelete} />
|
||||
<ActionButton onClick={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<SyncOutlined onClick={onRegenerate} />
|
||||
<ActionButton onClick={onRegenerate}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<MessageMetadata>{message.modelId}</MessageMetadata>
|
||||
{message.usage && (
|
||||
<>
|
||||
<MessageMetadata style={{ textTransform: 'uppercase' }}>
|
||||
tokens used: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
|
||||
{message.usage.completion_tokens})
|
||||
</MessageMetadata>
|
||||
</>
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
</MenusBar>
|
||||
)}
|
||||
@@ -119,26 +186,21 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 15px;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
const MessageContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
border-bottom: 0.5px dotted var(--color-border);
|
||||
.menubar {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
&.user {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.menubar {
|
||||
@@ -147,6 +209,47 @@ const MessageContent = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const MessageHeader = styled.div`
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: 4px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const UserWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 12px;
|
||||
`
|
||||
|
||||
const UserName = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const MessageTime = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const MessageContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 46px;
|
||||
margin-top: 5px;
|
||||
`
|
||||
|
||||
const MessageContentLoading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -157,17 +260,9 @@ const MessageContentLoading = styled.div`
|
||||
const MenusBar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
.anticon {
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
font-size: 15px;
|
||||
color: var(--color-icon);
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MessageMetadata = styled.div`
|
||||
@@ -176,4 +271,24 @@ const MessageMetadata = styled.div`
|
||||
user-select: text;
|
||||
`
|
||||
|
||||
const ActionButton = styled.div`
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
.anticon {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`
|
||||
|
||||
export default MessageItem
|
||||
|
||||
@@ -7,7 +7,7 @@ import MessageItem from './Message'
|
||||
import { reverse } from 'lodash'
|
||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
||||
import { t } from 'i18next'
|
||||
@@ -22,12 +22,12 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||
const { updateTopic } = useAssistant(assistant.id)
|
||||
const provider = useProviderByAssistant(assistant)
|
||||
const messagesRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const assistantDefaultMessage: Message = {
|
||||
id: 'assistant',
|
||||
role: 'assistant',
|
||||
content: assistant.prompt || t('assistant.default.description'),
|
||||
content: assistant.description || assistant.prompt || t('assistant.default.description'),
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
status: 'pending',
|
||||
@@ -65,7 +65,6 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
||||
console.debug({ assistant, provider, message: msg, topic })
|
||||
onSendMessage(msg)
|
||||
fetchChatCompletion({ assistant, messages: [...messages, msg], topic, onResponse: setLastMessage })
|
||||
}),
|
||||
@@ -95,11 +94,15 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
}, [topic.id])
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
|
||||
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
||||
}, [assistant, messages])
|
||||
|
||||
return (
|
||||
<Container id="messages" key={assistant.id} ref={messagesRef}>
|
||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||
{lastMessage && <MessageItem message={lastMessage} />}
|
||||
{reverse([...messages]).map((message, index) => (
|
||||
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
||||
@@ -116,7 +119,10 @@ const Container = styled.div`
|
||||
flex-direction: column-reverse;
|
||||
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
|
||||
padding-top: 10px;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
.message:first-child {
|
||||
border: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default Messages
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { colorPrimary } from '@renderer/config/antd'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Button, Dropdown, MenuProps } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
}
|
||||
|
||||
const Navigation: FC<Props> = ({ activeAssistant }) => {
|
||||
const { providers } = useProviders()
|
||||
const { model, setModel } = useAssistant(activeAssistant.id)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const items: MenuProps['items'] = providers
|
||||
.filter((p) => p.models.length > 0)
|
||||
.map((p) => ({
|
||||
key: p.id,
|
||||
label: t(`provider.${p.id}`),
|
||||
type: 'group',
|
||||
children: p.models.map((m) => ({
|
||||
key: m.id,
|
||||
label: m.name,
|
||||
style: m.id === model?.id ? { color: colorPrimary } : undefined,
|
||||
onClick: () => setModel(m)
|
||||
}))
|
||||
}))
|
||||
|
||||
return (
|
||||
<NavbarCenter style={{ border: 'none', padding: '0 15px' }}>
|
||||
{activeAssistant?.name}
|
||||
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
|
||||
<Button size="small" type="primary" ghost style={{ fontSize: '11px' }}>
|
||||
{model ? model.name : t('button.select_model')}
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
</NavbarCenter>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenu = styled(Dropdown)`
|
||||
-webkit-app-region: none;
|
||||
margin-left: 10px;
|
||||
`
|
||||
|
||||
export default Navigation
|
||||
85
src/renderer/src/pages/home/components/NavigationCenter.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { CodeSandboxOutlined } from '@ant-design/icons'
|
||||
import { NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Avatar, Button, Dropdown, MenuProps } from 'antd'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { NewButton } from '../HomePage'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { removeLeadingEmoji } from '@renderer/utils'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
}
|
||||
|
||||
const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { model, setModel } = useAssistant(activeAssistant.id)
|
||||
const { providers } = useProviders()
|
||||
const { t } = useTranslation()
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
|
||||
const items: MenuProps['items'] = providers
|
||||
.filter((p) => p.models.length > 0)
|
||||
.map((p) => ({
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: p.models.map((m) => ({
|
||||
key: m.id,
|
||||
label: upperFirst(m.name),
|
||||
style: m.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
|
||||
icon: <Avatar src={getModelLogo(m.id)} size={24} />,
|
||||
onClick: () => setModel(m)
|
||||
}))
|
||||
}))
|
||||
|
||||
return (
|
||||
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
|
||||
{!showAssistants && (
|
||||
<NewButton onClick={toggleShowAssistants} style={{ marginRight: 8 }}>
|
||||
<i className="iconfont icon-showsidebarhoriz" />
|
||||
</NewButton>
|
||||
)}
|
||||
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('assistant.default.name')}</AssistantName>
|
||||
<DropdownMenu
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
|
||||
trigger={['click']}
|
||||
overlayClassName="chat-nav-dropdown">
|
||||
<DropdownButton size="small" type="primary" ghost>
|
||||
<CodeSandboxOutlined />
|
||||
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
||||
</DropdownButton>
|
||||
</DropdownMenu>
|
||||
</NavbarCenter>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenu = styled(Dropdown)`
|
||||
-webkit-app-region: none;
|
||||
margin-left: 10px;
|
||||
`
|
||||
|
||||
const AssistantName = styled.span`
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
`
|
||||
|
||||
const DropdownButton = styled(Button)`
|
||||
font-size: 11px;
|
||||
border-radius: 15px;
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
margin-left: -2px;
|
||||
font-weight: bolder;
|
||||
`
|
||||
|
||||
export default NavigationCenter
|
||||
101
src/renderer/src/pages/home/components/RightSidebar.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import TopicsTab from './TopicsTab'
|
||||
import SettingsTab from './SettingsTab'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const RightSidebar: FC<Props> = (props) => {
|
||||
const [tab, setTab] = useState<'topic' | 'settings'>('topic')
|
||||
const { rightSidebarShown, showRightSidebar, hideRightSidebar } = useShowRightSidebar()
|
||||
const { t } = useTranslation()
|
||||
const isTopicTab = tab === 'topic'
|
||||
const isSettingsTab = tab === 'settings'
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
|
||||
if (rightSidebarShown && isTopicTab) {
|
||||
return hideRightSidebar()
|
||||
}
|
||||
if (rightSidebarShown) {
|
||||
return setTab('topic')
|
||||
}
|
||||
showRightSidebar()
|
||||
setTab('topic')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
|
||||
if (rightSidebarShown && isSettingsTab) {
|
||||
return hideRightSidebar()
|
||||
}
|
||||
if (rightSidebarShown) {
|
||||
return setTab('settings')
|
||||
}
|
||||
showRightSidebar()
|
||||
setTab('settings')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab('topic'))
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [hideRightSidebar, isSettingsTab, isTopicTab, rightSidebarShown, showRightSidebar])
|
||||
|
||||
return (
|
||||
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
|
||||
<Tabs>
|
||||
<Tab className={tab === 'topic' ? 'active' : ''} onClick={() => setTab('topic')}>
|
||||
{t('common.topics')}
|
||||
</Tab>
|
||||
<Tab className={tab === 'settings' ? 'active' : ''} onClick={() => setTab('settings')}>
|
||||
{t('settings.title')}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{tab === 'topic' && <TopicsTab {...props} />}
|
||||
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: var(--topic-list-width);
|
||||
height: 100%;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
overflow-y: auto;
|
||||
.collapsed {
|
||||
width: 0;
|
||||
border-left: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Tabs = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
padding: 0 10px;
|
||||
`
|
||||
|
||||
const Tab = styled.div`
|
||||
padding: 8px 0;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
border-bottom: 1px solid transparent;
|
||||
&.active {
|
||||
color: var(--color-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
`
|
||||
|
||||
export default RightSidebar
|
||||
@@ -28,7 +28,7 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
|
||||
placement="top"
|
||||
placement="topRight"
|
||||
trigger={['click']}
|
||||
arrow>
|
||||
{children}
|
||||
|
||||
210
src/renderer/src/pages/home/components/SettingsTab.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Assistant } from '@renderer/types'
|
||||
import styled from 'styled-components'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
}
|
||||
|
||||
const SettingsTab: FC<Props> = (props) => {
|
||||
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
|
||||
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { showMessageDivider, messageFont, showInputEstimatedTokens } = useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = useCallback(
|
||||
debounce(
|
||||
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
|
||||
updateAssistantSettings({
|
||||
...assistant.settings,
|
||||
temperature: _temperature ?? temperature,
|
||||
contextCount: _contextCount ?? contextCount
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{
|
||||
leading: false,
|
||||
trailing: true
|
||||
}
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setTemperature(value)
|
||||
onUpdateAssistantSettings({ _temperature: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onConextCountChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setConextCount(value)
|
||||
onUpdateAssistantSettings({ _contextCount: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_TEMPERATURE)
|
||||
setConextCount(DEFAULT_CONEXTCOUNT)
|
||||
updateAssistant({
|
||||
...assistant,
|
||||
settings: {
|
||||
...assistant.settings,
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
contextCount: DEFAULT_CONEXTCOUNT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
}, [assistant])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SettingSubtitle style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
{t('settings.messages.model.title')}{' '}
|
||||
<Button size="small" onClick={onReset}>
|
||||
{t('assistant.settings.reset')}
|
||||
</Button>
|
||||
</SettingSubtitle>
|
||||
<SettingDivider />
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.conext_count')}</Label>
|
||||
<Tooltip title={t('assistant.settings.temperature.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={18}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1.2}
|
||||
onChange={onTemperatureChange}
|
||||
value={typeof temperature === 'number' ? temperature : 0}
|
||||
marks={{ 0: '0', 0.7: '0.7', 1.2: '1.2' }}
|
||||
step={0.1}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<InputNumberic
|
||||
min={0}
|
||||
max={1.2}
|
||||
step={0.1}
|
||||
value={temperature}
|
||||
onChange={onTemperatureChange}
|
||||
controls={false}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.conext_count')}</Label>
|
||||
<Tooltip title={t('assistant.settings.conext_count.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={18}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={20}
|
||||
marks={{ 0: '0', 10: '10', 20: t('assistant.settings.max') }}
|
||||
onChange={onConextCountChange}
|
||||
value={typeof contextCount === 'number' ? contextCount : 0}
|
||||
step={1}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<InputNumberic
|
||||
min={0}
|
||||
max={20}
|
||||
step={1}
|
||||
value={contextCount}
|
||||
onChange={onConextCountChange}
|
||||
controls={false}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showMessageDivider}
|
||||
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={messageFont === 'serif'}
|
||||
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingSubtitle style={{ marginTop: 20 }}>{t('settings.messages.input.title')}</SettingSubtitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={showInputEstimatedTokens}
|
||||
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 0 15px;
|
||||
`
|
||||
|
||||
const InputNumberic = styled(InputNumber)`
|
||||
width: 45px;
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
text-align: center;
|
||||
.ant-input-number-input {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
|
||||
const Label = styled.p`
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const QuestionIcon = styled(QuestionCircleOutlined)`
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const SettingRowTitleSmall = styled(SettingRowTitle)`
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
export default SettingsTab
|
||||
@@ -1,196 +0,0 @@
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Button, Dropdown, MenuProps, Popconfirm } from 'antd'
|
||||
import { FC, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
|
||||
const { showRightSidebar } = useShowRightSidebar()
|
||||
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
|
||||
const currentTopic = useRef<Topic | null>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const topicMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
label: t('assistant.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <SignatureOutlined />,
|
||||
async onClick() {
|
||||
if (currentTopic.current) {
|
||||
const messages = await LocalStorage.getTopicMessages(currentTopic.current.id)
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...currentTopic.current, name: summaryText })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.rename'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('assistant.topics.edit.title'),
|
||||
message: t('assistant.topics.edit.placeholder'),
|
||||
defaultValue: currentTopic.current?.name || ''
|
||||
})
|
||||
if (name && currentTopic.current && currentTopic.current?.name !== name) {
|
||||
updateTopic({ ...currentTopic.current, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (assistant.topics.length > 1) {
|
||||
topicMenuItems.push({ type: 'divider' })
|
||||
topicMenuItems.push({
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
currentTopic.current && removeTopic(currentTopic.current)
|
||||
currentTopic.current = null
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
|
||||
}
|
||||
}
|
||||
|
||||
if (!showRightSidebar) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className={showRightSidebar ? '' : 'collapsed'}>
|
||||
<TopicTitle>
|
||||
<span>
|
||||
{t('assistant.topics.title')} ({assistant.topics.length})
|
||||
</span>
|
||||
<Popconfirm
|
||||
icon={false}
|
||||
title={t('assistant.topics.delete.all.title')}
|
||||
description={t('assistant.topics.delete.all.content')}
|
||||
placement="leftBottom"
|
||||
onConfirm={removeAllTopics}
|
||||
okText="Delete All"
|
||||
okType="danger"
|
||||
cancelText="Cancel">
|
||||
<DeleteButton type="text">
|
||||
<DeleteIcon />
|
||||
</DeleteButton>
|
||||
</Popconfirm>
|
||||
</TopicTitle>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{assistant.topics.map((topic, index) => (
|
||||
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Dropdown
|
||||
menu={{ items: topicMenuItems }}
|
||||
trigger={['contextMenu']}
|
||||
key={topic.id}
|
||||
onOpenChange={(open) => open && (currentTopic.current = topic)}>
|
||||
<TopicListItem
|
||||
className={topic.id === activeTopic?.id ? 'active' : ''}
|
||||
onClick={() => setActiveTopic(topic)}>
|
||||
{topic.name}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: var(--topic-list-width);
|
||||
height: 100%;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
border-left: none;
|
||||
}
|
||||
`
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
`
|
||||
|
||||
const TopicTitle = styled.div`
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const DeleteButton = styled(Button)`
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
.anticon {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DeleteIcon = styled(DeleteOutlined)`
|
||||
font-size: 16px;
|
||||
`
|
||||
|
||||
export default Topics
|
||||
146
src/renderer/src/pages/home/components/TopicsTab.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
|
||||
const { rightSidebarShown } = useShowRightSidebar()
|
||||
const { assistant, removeTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
|
||||
const { t } = useTranslation()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const getTopicMenuItems = (topic: Topic) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('assistant.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <SignatureOutlined />,
|
||||
async onClick() {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...topic, name: summaryText })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.rename'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('assistant.topics.edit.title'),
|
||||
message: t('assistant.topics.edit.placeholder'),
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
updateTopic({ ...topic, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (assistant.topics.length > 1) {
|
||||
menus.push({ type: 'divider' })
|
||||
menus.push({
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
removeTopic(topic)
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return menus
|
||||
}
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchTopic = (topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{assistant.topics.map((topic, index) => (
|
||||
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
|
||||
<TopicListItem
|
||||
className={topic.id === activeTopic?.id ? 'active' : ''}
|
||||
onClick={() => onSwitchTopic(topic)}>
|
||||
{topic.name}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 15px 10px;
|
||||
`
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
font-weight: bolder;
|
||||
}
|
||||
`
|
||||
|
||||
export default TopicsTab
|
||||
@@ -1,14 +1,43 @@
|
||||
import { Avatar } from 'antd'
|
||||
import { Avatar, Button, Progress, Row, Tag } 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'
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
|
||||
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 = (url: string) => {
|
||||
window.api.openWebsite(url)
|
||||
}
|
||||
|
||||
const mailto = async () => {
|
||||
const email = 'kangfenmao@qq.com'
|
||||
const subject = 'Cherry Studio Feedback'
|
||||
const version = (await window.api.getAppInfo()).version
|
||||
const platform = window.electron.process.platform
|
||||
const url = `mailto:${email}?subject=${subject}&body=%0A%0AVersion: ${version} | Platform: ${platform}`
|
||||
onOpenWebsite(url)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@@ -17,38 +46,124 @@ 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 }} />
|
||||
<Title>
|
||||
Cherry Studio <Version>(v{version})</Version>
|
||||
</Title>
|
||||
<Description>{t('settings.about.description')}</Description>
|
||||
<Changelog />
|
||||
</Container>
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.about.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<AboutHeader>
|
||||
<Row align="middle">
|
||||
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
|
||||
{percent > 0 && (
|
||||
<ProgressCircle
|
||||
type="circle"
|
||||
size={84}
|
||||
percent={percent}
|
||||
showInfo={false}
|
||||
strokeLinecap="butt"
|
||||
strokeColor="#67ad5b"
|
||||
/>
|
||||
)}
|
||||
<Avatar src={Logo} size={80} style={{ minHeight: 80 }} />
|
||||
</AvatarWrapper>
|
||||
<VersionWrapper>
|
||||
<Title>Cherry Studio</Title>
|
||||
<Description>{t('settings.about.description')}</Description>
|
||||
<Tag
|
||||
onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}
|
||||
color="cyan"
|
||||
style={{ marginTop: 8, cursor: 'pointer' }}>
|
||||
v{version}
|
||||
</Tag>
|
||||
</VersionWrapper>
|
||||
</Row>
|
||||
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
|
||||
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
|
||||
</CheckUpdateButton>
|
||||
</AboutHeader>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.releases.title')}</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}>
|
||||
{t('settings.about.releases.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://easys.run/cherry-studio')}>
|
||||
{t('settings.about.website.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.feedback.title')}</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues')}>
|
||||
{t('settings.about.feedback.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
|
||||
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
const AboutHeader = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
const VersionWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const Version = styled.span`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const Description = styled.div`
|
||||
@@ -57,4 +172,18 @@ const Description = styled.div`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const CheckUpdateButton = styled(Button)``
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
`
|
||||
|
||||
const ProgressCircle = styled(Progress)`
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
`
|
||||
|
||||
export default AboutSettings
|
||||
|
||||
@@ -1,15 +1,66 @@
|
||||
import { FC } from 'react'
|
||||
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
|
||||
import { Input } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Button, Col, Input, InputNumber, Row, Slider, Tooltip } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const AssistantSettings: FC = () => {
|
||||
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()
|
||||
const [temperature, setTemperature] = useState(defaultAssistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setConextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onUpdateAssistantSettings = useCallback(
|
||||
debounce(
|
||||
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
|
||||
updateDefaultAssistant({
|
||||
...defaultAssistant,
|
||||
settings: {
|
||||
...defaultAssistant.settings,
|
||||
temperature: _temperature ?? temperature,
|
||||
contextCount: _contextCount ?? contextCount
|
||||
}
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{ leading: false, trailing: true }
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setTemperature(value)
|
||||
onUpdateAssistantSettings({ _temperature: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onConextCountChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setConextCount(value)
|
||||
onUpdateAssistantSettings({ _contextCount: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_TEMPERATURE)
|
||||
setConextCount(DEFAULT_CONEXTCOUNT)
|
||||
updateDefaultAssistant({
|
||||
...defaultAssistant,
|
||||
settings: {
|
||||
...defaultAssistant.settings,
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
contextCount: DEFAULT_CONEXTCOUNT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.assistant.title')}</SettingTitle>
|
||||
@@ -27,8 +78,82 @@ const AssistantSettings: FC = () => {
|
||||
value={defaultAssistant.prompt}
|
||||
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
|
||||
/>
|
||||
<SettingDivider />
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.assistant.model_params')}</SettingSubtitle>
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.temperature')}</Label>
|
||||
<Tooltip title={t('assistant.settings.temperature.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
|
||||
<Col span={22}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1.2}
|
||||
onChange={onTemperatureChange}
|
||||
value={typeof temperature === 'number' ? temperature : 0}
|
||||
marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }}
|
||||
step={0.1}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1.2}
|
||||
step={0.1}
|
||||
value={temperature}
|
||||
onChange={onTemperatureChange}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.conext_count')}</Label>
|
||||
<Tooltip title={t('assistant.settings.conext_count.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
|
||||
<Col span={22}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={20}
|
||||
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('assistant.settings.max') }}
|
||||
onChange={onConextCountChange}
|
||||
value={typeof contextCount === 'number' ? contextCount : 0}
|
||||
step={1}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={20}
|
||||
step={1}
|
||||
value={contextCount}
|
||||
onChange={onConextCountChange}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={onReset} style={{ width: 100 }}>
|
||||
{t('assistant.settings.reset')}
|
||||
</Button>
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const Label = styled.p`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
`
|
||||
|
||||
const QuestionIcon = styled(QuestionCircleOutlined)`
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
export default AssistantSettings
|
||||
|
||||
@@ -1,29 +1,41 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
import { Avatar, Select, Upload } from 'antd'
|
||||
import { Avatar, Input, Select, Upload } from 'antd'
|
||||
import styled from 'styled-components'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { compressImage } from '@renderer/utils'
|
||||
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { setLanguage } from '@renderer/store/settings'
|
||||
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import i18next from 'i18next'
|
||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const avatar = useAvatar()
|
||||
const { language } = useSettings()
|
||||
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onSelectLanguage = (value: string) => {
|
||||
dispatch(setLanguage(value))
|
||||
i18next.changeLanguage(value)
|
||||
i18n.changeLanguage(value)
|
||||
localStorage.setItem('language', value)
|
||||
}
|
||||
|
||||
const onSetProxyUrl = () => {
|
||||
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
|
||||
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(_setProxyUrl(proxyUrl))
|
||||
window.api.setProxy(proxyUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.general.title')}</SettingTitle>
|
||||
@@ -41,6 +53,20 @@ const GeneralSettings: FC = () => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={theme}
|
||||
style={{ width: 120 }}
|
||||
onChange={setTheme}
|
||||
options={[
|
||||
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
||||
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
|
||||
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
|
||||
<Upload
|
||||
@@ -62,6 +88,29 @@ const GeneralSettings: FC = () => {
|
||||
</Upload>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.user_name')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.general.user_name.placeholder')}
|
||||
value={userName}
|
||||
onChange={(e) => dispatch(setUserName(e.target.value))}
|
||||
style={{ width: 150 }}
|
||||
maxLength={30}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder="socks5://127.0.0.1:6153"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
onBlur={() => onSetProxyUrl()}
|
||||
type="url"
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { find } from 'lodash'
|
||||
import { Model } from '@renderer/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
|
||||
const ModelSettings: FC = () => {
|
||||
const { defaultModel, topicNamingModel, setDefaultModel, setTopicNamingModel } = useDefaultModel()
|
||||
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
|
||||
useDefaultModel()
|
||||
const { providers } = useProviders()
|
||||
const allModels = providers.map((p) => p.models).flat()
|
||||
const { t } = useTranslation()
|
||||
@@ -16,7 +18,7 @@ const ModelSettings: FC = () => {
|
||||
const selectOptions = providers
|
||||
.filter((p) => p.models.length > 0)
|
||||
.map((p) => ({
|
||||
label: t(`provider.${p.id}`),
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
title: p.name,
|
||||
options: p.models.map((m) => ({
|
||||
label: m.name,
|
||||
@@ -24,9 +26,16 @@ const ModelSettings: FC = () => {
|
||||
}))
|
||||
}))
|
||||
|
||||
const iconStyle = { fontSize: 16, marginRight: 8 }
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.models.default_assistant_model')}</SettingTitle>
|
||||
<SettingTitle>
|
||||
<div>
|
||||
<MessageOutlined style={iconStyle} />
|
||||
{t('settings.models.default_assistant_model')}
|
||||
</div>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={defaultModel.id}
|
||||
@@ -35,7 +44,12 @@ const ModelSettings: FC = () => {
|
||||
options={selectOptions}
|
||||
/>
|
||||
<div style={{ height: 30 }} />
|
||||
<SettingTitle>{t('settings.models.topic_naming_model')}</SettingTitle>
|
||||
<SettingTitle>
|
||||
<div>
|
||||
<EditOutlined style={iconStyle} />
|
||||
{t('settings.models.topic_naming_model')}
|
||||
</div>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={topicNamingModel.id}
|
||||
@@ -43,6 +57,21 @@ const ModelSettings: FC = () => {
|
||||
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
|
||||
options={selectOptions}
|
||||
/>
|
||||
<div style={{ height: 30 }} />
|
||||
<SettingTitle>
|
||||
<div>
|
||||
<TranslationOutlined style={iconStyle} />
|
||||
{t('settings.models.translate_model')}
|
||||
</div>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={translateModel?.id}
|
||||
style={{ width: 200 }}
|
||||
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
|
||||
options={selectOptions}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { useProviders, useSystemProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLogo } from '@renderer/services/provider'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLogo } from '@renderer/config/provider'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { Avatar, Tag } from 'antd'
|
||||
import { droppableReorder, generateColorFromChar, getFirstCharacter, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, Dropdown, MenuProps, Tag } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import ProviderSetting from './components/ProviderSetting'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import AddProviderPopup from './components/AddProviderPopup'
|
||||
|
||||
const ProviderSettings: FC = () => {
|
||||
const providers = useSystemProviders()
|
||||
const { updateProviders } = useProviders()
|
||||
const providers = useAllProviders()
|
||||
const { updateProviders, addProvider, removeProvider, updateProvider } = useProviders()
|
||||
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
|
||||
const { t } = useTranslation()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
setDragging(false)
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
@@ -24,37 +29,109 @@ const ProviderSettings: FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onAddProvider = async () => {
|
||||
const prividerName = await AddProviderPopup.show()
|
||||
|
||||
if (!prividerName) {
|
||||
return
|
||||
}
|
||||
|
||||
const provider = {
|
||||
id: uuid(),
|
||||
name: prividerName,
|
||||
apiKey: '',
|
||||
apiHost: '',
|
||||
models: [],
|
||||
enabled: false,
|
||||
isSystem: false
|
||||
} as Provider
|
||||
addProvider(provider)
|
||||
setSelectedProvider(provider)
|
||||
}
|
||||
|
||||
const getDropdownMenus = (provider: Provider): MenuProps['items'] => {
|
||||
return [
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await AddProviderPopup.show(provider)
|
||||
name && updateProvider({ ...provider, name })
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
async onClick() {
|
||||
window.modal.confirm({
|
||||
title: t('settings.provider.delete.title'),
|
||||
content: t('settings.provider.delete.content'),
|
||||
okButtonProps: { danger: true },
|
||||
okText: t('common.delete'),
|
||||
onOk: () => {
|
||||
setSelectedProvider(providers.filter((p) => p.isSystem)[0])
|
||||
removeProvider(provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ProviderListContainer>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{providers.map((provider, index) => (
|
||||
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<ProviderListItem
|
||||
key={JSON.stringify(provider)}
|
||||
className={provider.id === selectedProvider?.id ? 'active' : ''}
|
||||
onClick={() => setSelectedProvider(provider)}>
|
||||
<Avatar src={getProviderLogo(provider.id)} size={22} />
|
||||
<ProviderItemName>{t(`provider.${provider.id}`)}</ProviderItemName>
|
||||
{provider.enabled && (
|
||||
<Tag color="green" style={{ marginLeft: 'auto' }}>
|
||||
ON
|
||||
</Tag>
|
||||
)}
|
||||
</ProviderListItem>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<ProviderList>
|
||||
<DragDropContext onDragStart={() => setDragging(true)} onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{providers.map((provider, index) => (
|
||||
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Dropdown
|
||||
menu={{ items: provider.isSystem ? [] : getDropdownMenus(provider) }}
|
||||
trigger={['contextMenu']}>
|
||||
<ProviderListItem
|
||||
key={JSON.stringify(provider)}
|
||||
className={provider.id === selectedProvider?.id ? 'active' : ''}
|
||||
onClick={() => setSelectedProvider(provider)}>
|
||||
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={25} />}
|
||||
{!provider.isSystem && (
|
||||
<Avatar
|
||||
size={25}
|
||||
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
|
||||
{getFirstCharacter(provider.name)}
|
||||
</Avatar>
|
||||
)}
|
||||
<ProviderItemName>
|
||||
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
|
||||
</ProviderItemName>
|
||||
{provider.enabled && (
|
||||
<Tag color="green" style={{ marginLeft: 'auto' }}>
|
||||
ON
|
||||
</Tag>
|
||||
)}
|
||||
</ProviderListItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</ProviderList>
|
||||
{!dragging && (
|
||||
<AddButtonWrapper>
|
||||
<Button type="dashed" style={{ width: '100%' }} icon={<PlusOutlined />} onClick={onAddProvider} />
|
||||
</AddButtonWrapper>
|
||||
)}
|
||||
</ProviderListContainer>
|
||||
<ProviderSetting provider={selectedProvider} key={JSON.stringify(selectedProvider)} />
|
||||
</Container>
|
||||
@@ -65,22 +142,30 @@ const Container = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const ProviderListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--assistants-width);
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
const ProviderList = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const ProviderListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
padding: 5px 8px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
@@ -88,17 +173,29 @@ const ProviderListItem = styled.div`
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: #135200;
|
||||
background: var(--color-primary-mute);
|
||||
}
|
||||
&.active {
|
||||
background: #135200;
|
||||
font-weight: bold;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
font-weight: bold !important;
|
||||
}
|
||||
`
|
||||
|
||||
const ProviderItemName = styled.div`
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const AddButtonWrapper = styled.div`
|
||||
height: 50px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
`
|
||||
|
||||
export default ProviderSettings
|
||||
|
||||
@@ -68,7 +68,7 @@ const SettingMenus = styled.ul`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--assistants-width);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
`
|
||||
|
||||
@@ -84,13 +84,14 @@ const MenuItem = styled.li`
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: #135200;
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
&.active {
|
||||
background: #135200;
|
||||
font-weight: bold;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
}
|
||||
|
||||
const model: Model = {
|
||||
id: values.id,
|
||||
provider: provider.id,
|
||||
id: values.id,
|
||||
name: values.name ? values.name : values.id.toUpperCase(),
|
||||
group: getDefaultGroupName(values.group || values.id)
|
||||
}
|
||||
@@ -75,9 +75,6 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
colon={false}
|
||||
style={{ marginTop: 25 }}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item name="provider" label={t('common.provider')} initialValue={provider.id} rules={[{ required: true }]}>
|
||||
<Input placeholder={t('settings.models.add.provider_name.placeholder')} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t('settings.models.add.model_id')}
|
||||
@@ -86,13 +83,17 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
<Input
|
||||
placeholder={t('settings.models.add.model_id.placeholder')}
|
||||
spellCheck={false}
|
||||
maxLength={50}
|
||||
onChange={(e) => {
|
||||
form.setFieldValue('name', e.target.value.toUpperCase())
|
||||
form.setFieldValue('group', getDefaultGroupName(e.target.value))
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label={t('settings.models.add.model_name')} tooltip="Example: GPT-3.5">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('settings.models.add.model_name')}
|
||||
tooltip={t('settings.models.add.model_name.placeholder')}>
|
||||
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Input, Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
provider?: Provider
|
||||
resolve: (name: string) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [name, setName] = useState(provider?.name || '')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
resolve(name)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
resolve('')
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve(name)
|
||||
}
|
||||
|
||||
const buttonDisabled = name.length === 0
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
width={360}
|
||||
closable={false}
|
||||
title={t('settings.provider.edit.name')}
|
||||
okButtonProps={{ disabled: buttonDisabled }}>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value.trim())}
|
||||
placeholder={t('settings.provider.edit.name.placeholder')}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onOk()}
|
||||
maxLength={32}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class AddProviderPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
}
|
||||
static show(provider?: Provider) {
|
||||
return new Promise<string>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
<PopupContainer
|
||||
provider={provider}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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 '@renderer/assets/styles/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
|
||||
@@ -2,7 +2,7 @@ import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined }
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { fetchModels } from '@renderer/services/api'
|
||||
import { getModelLogo } from '@renderer/services/provider'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
|
||||
import { Avatar, Button, Empty, Flex, Modal, Tag } from 'antd'
|
||||
@@ -86,7 +86,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
return (
|
||||
<Flex>
|
||||
<ModelHeaderTitle>
|
||||
{t(`provider.${provider.id}`)} {t('common.models')}
|
||||
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name} {t('common.models')}
|
||||
</ModelHeaderTitle>
|
||||
{loading && <LoadingOutlined size={20} />}
|
||||
</Flex>
|
||||
@@ -178,7 +178,8 @@ const ListHeader = styled.div`
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 8px 22px;
|
||||
color: #ffffff50;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
`
|
||||
|
||||
const ListItem = styled.div`
|
||||
@@ -199,14 +200,14 @@ const ListItemHeader = styled.div`
|
||||
`
|
||||
|
||||
const ListItemName = styled.div`
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-left: 6px;
|
||||
`
|
||||
|
||||
const ModelHeaderTitle = styled.div`
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-right: 10px;
|
||||
|
||||
@@ -5,14 +5,22 @@ import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { groupBy } from 'lodash'
|
||||
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
|
||||
import { getModelLogo } from '@renderer/services/provider'
|
||||
import { CheckOutlined, EditOutlined, ExportOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import {
|
||||
CheckOutlined,
|
||||
EditOutlined,
|
||||
ExportOutlined,
|
||||
LoadingOutlined,
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons'
|
||||
import AddModelPopup from './AddModelPopup'
|
||||
import EditModelsPopup from './EditModelsPopup'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { checkApi } from '@renderer/services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
|
||||
interface Props {
|
||||
provider: Provider
|
||||
@@ -24,8 +32,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
const [apiChecking, setApiChecking] = useState(false)
|
||||
const { updateProvider, models } = useProvider(provider.id)
|
||||
const { updateProvider, models, removeModel } = useProvider(provider.id)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const modelGroups = groupBy(models, 'group')
|
||||
|
||||
@@ -37,7 +46,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const onUpdateApiKey = () => updateProvider({ ...provider, apiKey })
|
||||
const onUpdateApiHost = () => updateProvider({ ...provider, apiHost })
|
||||
const onManageModel = () => EditModelsPopup.show({ provider })
|
||||
const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add_model'), provider })
|
||||
const onAddModel = () => AddModelPopup.show({ title: t('settings.models.add.add_model'), provider })
|
||||
|
||||
const onCheckApi = async () => {
|
||||
setApiChecking(true)
|
||||
@@ -52,17 +61,27 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const apiKeyWebsite = providerConfig?.websites?.apiKey
|
||||
const docsWebsite = providerConfig?.websites?.docs
|
||||
const modelsWebsite = providerConfig?.websites?.models
|
||||
const configedApiHost = providerConfig?.api?.url
|
||||
const apiEditable = provider.isSystem ? providerConfig?.api?.editable : true
|
||||
|
||||
const apiKeyDisabled = provider.id === 'ollama'
|
||||
const onReset = () => {
|
||||
setApiHost(configedApiHost)
|
||||
updateProvider({ ...provider, apiHost: configedApiHost })
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingContainer
|
||||
style={
|
||||
theme === 'dark'
|
||||
? { backgroundColor: 'var(--color-background)' }
|
||||
: { backgroundColor: 'var(--color-background-mute)' }
|
||||
}>
|
||||
<SettingTitle>
|
||||
<Flex align="center">
|
||||
<span>{t(`provider.${provider.id}`)}</span>
|
||||
<span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span>
|
||||
{officialWebsite! && (
|
||||
<Link target="_blank" href={providerConfig.websites.official}>
|
||||
<ExportOutlined style={{ marginLeft: '8px', color: 'white', fontSize: '12px' }} />
|
||||
<ExportOutlined style={{ marginLeft: '8px', color: 'var(--color-text)', fontSize: '12px' }} />
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -75,20 +94,18 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
<Input.Password
|
||||
value={apiKey}
|
||||
placeholder={t('settings.provider.api_key')}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
onBlur={onUpdateApiKey}
|
||||
spellCheck={false}
|
||||
disabled={apiKeyDisabled}
|
||||
type="password"
|
||||
autoFocus={provider.enabled && apiKey === ''}
|
||||
/>
|
||||
{!apiKeyDisabled && (
|
||||
<Button type={apiValid ? 'primary' : 'default'} ghost={apiValid} onClick={onCheckApi}>
|
||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
|
||||
</Button>
|
||||
)}
|
||||
<Button type={apiValid ? 'primary' : 'default'} ghost={apiValid} onClick={onCheckApi}>
|
||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{apiKeyWebsite && (
|
||||
<HelpTextRow>
|
||||
@@ -98,22 +115,28 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
</HelpTextRow>
|
||||
)}
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
disabled={provider.isSystem}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
disabled={!apiEditable}
|
||||
/>
|
||||
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
|
||||
</Space.Compact>
|
||||
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
|
||||
{Object.keys(modelGroups).map((group) => (
|
||||
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
|
||||
{modelGroups[group].map((model) => (
|
||||
<ModelListItem key={model.id}>
|
||||
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
|
||||
{model.name[0].toUpperCase()}
|
||||
</Avatar>
|
||||
{model.name}
|
||||
<ModelListHeader>
|
||||
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
|
||||
{model.name[0].toUpperCase()}
|
||||
</Avatar>
|
||||
{model.name}
|
||||
</ModelListHeader>
|
||||
<RemoveIcon onClick={() => removeModel(model)} />
|
||||
</ModelListItem>
|
||||
))}
|
||||
</Card>
|
||||
@@ -148,10 +171,16 @@ const ModelListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
const ModelListHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const HelpTextRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -161,7 +190,8 @@ const HelpTextRow = styled.div`
|
||||
|
||||
const HelpText = styled.div`
|
||||
font-size: 11px;
|
||||
color: #ffffff50;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
`
|
||||
|
||||
const HelpLink = styled(Link)`
|
||||
@@ -169,4 +199,15 @@ const HelpLink = styled(Link)`
|
||||
padding: 0 5px;
|
||||
`
|
||||
|
||||
const RemoveIcon = styled(MinusCircleOutlined)`
|
||||
margin-left: 10px;
|
||||
color: var(--color-error);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 0.75;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
export default ProviderSetting
|
||||
|
||||
@@ -43,5 +43,6 @@ export const SettingRow = styled.div`
|
||||
|
||||
export const SettingRowTitle = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
306
src/renderer/src/pages/translate/TranslatePage.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
SendOutlined,
|
||||
SettingOutlined,
|
||||
SwapOutlined,
|
||||
WarningOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/api'
|
||||
import { getDefaultAssistant } from '@renderer/services/assistant'
|
||||
import { Assistant, Message } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Button, Select, Space } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
let _text = ''
|
||||
let _result = ''
|
||||
let _targetLanguage = 'english'
|
||||
|
||||
const TranslatePage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
||||
const [text, setText] = useState(_text)
|
||||
const [result, setResult] = useState(_result)
|
||||
const { translateModel } = useDefaultModel()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
_text = text
|
||||
_result = result
|
||||
_targetLanguage = targetLanguage
|
||||
|
||||
const languageOptions = [
|
||||
{
|
||||
value: 'english',
|
||||
label: t('languages.english'),
|
||||
emoji: '🇬🇧'
|
||||
},
|
||||
{
|
||||
value: 'chinese',
|
||||
label: t('languages.chinese'),
|
||||
emoji: '🇨🇳'
|
||||
},
|
||||
{
|
||||
value: 'chinese-traditional',
|
||||
label: t('languages.chinese-traditional'),
|
||||
emoji: '🇭🇰'
|
||||
},
|
||||
{
|
||||
value: 'japanese',
|
||||
label: t('languages.japanese'),
|
||||
emoji: '🇯🇵'
|
||||
},
|
||||
{
|
||||
value: 'korean',
|
||||
label: t('languages.korean'),
|
||||
emoji: '🇰🇷'
|
||||
},
|
||||
{
|
||||
value: 'russian',
|
||||
label: t('languages.russian'),
|
||||
emoji: '🇷🇺'
|
||||
},
|
||||
{
|
||||
value: 'spanish',
|
||||
label: t('languages.spanish'),
|
||||
emoji: '🇪🇸'
|
||||
},
|
||||
{
|
||||
value: 'french',
|
||||
label: t('languages.french'),
|
||||
emoji: '🇫🇷'
|
||||
},
|
||||
{
|
||||
value: 'italian',
|
||||
label: t('languages.italian'),
|
||||
emoji: '🇮🇹'
|
||||
},
|
||||
{
|
||||
value: 'portuguese',
|
||||
label: t('languages.portuguese'),
|
||||
emoji: '🇵🇹'
|
||||
},
|
||||
{
|
||||
value: 'arabic',
|
||||
label: t('languages.arabic'),
|
||||
emoji: '🇸🇦'
|
||||
}
|
||||
]
|
||||
|
||||
const onTranslate = async () => {
|
||||
if (!text.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!translateModel) {
|
||||
window.message.error({
|
||||
content: t('translate.error.not_configured'),
|
||||
key: 'translate-message'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const assistant: Assistant = getDefaultAssistant()
|
||||
assistant.model = translateModel
|
||||
assistant.prompt = `Translate from input language to ${targetLanguage}, provide the translation result directly without any explanation, keep original format. If the target language is the same as the source language, do not translate. The text to be translated is as follows:\n\n ${text}`
|
||||
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
assistantId: assistant.id,
|
||||
topicId: uuid(),
|
||||
modelId: translateModel.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'sending'
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const translateText = await fetchTranslate({ message, assistant })
|
||||
setResult(translateText)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(result)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
isEmpty(text) && setResult('')
|
||||
}, [text])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<MenuContainer>
|
||||
<Select
|
||||
showSearch
|
||||
value="any"
|
||||
style={{ width: 180 }}
|
||||
optionFilterProp="label"
|
||||
disabled
|
||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
||||
/>
|
||||
<SwapOutlined />
|
||||
<Select
|
||||
showSearch
|
||||
value={targetLanguage}
|
||||
style={{ width: 180 }}
|
||||
optionFilterProp="label"
|
||||
options={languageOptions}
|
||||
onChange={(value) => setTargetLanguage(value)}
|
||||
optionRender={(option) => (
|
||||
<Space>
|
||||
<span role="img" aria-label={option.data.label}>
|
||||
{option.data.emoji}
|
||||
</span>
|
||||
{option.label}
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
{translateModel && (
|
||||
<Link to="/settings/model" style={{ color: 'var(--color-text-2)' }}>
|
||||
<SettingOutlined />
|
||||
</Link>
|
||||
)}
|
||||
{!translateModel && (
|
||||
<Link to="/settings/model" style={{ marginLeft: -10 }}>
|
||||
<Button
|
||||
type="link"
|
||||
style={{ color: 'var(--color-error)', textDecoration: 'underline' }}
|
||||
icon={<WarningOutlined />}>
|
||||
{t('translate.error.not_configured')}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</MenuContainer>
|
||||
<TranslateInputWrapper>
|
||||
<InputContainer>
|
||||
<Textarea
|
||||
variant="borderless"
|
||||
placeholder={t('translate.input.placeholder')}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={loading}
|
||||
allowClear
|
||||
/>
|
||||
<TranslateButton
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={onTranslate}
|
||||
disabled={!text.trim()}
|
||||
icon={<SendOutlined />}>
|
||||
{t('translate.button.translate')}
|
||||
</TranslateButton>
|
||||
</InputContainer>
|
||||
<OutputContainer>
|
||||
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
|
||||
<CopyButton
|
||||
onClick={onCopy}
|
||||
disabled={!result}
|
||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyOutlined />}
|
||||
/>
|
||||
</OutputContainer>
|
||||
</TranslateInputWrapper>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const MenuContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const TranslateInputWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 350px;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
.ant-input {
|
||||
resize: none;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const OutputContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
background-color: var(--color-background-soft);
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const OutputText = styled.div`
|
||||
padding: 5px 10px;
|
||||
max-height: calc(100vh - var(--navbar-height) - 120px);
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
|
||||
const TranslateButton = styled(Button)`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 10;
|
||||
`
|
||||
|
||||
const CopyButton = styled(Button)`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
`
|
||||
|
||||
export default TranslatePage
|
||||
37
src/renderer/src/providers/AntdProvider.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { ConfigProvider, theme } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
import { useTheme } from './ThemeProvider'
|
||||
|
||||
const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { language } = useSettings()
|
||||
const { theme: _theme } = useTheme()
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={getAntdLocale(language)}
|
||||
theme={{
|
||||
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
|
||||
token: {
|
||||
colorPrimary: '#00b96b',
|
||||
borderRadius: 5
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function getAntdLocale(language: string) {
|
||||
switch (language) {
|
||||
case 'zh-CN':
|
||||
return zhCN
|
||||
case 'en-US':
|
||||
return undefined
|
||||
default:
|
||||
return zhCN
|
||||
}
|
||||
}
|
||||
|
||||
export default AntdProvider
|
||||
43
src/renderer/src/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { ThemeMode } from '@renderer/store/settings'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: ThemeMode
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>({
|
||||
theme: ThemeMode.light,
|
||||
toggleTheme: () => {}
|
||||
})
|
||||
|
||||
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { theme, setTheme } = useSettings()
|
||||
const [_theme, _setTheme] = useState(theme)
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark)
|
||||
}
|
||||
|
||||
useEffect((): any => {
|
||||
if (theme === ThemeMode.auto) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
} else {
|
||||
_setTheme(theme)
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('theme-mode', _theme)
|
||||
window.api.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
|
||||
}, [_theme])
|
||||
|
||||
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext)
|
||||
171
src/renderer/src/services/ProviderSDK.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Assistant, Message, Provider } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { getDefaultModel, getTopNamingModel } from './assistant'
|
||||
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
|
||||
import { sum, takeRight } from 'lodash'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||
import { EVENT_NAMES } from './event'
|
||||
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
|
||||
|
||||
export default class ProviderSDK {
|
||||
provider: Provider
|
||||
openaiSdk: OpenAI
|
||||
anthropicSdk: Anthropic
|
||||
|
||||
constructor(provider: Provider) {
|
||||
this.provider = provider
|
||||
const host = provider.apiHost
|
||||
const baseURL = host.endsWith('/') ? host : `${provider.apiHost}/v1/`
|
||||
this.anthropicSdk = new Anthropic({ apiKey: provider.apiKey, baseURL })
|
||||
this.openaiSdk = new OpenAI({ dangerouslyAllowBrowser: true, apiKey: provider.apiKey, baseURL })
|
||||
}
|
||||
|
||||
private get isAnthropic() {
|
||||
return this.provider.id === 'anthropic'
|
||||
}
|
||||
|
||||
public async completions(
|
||||
messages: Message[],
|
||||
assistant: Assistant,
|
||||
onChunk: ({ text, usage }: { text?: string; usage?: OpenAI.Completions.CompletionUsage }) => void
|
||||
) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount } = getAssistantSettings(assistant)
|
||||
|
||||
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
||||
|
||||
const userMessages = takeRight(messages, contextCount + 1).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
if (this.isAnthropic) {
|
||||
await this.anthropicSdk.messages
|
||||
.stream({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
|
||||
max_tokens: 4096,
|
||||
temperature: assistant?.settings?.temperature
|
||||
})
|
||||
.on('text', (text) => onChunk({ text: text || '' }))
|
||||
.on('finalMessage', (message) =>
|
||||
onChunk({
|
||||
usage: {
|
||||
prompt_tokens: message.usage.input_tokens,
|
||||
completion_tokens: message.usage.output_tokens,
|
||||
total_tokens: sum(Object.values(message.usage))
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const stream = await this.openaiSdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||
stream: true,
|
||||
temperature: assistant?.settings?.temperature
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
||||
onChunk({ text: chunk.choices[0]?.delta?.content || '', usage: chunk.usage })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async translate(message: Message, assistant: Assistant) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const messages = [
|
||||
{ role: 'system', content: assistant.prompt },
|
||||
{ role: 'user', content: message.content }
|
||||
]
|
||||
|
||||
if (this.isAnthropic) {
|
||||
const response = await this.anthropicSdk.messages.create({
|
||||
model: model.id,
|
||||
messages: messages as MessageParam[],
|
||||
max_tokens: 4096,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
stream: false
|
||||
})
|
||||
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||
} else {
|
||||
const response = await this.openaiSdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: messages as ChatCompletionMessageParam[],
|
||||
stream: false
|
||||
})
|
||||
return response.choices[0].message?.content || ''
|
||||
}
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
|
||||
const userMessages = takeRight(messages, 5).map((message) => ({
|
||||
role: 'user',
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
}
|
||||
|
||||
if (this.isAnthropic) {
|
||||
const message = await this.anthropicSdk.messages.create({
|
||||
messages: [systemMessage, ...userMessages] as Anthropic.Messages.MessageParam[],
|
||||
model: model.id,
|
||||
stream: false,
|
||||
max_tokens: 50
|
||||
})
|
||||
|
||||
return message.content[0].type === 'text' ? message.content[0].text : null
|
||||
} else {
|
||||
const response = await this.openaiSdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
max_tokens: 50
|
||||
})
|
||||
|
||||
return removeQuotes(response.choices[0].message?.content || '')
|
||||
}
|
||||
}
|
||||
|
||||
public async check(): Promise<{ valid: boolean; error: Error | null }> {
|
||||
const model = this.provider.models[0]
|
||||
const body = {
|
||||
model: model.id,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 100,
|
||||
stream: false
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.isAnthropic) {
|
||||
const message = await this.anthropicSdk.messages.create(body as MessageCreateParamsNonStreaming)
|
||||
return { valid: message.content.length > 0, error: null }
|
||||
} else {
|
||||
const response = await this.openaiSdk.chat.completions.create(body as ChatCompletionCreateParamsNonStreaming)
|
||||
return { valid: Boolean(response?.choices[0].message), error: null }
|
||||
}
|
||||
} catch (error: any) {
|
||||
return { valid: false, error }
|
||||
}
|
||||
}
|
||||
|
||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||
try {
|
||||
if (this.isAnthropic) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await this.openaiSdk.models.list()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,37 @@
|
||||
import { Assistant, Message, Provider, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { EVENT_NAMES, EventEmitter } from './event'
|
||||
import { ChatCompletionMessageParam, ChatCompletionSystemMessageParam } from 'openai/resources'
|
||||
import OpenAI from 'openai'
|
||||
import { getAssistantProvider, getDefaultModel, getProviderByModel, getTopNamingModel } from './assistant'
|
||||
import { takeRight } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { Assistant, Message, Provider, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
getAssistantProvider,
|
||||
getDefaultModel,
|
||||
getProviderByModel,
|
||||
getTopNamingModel,
|
||||
getTranslateModel
|
||||
} from './assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from './event'
|
||||
import ProviderSDK from './ProviderSDK'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
interface FetchChatCompletionParams {
|
||||
export async function fetchChatCompletion({
|
||||
messages,
|
||||
topic,
|
||||
assistant,
|
||||
onResponse
|
||||
}: {
|
||||
messages: Message[]
|
||||
topic: Topic
|
||||
assistant: Assistant
|
||||
onResponse: (message: Message) => void
|
||||
}
|
||||
|
||||
const getOpenAiProvider = (provider: Provider) => {
|
||||
const host = provider.apiHost
|
||||
return new OpenAI({
|
||||
dangerouslyAllowBrowser: true,
|
||||
apiKey: provider.apiKey,
|
||||
baseURL: host.endsWith('/') ? host : `${provider.apiHost}/v1/`
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
const model = assistant.model || defaultModel
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
|
||||
store.dispatch(setGenerating(true))
|
||||
|
||||
@@ -49,79 +48,68 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
|
||||
|
||||
onResponse({ ...message })
|
||||
|
||||
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
||||
|
||||
const userMessages = takeRight(messages, 5).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
try {
|
||||
const stream = await openaiProvider.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||
stream: true
|
||||
await providerSdk.completions(messages, assistant, ({ text, usage }) => {
|
||||
message.content = message.content + text || ''
|
||||
message.usage = usage
|
||||
onResponse({ ...message, status: 'pending' })
|
||||
})
|
||||
|
||||
let content = ''
|
||||
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' })
|
||||
}
|
||||
|
||||
message.content = content
|
||||
message.usage = usage
|
||||
} catch (error: any) {
|
||||
message.content = `Error: ${error.message}`
|
||||
}
|
||||
|
||||
const paused = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)
|
||||
message.status = paused ? 'paused' : 'success'
|
||||
// Update message status
|
||||
message.status = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED) ? 'paused' : 'success'
|
||||
|
||||
// Emit chat completion event
|
||||
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, message)
|
||||
|
||||
// Reset generating state
|
||||
store.dispatch(setGenerating(false))
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
interface FetchMessagesSummaryParams {
|
||||
messages: Message[]
|
||||
assistant: Assistant
|
||||
}
|
||||
export async function fetchTranslate({ message, assistant }: { message: Message; assistant: Assistant }) {
|
||||
const model = getTranslateModel()
|
||||
|
||||
export async function fetchMessagesSummary({ messages, assistant }: FetchMessagesSummaryParams) {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
const provider = getProviderByModel(model)
|
||||
const openaiProvider = getOpenAiProvider(provider)
|
||||
|
||||
const userMessages: ChatCompletionMessageParam[] = takeRight(messages, 5).map((message) => ({
|
||||
role: 'user',
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
const systemMessage: ChatCompletionSystemMessageParam = {
|
||||
role: 'system',
|
||||
content:
|
||||
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,回复内容不需要用引号引起来,不需要在结尾加上句号。'
|
||||
if (!model) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const response = await openaiProvider.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages],
|
||||
stream: false
|
||||
})
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
return response.choices[0].message?.content
|
||||
if (!hasApiKey(provider)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
|
||||
try {
|
||||
return await providerSdk.translate(message, assistant)
|
||||
} catch (error: any) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (!hasApiKey(provider)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
|
||||
try {
|
||||
return await providerSdk.summaries(messages, assistant)
|
||||
} catch (error: any) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkApi(provider: Provider) {
|
||||
const openaiProvider = getOpenAiProvider(provider)
|
||||
const model = provider.models[0]
|
||||
const key = 'api-check'
|
||||
const style = { marginTop: '3vh' }
|
||||
@@ -141,40 +129,31 @@ export async function checkApi(provider: Provider) {
|
||||
return false
|
||||
}
|
||||
|
||||
let valid = false
|
||||
let errorMessage = ''
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
|
||||
try {
|
||||
const response = await openaiProvider.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
max_tokens: 100,
|
||||
stream: false
|
||||
})
|
||||
|
||||
valid = Boolean(response?.choices[0].message)
|
||||
} catch (error) {
|
||||
errorMessage = (error as Error).message
|
||||
valid = false
|
||||
}
|
||||
const { valid } = await providerSdk.check()
|
||||
|
||||
window.message[valid ? 'success' : 'error']({
|
||||
key: 'api-check',
|
||||
style: { marginTop: '3vh' },
|
||||
duration: valid ? 2 : 8,
|
||||
content: valid
|
||||
? i18n.t('message.api.connection.success')
|
||||
: i18n.t('message.api.connection.failed') + ' ' + errorMessage
|
||||
content: valid ? i18n.t('message.api.connection.success') : i18n.t('message.api.connection.failed')
|
||||
})
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
function hasApiKey(provider: Provider) {
|
||||
if (!provider) return false
|
||||
if (provider.id === 'ollama') return true
|
||||
return !isEmpty(provider.apiKey)
|
||||
}
|
||||
|
||||
export async function fetchModels(provider: Provider) {
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
|
||||
try {
|
||||
const openaiProvider = getOpenAiProvider(provider)
|
||||
const response = await openaiProvider.models.list()
|
||||
return response.data
|
||||
return await providerSdk.models()
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ export function getTopNamingModel() {
|
||||
return store.getState().llm.topicNamingModel
|
||||
}
|
||||
|
||||
export function getTranslateModel() {
|
||||
return store.getState().llm.translateModel
|
||||
}
|
||||
|
||||
export function getAssistantProvider(assistant: Assistant) {
|
||||
const providers = store.getState().llm.providers
|
||||
const provider = providers.find((p) => p.id === assistant.model?.provider)
|
||||
|
||||
@@ -10,5 +10,9 @@ export const EVENT_NAMES = {
|
||||
ADD_ASSISTANT: 'ADD_ASSISTANT',
|
||||
EDIT_MESSAGE: 'EDIT_MESSAGE',
|
||||
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE',
|
||||
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED'
|
||||
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED',
|
||||
ESTIMATED_TOKEN_COUNT: 'ESTIMATED_TOKEN_COUNT',
|
||||
SHOW_CHAT_SETTINGS: 'SHOW_CHAT_SETTINGS',
|
||||
SHOW_TOPIC_SIDEBAR: 'SHOW_TOPIC_SIDEBAR',
|
||||
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR'
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
|
||||
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
||||
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.jpeg'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
|
||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
switch (providerId) {
|
||||
case 'openai':
|
||||
return OpenAiProviderLogo
|
||||
case 'silicon':
|
||||
return SiliconFlowProviderLogo
|
||||
case 'deepseek':
|
||||
return DeepSeekProviderLogo
|
||||
case 'yi':
|
||||
return YiProviderLogo
|
||||
case 'groq':
|
||||
return GroqProviderLogo
|
||||
case 'zhipu':
|
||||
return ZhipuProviderLogo
|
||||
case 'ollama':
|
||||
return OllamaProviderLogo
|
||||
case 'moonshot':
|
||||
return MoonshotProviderLogo
|
||||
case 'openrouter':
|
||||
return OpenRouterProviderLogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function getModelLogo(modelId: string) {
|
||||
const logoMap = {
|
||||
gpt: ChatGPTModelLogo,
|
||||
glm: ChatGLMModelLogo,
|
||||
deepseek: DeepSeekModelLogo,
|
||||
qwen: QwenModelLogo,
|
||||
gemma: GemmaModelLogo,
|
||||
'yi-': YiModelLogo,
|
||||
llama: LlamaModelLogo,
|
||||
mixtral: MixtralModelLogo,
|
||||
mistral: MixtralModelLogo,
|
||||
moonshot: MoonshotModelLogo,
|
||||
phi: MicrosoftModelLogo
|
||||
}
|
||||
|
||||
for (const key in logoMap) {
|
||||
if (modelId.toLowerCase().includes(key)) {
|
||||
return logoMap[key]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { Assistant, Model, Topic } from '@renderer/types'
|
||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||
import { uniqBy } from 'lodash'
|
||||
|
||||
export interface AssistantsState {
|
||||
@@ -33,6 +33,16 @@ const assistantsSlice = createSlice({
|
||||
updateAssistant: (state, action: PayloadAction<Assistant>) => {
|
||||
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
|
||||
},
|
||||
updateAssistantSettings: (state, action: PayloadAction<{ assistantId: string; settings: AssistantSettings }>) => {
|
||||
state.assistants = state.assistants.map((assistant) =>
|
||||
assistant.id === action.payload.assistantId
|
||||
? {
|
||||
...assistant,
|
||||
settings: action.payload.settings
|
||||
}
|
||||
: assistant
|
||||
)
|
||||
},
|
||||
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
|
||||
state.assistants = state.assistants.map((assistant) =>
|
||||
assistant.id === action.payload.assistantId
|
||||
@@ -111,7 +121,8 @@ export const {
|
||||
updateTopic,
|
||||
updateTopics,
|
||||
removeAllTopics,
|
||||
setModel
|
||||
setModel,
|
||||
updateAssistantSettings
|
||||
} = assistantsSlice.actions
|
||||
|
||||
export default assistantsSlice.reducer
|
||||
|
||||
@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 9,
|
||||
version: 17,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -7,18 +7,20 @@ export interface LlmState {
|
||||
providers: Provider[]
|
||||
defaultModel: Model
|
||||
topicNamingModel: Model
|
||||
translateModel: Model
|
||||
}
|
||||
|
||||
const initialState: LlmState = {
|
||||
defaultModel: SYSTEM_MODELS.openai[0],
|
||||
topicNamingModel: SYSTEM_MODELS.openai[0],
|
||||
translateModel: SYSTEM_MODELS.openai[0],
|
||||
providers: [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: SYSTEM_MODELS.openai.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.openai.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
},
|
||||
@@ -27,7 +29,7 @@ const initialState: LlmState = {
|
||||
name: 'Silicon',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.siliconflow.cn',
|
||||
models: SYSTEM_MODELS.silicon.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.silicon.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -36,7 +38,7 @@ const initialState: LlmState = {
|
||||
name: 'deepseek',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.deepseek.com',
|
||||
models: SYSTEM_MODELS.deepseek.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.deepseek.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -45,7 +47,7 @@ const initialState: LlmState = {
|
||||
name: 'Yi',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.lingyiwanwu.com',
|
||||
models: SYSTEM_MODELS.yi.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.yi.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -54,7 +56,7 @@ const initialState: LlmState = {
|
||||
name: 'ZhiPu',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -63,7 +65,43 @@ const initialState: LlmState = {
|
||||
name: 'Moonshot AI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.moonshot.cn',
|
||||
models: SYSTEM_MODELS.moonshot.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.moonshot.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'baichuan',
|
||||
name: 'BAICHUAN AI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.baichuan-ai.com',
|
||||
models: SYSTEM_MODELS.baichuan.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'dashscope',
|
||||
name: 'DashScope',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
models: SYSTEM_MODELS.dashscope.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.anthropic.com/',
|
||||
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'aihubmix',
|
||||
name: 'AiHubMix',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -72,7 +110,7 @@ const initialState: LlmState = {
|
||||
name: 'OpenRouter',
|
||||
apiKey: '',
|
||||
apiHost: 'https://openrouter.ai/api/v1/',
|
||||
models: SYSTEM_MODELS.openrouter.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.openrouter.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -81,7 +119,7 @@ const initialState: LlmState = {
|
||||
name: 'Groq',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.groq.com/openai',
|
||||
models: SYSTEM_MODELS.groq.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.groq.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -110,8 +148,8 @@ const settingsSlice = createSlice({
|
||||
addProvider: (state, action: PayloadAction<Provider>) => {
|
||||
state.providers.push(action.payload)
|
||||
},
|
||||
removeProvider: (state, action: PayloadAction<{ id: string }>) => {
|
||||
state.providers = state.providers.filter((p) => p.id !== action.payload.id && !p.isSystem)
|
||||
removeProvider: (state, action: PayloadAction<Provider>) => {
|
||||
state.providers = state.providers.filter((p) => p.id !== action.payload.id)
|
||||
},
|
||||
addModel: (state, action: PayloadAction<{ providerId: string; model: Model }>) => {
|
||||
state.providers = state.providers.map((p) =>
|
||||
@@ -138,6 +176,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||
state.topicNamingModel = action.payload.model
|
||||
},
|
||||
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||
state.translateModel = action.payload.model
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -150,7 +191,8 @@ export const {
|
||||
addModel,
|
||||
removeModel,
|
||||
setDefaultModel,
|
||||
setTopicNamingModel
|
||||
setTopicNamingModel,
|
||||
setTranslateModel
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -5,8 +5,7 @@ import { isEmpty } from 'lodash'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { Assistant } from '@renderer/types'
|
||||
|
||||
const migrate = createMigrate({
|
||||
// @ts-ignore store type is unknown
|
||||
const migrateConfig = {
|
||||
'2': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -20,13 +19,12 @@ const migrate = createMigrate({
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.lingyiwanwu.com',
|
||||
isSystem: true,
|
||||
models: SYSTEM_MODELS.yi.filter((m) => m.defaultEnabled)
|
||||
models: SYSTEM_MODELS.yi.filter((m) => m.enabled)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'3': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -40,13 +38,12 @@ const migrate = createMigrate({
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
isSystem: true,
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.defaultEnabled)
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.enabled)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'4': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -66,7 +63,6 @@ const migrate = createMigrate({
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'5': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -80,13 +76,12 @@ const migrate = createMigrate({
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.moonshot.cn',
|
||||
isSystem: true,
|
||||
models: SYSTEM_MODELS.moonshot.filter((m) => m.defaultEnabled)
|
||||
models: SYSTEM_MODELS.moonshot.filter((m) => m.enabled)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'6': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -99,14 +94,13 @@ const migrate = createMigrate({
|
||||
name: 'OpenRouter',
|
||||
apiKey: '',
|
||||
apiHost: 'https://openrouter.ai/api/v1/',
|
||||
models: SYSTEM_MODELS.openrouter.filter((m) => m.defaultEnabled),
|
||||
models: SYSTEM_MODELS.openrouter.filter((m) => m.enabled),
|
||||
isSystem: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'7': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -116,7 +110,6 @@ const migrate = createMigrate({
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'8': (state: RootState) => {
|
||||
const fixAssistantName = (assistant: Assistant) => {
|
||||
if (isEmpty(assistant.name)) {
|
||||
@@ -142,7 +135,6 @@ const migrate = createMigrate({
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore store type is unknown
|
||||
'9': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
@@ -150,13 +142,137 @@ const migrate = createMigrate({
|
||||
...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)
|
||||
provider.models = SYSTEM_MODELS.zhipu.filter((m) => m.enabled)
|
||||
}
|
||||
return provider
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
'10': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
llm: {
|
||||
...state.llm,
|
||||
providers: [
|
||||
...state.llm.providers,
|
||||
{
|
||||
id: 'baichuan',
|
||||
name: 'BAICHUAN AI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.baichuan-ai.com',
|
||||
models: SYSTEM_MODELS.baichuan.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'11': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
llm: {
|
||||
...state.llm,
|
||||
providers: [
|
||||
...state.llm.providers,
|
||||
{
|
||||
id: 'dashscope',
|
||||
name: 'DashScope',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
models: SYSTEM_MODELS.dashscope.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.anthropic.com/',
|
||||
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'12': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
llm: {
|
||||
...state.llm,
|
||||
providers: [
|
||||
...state.llm.providers,
|
||||
{
|
||||
id: 'aihubmix',
|
||||
name: 'AiHubMix',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'13': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
assistants: {
|
||||
...state.assistants,
|
||||
defaultAssistant: {
|
||||
...state.assistants.defaultAssistant,
|
||||
name: ['Default Assistant', '默认助手'].includes(state.assistants.defaultAssistant.name)
|
||||
? i18n.t(`assistant.default.name`)
|
||||
: state.assistants.defaultAssistant.name
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'14': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
showAssistants: true,
|
||||
proxyUrl: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
'15': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
userName: '',
|
||||
showMessageDivider: true
|
||||
}
|
||||
}
|
||||
},
|
||||
'16': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false
|
||||
}
|
||||
}
|
||||
},
|
||||
'17': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
theme: 'auto'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const migrate = createMigrate(migrateConfig as any)
|
||||
|
||||
export default migrate
|
||||
|
||||
@@ -2,16 +2,36 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
|
||||
|
||||
export enum ThemeMode {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
auto = 'auto'
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
showRightSidebar: boolean
|
||||
showAssistants: boolean
|
||||
sendMessageShortcut: SendMessageShortcut
|
||||
language: string
|
||||
proxyUrl?: string
|
||||
userName: string
|
||||
showMessageDivider: boolean
|
||||
messageFont: 'system' | 'serif'
|
||||
showInputEstimatedTokens: boolean
|
||||
theme: ThemeMode
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
showRightSidebar: true,
|
||||
showAssistants: true,
|
||||
sendMessageShortcut: 'Enter',
|
||||
language: navigator.language
|
||||
language: navigator.language,
|
||||
proxyUrl: undefined,
|
||||
userName: '',
|
||||
showMessageDivider: true,
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false,
|
||||
theme: ThemeMode.light
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -21,15 +41,51 @@ const settingsSlice = createSlice({
|
||||
toggleRightSidebar: (state) => {
|
||||
state.showRightSidebar = !state.showRightSidebar
|
||||
},
|
||||
setShowRightSidebar: (state, action: PayloadAction<boolean>) => {
|
||||
state.showRightSidebar = action.payload
|
||||
},
|
||||
toggleShowAssistants: (state) => {
|
||||
state.showAssistants = !state.showAssistants
|
||||
},
|
||||
setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => {
|
||||
state.sendMessageShortcut = action.payload
|
||||
},
|
||||
setLanguage: (state, action: PayloadAction<string>) => {
|
||||
state.language = action.payload
|
||||
},
|
||||
setProxyUrl: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.proxyUrl = action.payload
|
||||
},
|
||||
setUserName: (state, action: PayloadAction<string>) => {
|
||||
state.userName = action.payload
|
||||
},
|
||||
setShowMessageDivider: (state, action: PayloadAction<boolean>) => {
|
||||
state.showMessageDivider = action.payload
|
||||
},
|
||||
setMessageFont: (state, action: PayloadAction<'system' | 'serif'>) => {
|
||||
state.messageFont = action.payload
|
||||
},
|
||||
setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => {
|
||||
state.showInputEstimatedTokens = action.payload
|
||||
},
|
||||
setTheme: (state, action: PayloadAction<ThemeMode>) => {
|
||||
state.theme = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { toggleRightSidebar, setSendMessageShortcut, setLanguage } = settingsSlice.actions
|
||||
export const {
|
||||
setShowRightSidebar,
|
||||
toggleRightSidebar,
|
||||
toggleShowAssistants,
|
||||
setSendMessageShortcut,
|
||||
setLanguage,
|
||||
setProxyUrl,
|
||||
setUserName,
|
||||
setShowMessageDivider,
|
||||
setMessageFont,
|
||||
setShowInputEstimatedTokens,
|
||||
setTheme
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -3,9 +3,16 @@ import OpenAI from 'openai'
|
||||
export type Assistant = {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
prompt: string
|
||||
topics: Topic[]
|
||||
model?: Model
|
||||
settings?: AssistantSettings
|
||||
}
|
||||
|
||||
export type AssistantSettings = {
|
||||
contextCount: number
|
||||
temperature: number
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
@@ -54,6 +61,8 @@ export type Model = {
|
||||
export type SystemAssistant = {
|
||||
id: string
|
||||
name: string
|
||||
emoji: string
|
||||
description?: string
|
||||
prompt: string
|
||||
group: string
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import imageCompression from 'browser-image-compression'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Assistant, AssistantSettings, Message, Model } from '@renderer/types'
|
||||
import { GPTTokens } from 'gpt-tokens'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { takeRight } from 'lodash'
|
||||
|
||||
export const runAsyncFunction = async (fn: () => void) => {
|
||||
await fn()
|
||||
@@ -90,11 +93,135 @@ export function droppableReorder<T>(list: T[], startIndex: number, endIndex: num
|
||||
return result
|
||||
}
|
||||
|
||||
// firstLetter
|
||||
export const firstLetter = (str?: string) => {
|
||||
return str ? str[0] : ''
|
||||
export function firstLetter(str: string): string {
|
||||
const match = str.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
|
||||
return match ? match[0] : ''
|
||||
}
|
||||
|
||||
export function removeLeadingEmoji(str: string): string {
|
||||
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
|
||||
return str.replace(emojiRegex, '')
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: any) {
|
||||
if (!error) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error
|
||||
}
|
||||
|
||||
if (error?.error) {
|
||||
return getErrorMessage(error.error)
|
||||
}
|
||||
|
||||
if (error?.message) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function removeQuotes(str) {
|
||||
return str.replace(/['"]+/g, '')
|
||||
}
|
||||
|
||||
export function generateColorFromChar(char) {
|
||||
// 使用字符的Unicode值作为随机种子
|
||||
const seed = char.charCodeAt(0)
|
||||
|
||||
// 使用简单的线性同余生成器创建伪随机数
|
||||
const a = 1664525
|
||||
const c = 1013904223
|
||||
const m = Math.pow(2, 32)
|
||||
|
||||
// 生成三个伪随机数作为RGB值
|
||||
let r = (a * seed + c) % m
|
||||
let g = (a * r + c) % m
|
||||
let b = (a * g + c) % m
|
||||
|
||||
// 将伪随机数转换为0-255范围内的整数
|
||||
r = Math.floor((r / m) * 256)
|
||||
g = Math.floor((g / m) * 256)
|
||||
b = Math.floor((b / m) * 256)
|
||||
|
||||
// 返回十六进制颜色字符串
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function getFirstCharacter(str) {
|
||||
if (str.length === 0) return ''
|
||||
|
||||
// 使用 for...of 循环来获取第一个字符
|
||||
for (const char of str) {
|
||||
return char
|
||||
}
|
||||
}
|
||||
|
||||
export const getAssistantSettings = (assistant: Assistant): AssistantSettings => {
|
||||
const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
|
||||
return {
|
||||
contextCount: contextCount === 20 ? 100000 : contextCount,
|
||||
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE
|
||||
}
|
||||
}
|
||||
|
||||
export function estimateInputTokenCount(text: string) {
|
||||
const input = new GPTTokens({
|
||||
model: 'gpt-4o',
|
||||
messages: [{ role: 'user', content: text }]
|
||||
})
|
||||
|
||||
return input.usedTokens - 7
|
||||
}
|
||||
|
||||
export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[]) {
|
||||
const { contextCount } = getAssistantSettings(assistant)
|
||||
|
||||
const all = new GPTTokens({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{ role: 'system', content: assistant.prompt },
|
||||
...takeRight(msgs, contextCount).map((message) => ({ role: message.role, content: message.content }))
|
||||
]
|
||||
})
|
||||
|
||||
return all.usedTokens - 7
|
||||
}
|
||||
|
||||
/**
|
||||
* is valid proxy url
|
||||
* @param url proxy url
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isValidProxyUrl = (url: string) => {
|
||||
return url.includes('://')
|
||||
}
|
||||
|
||||
export function loadScript(url: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.src = url
|
||||
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
211
src/website/index.html
Normal file
@@ -0,0 +1,211 @@
|
||||
<!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 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
|
||||
<meta name="keywords" content="Cherry Studio AI, AI 助手, GPT 客户端, 多模型, iOS, macOS, Windows, LLM" />
|
||||
<meta name="author" content="kangfenmao" />
|
||||
<link rel="canonical" href="https://cherry-ai.com" />
|
||||
<link rel="icon" type="image/png" href="https://cherry-ai.com/logo.png" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://cherry-ai.com" />
|
||||
<meta property="og:title" content="Cherry Studio AI - 多模型 AI 助手" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Cherry Studio AI 是一款强大的多模型 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 - 多模型 AI 助手" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Cherry Studio AI 是一款强大的多模型 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 - 多模型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%;
|
||||
margin-top: -10vh;
|
||||
border-radius: 20%;
|
||||
}
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.description {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
.download-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
.loading {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body id="app">
|
||||
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">
|
||||
<img src="https://cherry-ai.com/logo.png" alt="Cherry Studio AI Logo" class="logo" />
|
||||
</a>
|
||||
<h1>Cherry Studio AI</h1>
|
||||
<p class="description">Windows/macOS GPT 客户端</p>
|
||||
<div class="download-buttons">
|
||||
<a
|
||||
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-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/v${version}/Cherry-Studio-${version}-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/v${version}/Cherry-Studio-${version}-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">
|
||||
🎉 <a href="https://github.com/kangfenmao/cherry-studio" target="_blank">Cherry Studio AI</a> 最新版本
|
||||
<a :href="`https://github.com/kangfenmao/cherry-studio/releases/tag/v${version}`" target="_blank" v-cloak
|
||||
>v{{version}}</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 AI",
|
||||
"applicationCategory": "UtilitiesApplication",
|
||||
"operatingSystem": "iOS, macOS, Windows",
|
||||
"description": "Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<script>
|
||||
const { createApp } = Vue
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
version: '0.3.2',
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true
|
||||
fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest')
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.version = data.tag_name.replace('v', '')))
|
||||
.finally(() => (this.loading = false))
|
||||
}
|
||||
}).mount('#app')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||