Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdacc56fd7 | ||
|
|
455d909c74 | ||
|
|
52d84afed6 | ||
|
|
f06d1d4d9a | ||
|
|
805a65bbaa | ||
|
|
f217950b13 | ||
|
|
9ff65441ef | ||
|
|
2b20282a41 | ||
|
|
96ad2de896 | ||
|
|
e1ea875c21 | ||
|
|
500e91977c | ||
|
|
bd194ff955 | ||
|
|
828bd71f22 | ||
|
|
5991f692b2 | ||
|
|
200d78a140 | ||
|
|
9a502b5e47 | ||
|
|
97ef3772ea | ||
|
|
eb18be200e | ||
|
|
467e97ff4b | ||
|
|
27b802d3c2 | ||
|
|
37b0a175f7 | ||
|
|
b2b79f12a2 | ||
|
|
885c578582 | ||
|
|
e61e4b109a | ||
|
|
f3bafbeb52 | ||
|
|
e55c0cdcef | ||
|
|
e73bbf4d6a | ||
|
|
3859289218 | ||
|
|
591bb45a4e | ||
|
|
b31f518fca | ||
|
|
dfbdb989db | ||
|
|
f194ebbc20 | ||
|
|
ab0e7e1e07 | ||
|
|
d809f50c0e | ||
|
|
a48d24de26 | ||
|
|
0dacc20e74 | ||
|
|
08df6cb4f8 | ||
|
|
0676ac8942 | ||
|
|
c257e8f0fe | ||
|
|
521670f683 | ||
|
|
87216b5d91 | ||
|
|
e6122a3d36 | ||
|
|
e6e1502308 | ||
|
|
7f5be3a688 | ||
|
|
4dde49a9f0 | ||
|
|
ce830b692b | ||
|
|
563472f3a9 | ||
|
|
14acd45927 | ||
|
|
9e2c7a08df | ||
|
|
f10c8dc379 | ||
|
|
fdd815879a | ||
|
|
635f238576 | ||
|
|
615e337e3f | ||
|
|
acd5d4b192 | ||
|
|
9a41b697c6 | ||
|
|
5cb67e00a6 | ||
|
|
350f13e97c | ||
|
|
4d6cbf5073 | ||
|
|
8d7b10d21e | ||
|
|
6753a93c0d | ||
|
|
9ee763337d | ||
|
|
ace0cb7823 | ||
|
|
44e518ef03 | ||
|
|
e28b96b45e | ||
|
|
11427a980c | ||
|
|
cb95562e58 | ||
|
|
89bdab58f7 | ||
|
|
d42ee59335 | ||
|
|
88e7ab211d | ||
|
|
5347bdfa83 | ||
|
|
c8711c5804 | ||
|
|
24cf3bb043 | ||
|
|
0531ecf3cf | ||
|
|
0cbfd26883 | ||
|
|
ee398489de | ||
|
|
71d7c2c738 | ||
|
|
b98f7298a2 | ||
|
|
de4f2599be | ||
|
|
93b32e8e21 | ||
|
|
e353d0f8ee | ||
|
|
dfd42fe9a6 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,3 +45,6 @@ out
|
||||
# ENV
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Local
|
||||
local
|
||||
|
||||
53
.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch
Normal file
53
.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch
Normal file
@@ -0,0 +1,53 @@
|
||||
diff --git a/lib/check-signature.js b/lib/check-signature.js
|
||||
index 324568af71bcc4372c9f959131ecd24122848c86..677348e0a138ff608b2ac41f592d813b15ee4956 100644
|
||||
--- a/lib/check-signature.js
|
||||
+++ b/lib/check-signature.js
|
||||
@@ -41,16 +41,12 @@ const spawn_1 = require("./spawn");
|
||||
const debug_1 = __importDefault(require("debug"));
|
||||
const d = (0, debug_1.default)('electron-notarize');
|
||||
const codesignDisplay = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
|
||||
return result;
|
||||
});
|
||||
const codesign = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
d('attempting to check codesign of app:', opts.appPath);
|
||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
|
||||
return result;
|
||||
});
|
||||
function checkSignatures(opts) {
|
||||
diff --git a/lib/notarytool.js b/lib/notarytool.js
|
||||
index 1ab090efb2101fc8bee5553445e0349c54474421..a5ddfd922197449fc56078e4a7e9a2ee5d8d207d 100644
|
||||
--- a/lib/notarytool.js
|
||||
+++ b/lib/notarytool.js
|
||||
@@ -92,9 +92,7 @@ function notarizeAndWaitForNotaryTool(opts) {
|
||||
else {
|
||||
filePath = path.resolve(dir, `${path.parse(opts.appPath).name}.zip`);
|
||||
d('zipping application to:', filePath);
|
||||
- const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), filePath], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', opts.appPath, filePath]);
|
||||
if (zipResult.code !== 0) {
|
||||
throw new Error(`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`);
|
||||
}
|
||||
diff --git a/lib/staple.js b/lib/staple.js
|
||||
index 47dbd85b2fc279d999b57f47fb8171e1cc674436..f8829e6ac54fcd630a730d12d75acc1591b953b6 100644
|
||||
--- a/lib/staple.js
|
||||
+++ b/lib/staple.js
|
||||
@@ -43,9 +43,7 @@ const d = (0, debug_1.default)('electron-notarize:staple');
|
||||
function stapleApp(opts) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
d('attempting to staple app:', opts.appPath);
|
||||
- const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', opts.appPath]);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`Failed to staple your application with code: ${result.code}\n\n${result.output}`);
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
nodeLinker: node-modules
|
||||
enableImmutableInstalls: false
|
||||
|
||||
httpTimeout: 300000
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
@@ -3,12 +3,13 @@ productName: Cherry Studio
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!{.vscode,.yarn,.github}'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
- '!src/*'
|
||||
- '!local'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
@@ -56,5 +57,11 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
增加应用备份和恢复功能
|
||||
增加更多AI小程序
|
||||
本次更新:
|
||||
增加知乎直问小程序
|
||||
支持 DeepSeek V2.5
|
||||
各种弹窗居中显示
|
||||
近期更新:
|
||||
智能助理和消息列表合并
|
||||
优化输入框样式
|
||||
提升小程序稳定性
|
||||
|
||||
@@ -4,7 +4,12 @@ import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
ollama: resolve('ollama/src')
|
||||
}
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
|
||||
22
package.json
22
package.json
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.6.1",
|
||||
"name": "CherryStudio",
|
||||
"version": "0.6.12",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"local"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
@@ -25,12 +31,10 @@
|
||||
"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",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1"
|
||||
"electron-window-state": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.3",
|
||||
@@ -51,15 +55,15 @@
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"electron": "^28.2.0",
|
||||
"electron": "^28.3.3",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-vite": "^2.0.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"gpt-tokens": "^1.3.6",
|
||||
"i18next": "^23.11.5",
|
||||
@@ -76,6 +80,7 @@
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
@@ -91,7 +96,8 @@
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@electron/notarize": "2.3.2"
|
||||
"@electron/notarize": "2.3.2",
|
||||
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import * as Sentry from '@sentry/electron/main'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
import { createMainWindow } from './window'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
|
||||
|
||||
@@ -29,8 +30,6 @@ app.whenReady().then(() => {
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
@@ -44,6 +43,3 @@ 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'
|
||||
})
|
||||
|
||||
@@ -50,7 +50,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
ipcMain.handle('check-for-update', async () => {
|
||||
autoUpdater.logger?.info('触发检查更新')
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
|
||||
@@ -17,11 +17,6 @@ export default class AppUpdater {
|
||||
mainWindow.webContents.send('update-error', error)
|
||||
})
|
||||
|
||||
// 检测是否需要更新
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
logger.info('正在检查更新……')
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||
mainWindow.webContents.send('update-available', releaseInfo)
|
||||
@@ -59,7 +54,6 @@ export default class AppUpdater {
|
||||
|
||||
// 检测到不需要更新时
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
logger.info('现在使用的就是最新版本,不用更新')
|
||||
mainWindow.webContents.send('update-not-available')
|
||||
})
|
||||
|
||||
|
||||
77
src/main/utils/upgrade.ts
Normal file
77
src/main/utils/upgrade.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { spawn } from 'child_process'
|
||||
import { app, dialog } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export async function updateUserDataPath() {
|
||||
const currentPath = app.getPath('userData')
|
||||
const oldPath = currentPath.replace('CherryStudio', 'cherry-studio')
|
||||
|
||||
if (currentPath !== oldPath && fs.existsSync(oldPath)) {
|
||||
Logger.log('Update userData path')
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 系统:创建 bat 文件
|
||||
const batPath = await createWindowsBatFile(oldPath, currentPath)
|
||||
await promptRestartAndExecute(batPath)
|
||||
} else {
|
||||
// 其他系统:直接更新
|
||||
fs.rmSync(currentPath, { recursive: true, force: true })
|
||||
fs.renameSync(oldPath, currentPath)
|
||||
Logger.log(`Directory renamed: ${currentPath}`)
|
||||
await promptRestart()
|
||||
}
|
||||
} catch (error: any) {
|
||||
Logger.error('Error updating userData path:', error)
|
||||
dialog.showErrorBox('错误', `更新用户数据目录时发生错误: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
Logger.log('userData path does not need to be updated')
|
||||
}
|
||||
}
|
||||
|
||||
async function createWindowsBatFile(oldPath: string, currentPath: string): Promise<string> {
|
||||
const batPath = path.join(app.getPath('temp'), 'rename_userdata.bat')
|
||||
const appPath = app.getPath('exe')
|
||||
const batContent = `
|
||||
@echo off
|
||||
timeout /t 2 /nobreak
|
||||
rmdir /s /q "${currentPath}"
|
||||
rename "${oldPath}" "${path.basename(currentPath)}"
|
||||
start "" "${appPath}"
|
||||
del "%~f0"
|
||||
`
|
||||
fs.writeFileSync(batPath, batContent)
|
||||
return batPath
|
||||
}
|
||||
|
||||
async function promptRestartAndExecute(batPath: string) {
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '应用需要重启',
|
||||
message: '用户数据目录将在重启后更新。请重启应用以应用更改。',
|
||||
buttons: ['手动重启']
|
||||
})
|
||||
|
||||
// 执行 bat 文件
|
||||
spawn('cmd.exe', ['/c', batPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
|
||||
app.exit(0)
|
||||
}
|
||||
|
||||
async function promptRestart() {
|
||||
await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '应用需要重启',
|
||||
message: '用户数据目录已更新。请重启应用以应用更改。',
|
||||
buttons: ['重启']
|
||||
})
|
||||
|
||||
app.relaunch()
|
||||
app.exit(0)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import util from 'node:util'
|
||||
import zlib from 'node:zlib'
|
||||
|
||||
import logger from 'electron-log'
|
||||
|
||||
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
|
||||
const gzipPromise = util.promisify(zlib.gzip)
|
||||
const gunzipPromise = util.promisify(zlib.gunzip)
|
||||
@@ -16,7 +18,7 @@ export async function compress(str) {
|
||||
const compressedBuffer = await gzipPromise(buffer)
|
||||
return compressedBuffer
|
||||
} catch (error) {
|
||||
console.error('Compression failed:', error)
|
||||
logger.error('Compression failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -31,7 +33,7 @@ export async function decompress(compressedBuffer) {
|
||||
const buffer = await gunzipPromise(compressedBuffer)
|
||||
return buffer.toString('utf-8')
|
||||
} catch (error) {
|
||||
console.error('Decompression failed:', error)
|
||||
logger.error('Decompression failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ export function createMainWindow() {
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
// devTools: !app.isPackaged,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:; frame-src * file:" />
|
||||
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: *; frame-src * file:" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -5,13 +5,13 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
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 (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'iconfont'; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1724204739157') format('woff2');
|
||||
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -11,6 +11,30 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: '\e6cd';
|
||||
}
|
||||
|
||||
.icon-ai-model:before {
|
||||
content: '\e827';
|
||||
}
|
||||
|
||||
.icon-ai-model1:before {
|
||||
content: '\ec09';
|
||||
}
|
||||
|
||||
.icon-gridlines:before {
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-grid-row-2copy:before {
|
||||
content: '\e681';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: '\e869';
|
||||
}
|
||||
|
||||
.icon-business-smart-assistant:before {
|
||||
content: '\e601';
|
||||
}
|
||||
@@ -39,11 +63,11 @@
|
||||
content: '\e758';
|
||||
}
|
||||
|
||||
.icon-hidesidebarhoriz:before {
|
||||
.icon-hide-sidebar:before {
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-showsidebarhoriz:before {
|
||||
.icon-show-sidebar:before {
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
BIN
src/renderer/src/assets/images/apps/perplexity.webp
Normal file
BIN
src/renderer/src/assets/images/apps/perplexity.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/apps/zhihu.png
Normal file
BIN
src/renderer/src/assets/images/apps/zhihu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/avatar.png
Normal file
BIN
src/renderer/src/assets/images/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/avatar.webp
Normal file
BIN
src/renderer/src/assets/images/avatar.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
src/renderer/src/assets/images/models/chatglm.png
Normal file
BIN
src/renderer/src/assets/images/models/chatglm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -36,8 +36,8 @@
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #323232;
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
|
||||
|
||||
--navbar-background-mac: rgba(30, 30, 30, 0.8);
|
||||
--navbar-background: rgba(30, 30, 30);
|
||||
@@ -48,8 +48,8 @@
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 85px;
|
||||
|
||||
--assistants-width: 245px;
|
||||
--topic-list-width: 260px;
|
||||
--assistants-width: 280px;
|
||||
--topic-list-width: 280px;
|
||||
--settings-width: var(--assistants-width);
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ body[theme-mode='light'] {
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
|
||||
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.75);
|
||||
--navbar-background: rgba(255, 255, 255);
|
||||
@@ -204,3 +204,27 @@ body,
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-header {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.segmented-tab {
|
||||
.ant-segmented-item-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 13px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.anticon-setting {
|
||||
font-size: 12px;
|
||||
}
|
||||
.ant-segmented-item-icon + * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
pre {
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
&::marker {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
@@ -98,7 +101,8 @@
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
p code {
|
||||
p code,
|
||||
li code {
|
||||
background: var(--color-background-mute);
|
||||
padding: 3px 5px;
|
||||
border-radius: 5px;
|
||||
@@ -106,19 +110,25 @@
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
padding: 1em 0;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||
background-color: var(--color-background-mute);
|
||||
&:not(pre pre) {
|
||||
> code:not(pre pre > code) {
|
||||
padding: 15px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
margin: 0 !important;
|
||||
}
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* 全局初始化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
OnDragStartResponder,
|
||||
ResponderProvided
|
||||
} from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props<T> {
|
||||
list: T[]
|
||||
style?: React.CSSProperties
|
||||
listStyle?: React.CSSProperties
|
||||
children: (item: T, index: number) => React.ReactNode
|
||||
onUpdate: (list: T[]) => void
|
||||
onDragStart?: () => void
|
||||
onDragEnd?: () => void
|
||||
onDragStart?: OnDragStartResponder
|
||||
onDragEnd?: OnDragEndResponder
|
||||
}
|
||||
|
||||
const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => {
|
||||
const _onDragEnd = (result: DropResult) => {
|
||||
onDragEnd?.()
|
||||
const DragableList: FC<Props<any>> = ({ children, list, style, listStyle, onDragStart, onUpdate, onDragEnd }) => {
|
||||
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided)
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
@@ -25,7 +35,7 @@ const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, o
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
||||
{list.map((item, index) => (
|
||||
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index}>
|
||||
{(provided) => (
|
||||
@@ -33,7 +43,7 @@ const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, o
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 8 }}>
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { FC, useEffect, useRef } from 'react'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
@@ -5,7 +6,8 @@ import store from '@renderer/store'
|
||||
import { setMinappShow } from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { Drawer } from 'antd'
|
||||
import { useRef, useState } from 'react'
|
||||
import { WebviewTag } from 'electron'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { TopView } from '../TopView'
|
||||
@@ -17,7 +19,7 @@ interface Props {
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const webviewRef = useRef<WebviewTag | null>(null)
|
||||
|
||||
useBridge()
|
||||
|
||||
@@ -29,8 +31,8 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
}
|
||||
|
||||
const onReload = () => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = app.url
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.src = app.url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +61,27 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const webview = webviewRef.current
|
||||
|
||||
if (webview) {
|
||||
const handleNewWindow = (event: any) => {
|
||||
event.preventDefault()
|
||||
if (webview.loadURL) {
|
||||
webview.loadURL(event.url)
|
||||
}
|
||||
}
|
||||
|
||||
webview.addEventListener('new-window', handleNewWindow)
|
||||
|
||||
return () => {
|
||||
webview.removeEventListener('new-window', handleNewWindow)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={<Title />}
|
||||
@@ -72,17 +95,17 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
maskClosable={false}
|
||||
closeIcon={null}
|
||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||
<Frame src={app.url} ref={iframeRef} />
|
||||
<webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const Frame = styled.iframe`
|
||||
width: calc(100vw - var(--sidebar-width));
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border: none;
|
||||
background-color: white;
|
||||
`
|
||||
const WebviewStyle: React.CSSProperties = {
|
||||
width: 'calc(100vw - var(--sidebar-width))',
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
backgroundColor: 'white',
|
||||
display: 'inline-flex'
|
||||
}
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -3,6 +3,7 @@ import systemAgents from '@renderer/config/agents.json'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { covertAgentToAssistant } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Agent, Assistant } from '@renderer/types'
|
||||
import { Input, Modal, Tag } from 'antd'
|
||||
import { useMemo, useState } from 'react'
|
||||
@@ -25,25 +26,32 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
() => ({
|
||||
id: defaultAssistant.id,
|
||||
name: defaultAssistant.name,
|
||||
emoji: '',
|
||||
emoji: defaultAssistant.emoji || '',
|
||||
prompt: defaultAssistant.prompt,
|
||||
group: 'system'
|
||||
}),
|
||||
[defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
|
||||
[defaultAssistant.emoji, defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
|
||||
)
|
||||
|
||||
const agents = useMemo(() => {
|
||||
const allAgents = [defaultAgent, ...userAgents, ...systemAgents] as Agent[]
|
||||
const list = allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))
|
||||
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||
const list = [defaultAgent, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
|
||||
return searchText
|
||||
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
|
||||
: list
|
||||
}, [assistants, defaultAgent, searchText, userAgents])
|
||||
|
||||
const onCreateAssistant = (agent: Agent) => {
|
||||
if (assistants.map((a) => a.id).includes(String(agent.id))) return
|
||||
if (agent.id !== 'default') {
|
||||
if (assistants.map((a) => a.id).includes(String(agent.id))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const assistant = covertAgentToAssistant(agent)
|
||||
|
||||
addAssistant(assistant)
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
resolve(assistant)
|
||||
setOpen(false)
|
||||
}
|
||||
@@ -59,13 +67,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ marginTop: '5vh' }}
|
||||
centered
|
||||
title={t('chat.add.assistant.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName=""
|
||||
maskTransitionName=""
|
||||
transitionName="ant-move-down"
|
||||
maskTransitionName="ant-fade"
|
||||
footer={null}>
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
|
||||
@@ -34,7 +34,15 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={assistant.name} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}>
|
||||
<Modal
|
||||
title={assistant.name}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={handleCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="ant-move-down"
|
||||
maskTransitionName="ant-fade"
|
||||
centered>
|
||||
<Box mb={8}>{t('common.name')}</Box>
|
||||
<Input
|
||||
placeholder={t('common.assistant') + t('common.name')}
|
||||
@@ -45,7 +53,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
||||
{t('common.prompt')}
|
||||
</Box>
|
||||
<TextArea
|
||||
rows={4}
|
||||
rows={10}
|
||||
placeholder={t('common.assistant') + t('common.prompt')}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useRuntime } from '@renderer/hooks/useStore'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Props = PropsWithChildren & JSX.IntrinsicElements['div']
|
||||
|
||||
const navbarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||
|
||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
const { minappShow } = useRuntime()
|
||||
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBackgroundColor
|
||||
const { windowStyle } = useSettings()
|
||||
|
||||
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
||||
const navbarBgColor = macTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBgColor
|
||||
|
||||
return (
|
||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
||||
@@ -39,7 +42,6 @@ const NavbarContainer = styled.div`
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
background-color: ${navbarBackgroundColor};
|
||||
transition: background-color 0.3s ease;
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
@@ -1,49 +1,67 @@
|
||||
import { TranslationOutlined } from '@ant-design/icons'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useStore'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useRuntime, useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { Avatar } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import UserPopup from '../Popups/UserPopup'
|
||||
|
||||
const sidebarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const avatar = useAvatar()
|
||||
const { minappShow } = useRuntime()
|
||||
const { toggleShowAssistants } = useShowAssistants()
|
||||
const { generating } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { windowStyle } = useSettings()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
|
||||
const onEditUser = () => {
|
||||
UserPopup.show()
|
||||
const onEditUser = () => UserPopup.show()
|
||||
|
||||
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
||||
const sidebarBgColor = macTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||
|
||||
const to = (path: string) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const onToggleShowAssistants = () => {
|
||||
pathname === '/' ? toggleShowAssistants() : navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBackgroundColor }}>
|
||||
<AvatarImg src={avatar || Logo} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBgColor }}>
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
<MainMenus>
|
||||
<Menus>
|
||||
<StyledLink to="/">
|
||||
<StyledLink onClick={onToggleShowAssistants}>
|
||||
<Icon className={isRoute('/')}>
|
||||
<i className="iconfont icon-chat"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/agents">
|
||||
<StyledLink onClick={() => to('/agents')}>
|
||||
<Icon className={isRoute('/agents')}>
|
||||
<i className="iconfont icon-business-smart-assistant"></i>
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/translate">
|
||||
<StyledLink onClick={() => to('/translate')}>
|
||||
<Icon className={isRoute('/translate')}>
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
<StyledLink to="/apps">
|
||||
<StyledLink onClick={() => to('/apps')}>
|
||||
<Icon className={isRoute('/apps')}>
|
||||
<i className="iconfont icon-appstore"></i>
|
||||
</Icon>
|
||||
@@ -51,7 +69,7 @@ const Sidebar: FC = () => {
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus>
|
||||
<StyledLink to="/settings/provider">
|
||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting"></i>
|
||||
</Icon>
|
||||
@@ -72,7 +90,6 @@ const Container = styled.div`
|
||||
-webkit-app-region: drag !important;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
|
||||
background-color: ${sidebarBackgroundColor};
|
||||
transition: background-color 0.3s ease;
|
||||
`
|
||||
|
||||
@@ -133,7 +150,7 @@ const Icon = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
const StyledLink = styled.div`
|
||||
text-decoration: none;
|
||||
-webkit-app-region: none;
|
||||
&* {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const DEFAULT_TEMPERATURE = 0.7
|
||||
export const DEFAULT_CONEXTCOUNT = 5
|
||||
export const DEFAULT_CONEXTCOUNT = 6
|
||||
export const DEFAULT_MAX_TOKENS = 4096
|
||||
export const FONT_FAMILY =
|
||||
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
||||
|
||||
6
src/renderer/src/config/env.ts
Normal file
6
src/renderer/src/config/env.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as UserAvatar } from '@renderer/assets/images/avatar.png'
|
||||
export { default as AppLogo } from '@renderer/assets/images/logo.png'
|
||||
|
||||
export const APP_NAME = 'Cherry Studio'
|
||||
|
||||
export const isLocalAi = false
|
||||
85
src/renderer/src/config/minapp.ts
Normal file
85
src/renderer/src/config/minapp.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
|
||||
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
|
||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
|
||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
|
||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
|
||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
|
||||
const _apps: MinAppType[] = [
|
||||
{
|
||||
name: 'AI 助手',
|
||||
logo: AiAssistantAppLogo,
|
||||
url: 'https://bot.360.com/'
|
||||
},
|
||||
{
|
||||
name: '文心一言',
|
||||
logo: BaiduAiAppLogo,
|
||||
url: 'https://yiyan.baidu.com/'
|
||||
},
|
||||
{
|
||||
name: 'SparkDesk',
|
||||
logo: SparkDeskAppLogo,
|
||||
url: 'https://xinghuo.xfyun.cn/desk'
|
||||
},
|
||||
{
|
||||
name: '腾讯元宝',
|
||||
logo: TencentYuanbaoAppLogo,
|
||||
url: 'https://yuanbao.tencent.com/chat'
|
||||
},
|
||||
{
|
||||
name: '商量',
|
||||
logo: SensetimeAppLogo,
|
||||
url: 'https://chat.sensetime.com/wb/chat'
|
||||
},
|
||||
{
|
||||
name: '360AI搜索',
|
||||
logo: AiSearchAppLogo,
|
||||
url: 'https://so.360.com/'
|
||||
},
|
||||
{
|
||||
name: '秘塔AI搜索',
|
||||
logo: MetasoAppLogo,
|
||||
url: 'https://metaso.cn/'
|
||||
},
|
||||
{
|
||||
name: '天工AI',
|
||||
logo: TiangongAiLogo,
|
||||
url: 'https://www.tiangong.cn/'
|
||||
},
|
||||
{
|
||||
name: 'DEVV_',
|
||||
logo: DevvAppLogo,
|
||||
url: 'https://devv.ai/'
|
||||
},
|
||||
{
|
||||
name: 'perplexity',
|
||||
logo: PerplexityAppLogo,
|
||||
url: 'https://www.perplexity.ai/'
|
||||
},
|
||||
{
|
||||
name: '知乎直答',
|
||||
logo: ZhihuAppLogo,
|
||||
url: 'https://zhida.zhihu.com/'
|
||||
}
|
||||
]
|
||||
|
||||
export function getAllMinApps() {
|
||||
const list: MinAppType[] = (Object.entries(PROVIDER_CONFIG) as any[])
|
||||
.filter(([, config]) => config.app)
|
||||
.map(([key, config]) => ({ id: key, ...config.app }))
|
||||
.concat(_apps)
|
||||
return list
|
||||
}
|
||||
|
||||
export function startMinAppById(id: string) {
|
||||
const app = getAllMinApps().find((app) => app?.id === id)
|
||||
app && MinApp.start(app)
|
||||
}
|
||||
@@ -1,36 +1,66 @@
|
||||
import { Model } from '@renderer/types'
|
||||
|
||||
type SystemModel = Model & { enabled: boolean }
|
||||
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-turbo|dall|cogview/i
|
||||
const EMBEDDING_REGEX = /embedding/i
|
||||
|
||||
export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
ollama: [],
|
||||
silicon: [
|
||||
{
|
||||
id: 'Qwen/Qwen2-7B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-7B-Instruct',
|
||||
group: 'Qwen2'
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-72B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-72B-Instruct',
|
||||
group: 'Qwen2'
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
provider: 'silicon',
|
||||
name: 'GLM-4-9B-Chat',
|
||||
group: 'GLM'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'DeepSeek-V2-Chat',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'DeepSeek-Coder-V2-Instruct',
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4o',
|
||||
group: 'GPT 4o',
|
||||
enabled: true
|
||||
group: 'GPT 4o'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4o-mini',
|
||||
group: 'GPT 4o',
|
||||
enabled: true
|
||||
group: 'GPT 4o'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-turbo',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4 Turbo',
|
||||
group: 'GPT 4',
|
||||
enabled: true
|
||||
group: 'GPT 4'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4',
|
||||
group: 'GPT 4',
|
||||
enabled: true
|
||||
group: 'GPT 4'
|
||||
}
|
||||
],
|
||||
gemini: [
|
||||
@@ -38,129 +68,39 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'gemini-1.5-flash',
|
||||
provider: 'gemini',
|
||||
name: 'Gemini 1.5 Flash',
|
||||
group: 'Gemini 1.5',
|
||||
enabled: true
|
||||
group: 'Gemini 1.5'
|
||||
},
|
||||
{
|
||||
id: 'gemini-1.5-pro-exp-0801',
|
||||
provider: 'gemini',
|
||||
name: 'Gemini 1.5 Pro Experimental 0801',
|
||||
group: 'Gemini 1.5',
|
||||
enabled: true
|
||||
group: 'Gemini 1.5'
|
||||
}
|
||||
],
|
||||
silicon: [
|
||||
anthropic: [
|
||||
{
|
||||
id: 'Qwen/Qwen2-7B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-7B-Instruct',
|
||||
group: 'Qwen2',
|
||||
enabled: true
|
||||
id: 'claude-3-5-sonnet-20240620',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
group: 'Claude 3.5'
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-1.5B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-1.5B-Instruct',
|
||||
group: 'Qwen2',
|
||||
enabled: false
|
||||
id: 'claude-3-opus-20240229',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3 Opus',
|
||||
group: 'Claude 3'
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-7B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-7B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
enabled: false
|
||||
id: 'claude-3-sonnet-20240229',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3 Sonnet',
|
||||
group: 'Claude 3'
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-72B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-72B-Instruct',
|
||||
group: 'Qwen2',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-57B-A14B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2-57B-A14B-Instruct',
|
||||
group: 'Qwen2',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-110B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-110B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-32B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-32B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-14B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen1.5-14B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'DeepSeek-V2-Chat',
|
||||
group: 'DeepSeek',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'DeepSeek-Coder-V2-Instruct',
|
||||
group: 'DeepSeek',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/deepseek-llm-67b-chat',
|
||||
provider: 'silicon',
|
||||
name: 'Deepseek-LLM-67B-Chat',
|
||||
group: 'DeepSeek',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
provider: 'silicon',
|
||||
name: 'GLM-4-9B-Chat',
|
||||
group: 'GLM',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'THUDM/chatglm3-6b',
|
||||
provider: 'silicon',
|
||||
name: 'GhatGLM3-6B',
|
||||
group: 'GLM',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-9B-Chat-16K',
|
||||
provider: 'silicon',
|
||||
name: 'Yi-1.5-9B-Chat-16K',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-6B-Chat',
|
||||
provider: 'silicon',
|
||||
name: 'Yi-1.5-6B-Chat',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-34B-Chat-16K',
|
||||
provider: 'silicon',
|
||||
name: 'Yi-1.5-34B-Chat-16K',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
id: 'claude-3-haiku-20240307',
|
||||
provider: 'anthropic',
|
||||
name: 'Claude 3 Haiku',
|
||||
group: 'Claude 3'
|
||||
}
|
||||
],
|
||||
deepseek: [
|
||||
@@ -168,15 +108,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'deepseek-chat',
|
||||
provider: 'deepseek',
|
||||
name: 'DeepSeek Chat',
|
||||
group: 'DeepSeek Chat',
|
||||
enabled: true
|
||||
group: 'DeepSeek Chat'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-coder',
|
||||
provider: 'deepseek',
|
||||
name: 'DeepSeek Coder',
|
||||
group: 'DeepSeek Coder',
|
||||
enabled: true
|
||||
group: 'DeepSeek Coder'
|
||||
}
|
||||
],
|
||||
yi: [
|
||||
@@ -184,87 +122,87 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'yi-large',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Large',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
group: 'Yi'
|
||||
},
|
||||
{
|
||||
id: 'yi-large-turbo',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Large-Turbo',
|
||||
group: 'Yi',
|
||||
enabled: true
|
||||
group: 'Yi'
|
||||
},
|
||||
{
|
||||
id: 'yi-large-rag',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Large-Rag',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
group: 'Yi'
|
||||
},
|
||||
{
|
||||
id: 'yi-medium',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Medium',
|
||||
group: 'Yi',
|
||||
enabled: true
|
||||
group: 'Yi'
|
||||
},
|
||||
{
|
||||
id: 'yi-medium-200k',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Medium-200k',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
group: 'Yi'
|
||||
},
|
||||
{
|
||||
id: 'yi-spark',
|
||||
provider: 'yi',
|
||||
name: 'Yi-Spark',
|
||||
group: 'Yi',
|
||||
enabled: false
|
||||
group: 'Yi'
|
||||
}
|
||||
],
|
||||
zhipu: [
|
||||
{
|
||||
id: 'glm-4-0520',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-0520',
|
||||
group: 'GLM',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'glm-4',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4',
|
||||
group: 'GLM',
|
||||
enabled: false
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-airx',
|
||||
id: 'glm-4-plus',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AirX',
|
||||
group: 'GLM',
|
||||
enabled: false
|
||||
name: 'GLM-4-Plus',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-air',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Air',
|
||||
group: 'GLM',
|
||||
enabled: true
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-airx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AirX',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flash',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Flash',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4V',
|
||||
group: 'GLM',
|
||||
enabled: false
|
||||
name: 'GLM 4V',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v-plus',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4V-Plus',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-alltools',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AllTools',
|
||||
group: 'GLM',
|
||||
enabled: false
|
||||
group: 'GLM-4-AllTools'
|
||||
}
|
||||
],
|
||||
moonshot: [
|
||||
@@ -272,22 +210,19 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'moonshot-v1-8k',
|
||||
provider: 'moonshot',
|
||||
name: 'Moonshot V1 8k',
|
||||
group: 'Moonshot V1',
|
||||
enabled: true
|
||||
group: 'Moonshot V1'
|
||||
},
|
||||
{
|
||||
id: 'moonshot-v1-32k',
|
||||
provider: 'moonshot',
|
||||
name: 'Moonshot V1 32k',
|
||||
group: 'Moonshot V1',
|
||||
enabled: true
|
||||
group: 'Moonshot V1'
|
||||
},
|
||||
{
|
||||
id: 'moonshot-v1-128k',
|
||||
provider: 'moonshot',
|
||||
name: 'Moonshot V1 128k',
|
||||
group: 'Moonshot V1',
|
||||
enabled: true
|
||||
group: 'Moonshot V1'
|
||||
}
|
||||
],
|
||||
baichuan: [
|
||||
@@ -295,22 +230,19 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'Baichuan4',
|
||||
provider: 'baichuan',
|
||||
name: 'Baichuan4',
|
||||
group: 'Baichuan4',
|
||||
enabled: true
|
||||
group: 'Baichuan4'
|
||||
},
|
||||
{
|
||||
id: 'Baichuan3-Turbo',
|
||||
provider: 'baichuan',
|
||||
name: 'Baichuan3 Turbo',
|
||||
group: 'Baichuan3',
|
||||
enabled: true
|
||||
group: 'Baichuan3'
|
||||
},
|
||||
{
|
||||
id: 'Baichuan3-Turbo-128k',
|
||||
provider: 'baichuan',
|
||||
name: 'Baichuan3 Turbo 128k',
|
||||
group: 'Baichuan3',
|
||||
enabled: true
|
||||
group: 'Baichuan3'
|
||||
}
|
||||
],
|
||||
dashscope: [
|
||||
@@ -318,22 +250,19 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'qwen-turbo',
|
||||
provider: 'dashscope',
|
||||
name: 'Qwen Turbo',
|
||||
group: 'Qwen',
|
||||
enabled: true
|
||||
group: 'Qwen'
|
||||
},
|
||||
{
|
||||
id: 'qwen-plus',
|
||||
provider: 'dashscope',
|
||||
name: 'Qwen Plus',
|
||||
group: 'Qwen',
|
||||
enabled: true
|
||||
group: 'Qwen'
|
||||
},
|
||||
{
|
||||
id: 'qwen-max',
|
||||
provider: 'dashscope',
|
||||
name: 'Qwen Max',
|
||||
group: 'Qwen',
|
||||
enabled: true
|
||||
group: 'Qwen'
|
||||
}
|
||||
],
|
||||
stepfun: [
|
||||
@@ -341,15 +270,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'step-1-8k',
|
||||
provider: 'stepfun',
|
||||
name: 'Step 1 8K',
|
||||
group: 'Step 1',
|
||||
enabled: true
|
||||
group: 'Step 1'
|
||||
},
|
||||
{
|
||||
id: 'step-1-flash',
|
||||
provider: 'stepfun',
|
||||
name: 'Step 1 Flash',
|
||||
group: 'Step 1',
|
||||
enabled: true
|
||||
group: 'Step 1'
|
||||
}
|
||||
],
|
||||
doubao: [],
|
||||
@@ -358,29 +285,25 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'abab6.5s-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab6.5s',
|
||||
group: 'abab6',
|
||||
enabled: true
|
||||
group: 'abab6'
|
||||
},
|
||||
{
|
||||
id: 'abab6.5g-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab6.5g',
|
||||
group: 'abab6',
|
||||
enabled: true
|
||||
group: 'abab6'
|
||||
},
|
||||
{
|
||||
id: 'abab6.5t-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab6.5t',
|
||||
group: 'abab6',
|
||||
enabled: true
|
||||
group: 'abab6'
|
||||
},
|
||||
{
|
||||
id: 'abab5.5s-chat',
|
||||
provider: 'minimax',
|
||||
name: 'abab5.5s',
|
||||
group: 'abab5',
|
||||
enabled: true
|
||||
group: 'abab5'
|
||||
}
|
||||
],
|
||||
aihubmix: [
|
||||
@@ -388,15 +311,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'aihubmix',
|
||||
name: 'GPT-4o Mini',
|
||||
group: 'GPT-4o',
|
||||
enabled: true
|
||||
group: 'GPT-4o'
|
||||
},
|
||||
{
|
||||
id: 'aihubmix-Llama-3-70B-Instruct',
|
||||
provider: 'aihubmix',
|
||||
name: 'Llama 3 70B Instruct',
|
||||
group: 'Llama3',
|
||||
enabled: true
|
||||
group: 'Llama3'
|
||||
}
|
||||
],
|
||||
openrouter: [
|
||||
@@ -404,36 +325,31 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'google/gemma-2-9b-it:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Google: Gemma 2 9B',
|
||||
group: 'Gemma',
|
||||
enabled: true
|
||||
group: 'Gemma'
|
||||
},
|
||||
{
|
||||
id: 'microsoft/phi-3-mini-128k-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Phi-3 Mini 128K Instruct',
|
||||
group: 'Phi',
|
||||
enabled: true
|
||||
group: 'Phi'
|
||||
},
|
||||
{
|
||||
id: 'microsoft/phi-3-medium-128k-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Phi-3 Medium 128K Instruct',
|
||||
group: 'Phi',
|
||||
enabled: true
|
||||
group: 'Phi'
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/llama-3-8b-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Meta: Llama 3 8B Instruct',
|
||||
group: 'Llama3',
|
||||
enabled: true
|
||||
group: 'Llama3'
|
||||
},
|
||||
{
|
||||
id: 'mistralai/mistral-7b-instruct:free',
|
||||
provider: 'openrouter',
|
||||
name: 'Mistral: Mistral 7B Instruct',
|
||||
group: 'Mistral',
|
||||
enabled: true
|
||||
group: 'Mistral'
|
||||
}
|
||||
],
|
||||
groq: [
|
||||
@@ -441,59 +357,33 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
id: 'llama3-8b-8192',
|
||||
provider: 'groq',
|
||||
name: 'LLaMA3 8B',
|
||||
group: 'Llama3',
|
||||
enabled: false
|
||||
group: 'Llama3'
|
||||
},
|
||||
{
|
||||
id: 'llama3-70b-8192',
|
||||
provider: 'groq',
|
||||
name: 'LLaMA3 70B',
|
||||
group: 'Llama3',
|
||||
enabled: true
|
||||
group: 'Llama3'
|
||||
},
|
||||
{
|
||||
id: 'mixtral-8x7b-32768',
|
||||
provider: 'groq',
|
||||
name: 'Mixtral 8x7B',
|
||||
group: 'Mixtral',
|
||||
enabled: false
|
||||
group: 'Mixtral'
|
||||
},
|
||||
{
|
||||
id: 'gemma-7b-it',
|
||||
provider: 'groq',
|
||||
name: 'Gemma 7B',
|
||||
group: 'Gemma',
|
||||
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
|
||||
group: 'Gemma'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function isTextToImageModel(model: Model): boolean {
|
||||
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||
}
|
||||
|
||||
export function isEmbeddingModel(model: Model): boolean {
|
||||
return EMBEDDING_REGEX.test(model.id)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png'
|
||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
|
||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
||||
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
||||
|
||||
8
src/renderer/src/env.d.ts
vendored
8
src/renderer/src/env.d.ts
vendored
@@ -4,6 +4,14 @@ import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import { MessageInstance } from 'antd/es/message/interface'
|
||||
import { HookAPI } from 'antd/es/modal/useModal'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
message: MessageInstance
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import i18n from '@renderer/i18n'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
@@ -5,12 +6,14 @@ import { setAvatar } from '@renderer/store/runtime'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import { useSettings } from './useSettings'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl } = useSettings()
|
||||
const { language } = useSettings()
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@@ -33,4 +36,14 @@ export function useAppInit() {
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || navigator.language || 'en-US')
|
||||
}, [language])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLocalAi) {
|
||||
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)
|
||||
setDefaultModel(model)
|
||||
setTopicNamingModel(model)
|
||||
setTranslateModel(model)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}
|
||||
|
||||
7
src/renderer/src/hooks/useModel.ts
Normal file
7
src/renderer/src/hooks/useModel.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useProviders } from './useProvider'
|
||||
|
||||
export function useModel(id?: string) {
|
||||
const { providers } = useProviders()
|
||||
const allModels = providers.map((p) => p.models).flat()
|
||||
return allModels.find((m) => m.id === id)
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
SendMessageShortcut,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setTheme,
|
||||
setTopicPosition,
|
||||
setWindowStyle,
|
||||
ThemeMode
|
||||
} from '@renderer/store/settings'
|
||||
|
||||
@@ -17,6 +19,12 @@ export function useSettings() {
|
||||
},
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
},
|
||||
setWindowStyle(windowStyle: 'transparent' | 'opaque') {
|
||||
dispatch(setWindowStyle(windowStyle))
|
||||
},
|
||||
setTopicPosition(topicPosition: 'left' | 'right') {
|
||||
dispatch(setTopicPosition(topicPosition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setShowRightSidebar, toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
|
||||
|
||||
export function useShowRightSidebar() {
|
||||
const showRightSidebar = useAppSelector((state) => state.settings.showRightSidebar)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
rightSidebarShown: showRightSidebar,
|
||||
toggleRightSidebar: () => dispatch(toggleRightSidebar()),
|
||||
showRightSidebar: () => dispatch(setShowRightSidebar(true)),
|
||||
hideRightSidebar: () => dispatch(setShowRightSidebar(false))
|
||||
}
|
||||
}
|
||||
import { setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings'
|
||||
|
||||
export function useShowAssistants() {
|
||||
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
||||
@@ -23,6 +11,17 @@ export function useShowAssistants() {
|
||||
}
|
||||
}
|
||||
|
||||
export function useShowTopics() {
|
||||
const showTopics = useAppSelector((state) => state.settings.showTopics)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
showTopics,
|
||||
setShowTopics: (show: boolean) => dispatch(setShowTopics(show)),
|
||||
toggleShowTopics: () => dispatch(toggleShowTopics())
|
||||
}
|
||||
}
|
||||
|
||||
export function useRuntime() {
|
||||
return useAppSelector((state) => state.runtime)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ import { Assistant, Topic } from '@renderer/types'
|
||||
import { find } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useAssistant } from './useAssistant'
|
||||
|
||||
let _activeTopic: Topic
|
||||
|
||||
export function useActiveTopic(assistant: Assistant) {
|
||||
export function useActiveTopic(_assistant: Assistant) {
|
||||
const { assistant } = useAssistant(_assistant.id)
|
||||
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -29,13 +29,17 @@ const resources = {
|
||||
select: 'Select',
|
||||
search: 'Search',
|
||||
default: 'Default',
|
||||
warning: 'Warning'
|
||||
warning: 'Warning',
|
||||
back: 'Back',
|
||||
chat: 'Chat'
|
||||
},
|
||||
button: {
|
||||
add: 'Add',
|
||||
added: 'Added',
|
||||
manage: 'Manage',
|
||||
select_model: 'Select Model'
|
||||
select_model: 'Select Model',
|
||||
'show.all': 'Show All',
|
||||
collapse: 'Collapse'
|
||||
},
|
||||
message: {
|
||||
copied: 'Copied!',
|
||||
@@ -57,7 +61,7 @@ const resources = {
|
||||
},
|
||||
chat: {
|
||||
save: 'Save',
|
||||
'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',
|
||||
@@ -66,17 +70,20 @@ const resources = {
|
||||
'topics.edit.placeholder': 'Enter new name',
|
||||
'topics.delete.all.title': 'Delete all topics',
|
||||
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
|
||||
'topics.list': 'Topic List',
|
||||
'input.new_topic': 'New Topic',
|
||||
'input.topics': ' Topics ',
|
||||
'input.clear': 'Clear',
|
||||
'input.clear': 'Clear Messages',
|
||||
'input.new.context': 'Clear Context',
|
||||
'input.expand': 'Expand',
|
||||
'input.collapse': 'Collapse',
|
||||
'input.clear.title': 'Clear all messages?',
|
||||
'input.clear.content': 'Are you sure to clear all messages?',
|
||||
'input.clear.content': 'Do you want to clear all messages of the current topic?',
|
||||
'input.placeholder': 'Type your message here...',
|
||||
'input.send': 'Send',
|
||||
'input.pause': 'Pause',
|
||||
'input.settings': 'Settings',
|
||||
'input.upload': 'Upload image png、jpg、jpeg',
|
||||
'input.context_count.tip': 'Context Count',
|
||||
'input.estimated_tokens.tip': 'Estimated tokens',
|
||||
'settings.temperature': 'Temperature',
|
||||
@@ -91,7 +98,8 @@ const resources = {
|
||||
'settings.set_as_default': 'Apply to default assistant',
|
||||
'settings.max': 'Max',
|
||||
'suggestions.title': 'Suggested Questions',
|
||||
'add.assistant.title': 'Add Assistant'
|
||||
'add.assistant.title': 'Add Assistant',
|
||||
'message.new.context': 'New Context'
|
||||
},
|
||||
agents: {
|
||||
title: 'Agents',
|
||||
@@ -147,6 +155,8 @@ const resources = {
|
||||
'general.user_name': 'User Name',
|
||||
'general.user_name.placeholder': 'Enter your name',
|
||||
'general.backup.title': 'Data Backup and Recovery',
|
||||
'general.backup.button': 'Backup',
|
||||
'general.restore.button': 'Restore',
|
||||
'general.reset.title': 'Data Reset',
|
||||
'general.reset.button': 'Reset',
|
||||
'provider.api_key': 'API Key',
|
||||
@@ -198,7 +208,13 @@ const resources = {
|
||||
'theme.dark': 'Dark',
|
||||
'theme.light': 'Light',
|
||||
'theme.auto': 'Auto',
|
||||
'font_size.title': 'Message Font Size'
|
||||
'theme.window.style.title': 'Window Style',
|
||||
'theme.window.style.transparent': 'Transparent Window',
|
||||
'theme.window.style.opaque': 'Opaque Window',
|
||||
'font_size.title': 'Message Font Size',
|
||||
'topic.position': 'Topic Position',
|
||||
'topic.position.left': 'Left',
|
||||
'topic.position.right': 'Right'
|
||||
},
|
||||
translate: {
|
||||
title: 'Translation',
|
||||
@@ -266,13 +282,17 @@ const resources = {
|
||||
select: '选择',
|
||||
search: '搜索',
|
||||
default: '默认',
|
||||
warning: '警告'
|
||||
warning: '警告',
|
||||
back: '返回',
|
||||
chat: '聊天'
|
||||
},
|
||||
button: {
|
||||
add: '添加',
|
||||
added: '已添加',
|
||||
manage: '管理',
|
||||
select_model: '选择模型'
|
||||
select_model: '选择模型',
|
||||
'show.all': '显示全部',
|
||||
collapse: '收起'
|
||||
},
|
||||
message: {
|
||||
copied: '已复制',
|
||||
@@ -294,7 +314,7 @@ const resources = {
|
||||
},
|
||||
chat: {
|
||||
save: '保存',
|
||||
'default.name': '🔆 默认助手 - Assistant',
|
||||
'default.name': '默认助手',
|
||||
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
|
||||
'default.topic.name': '默认话题',
|
||||
'topics.title': '话题',
|
||||
@@ -303,17 +323,20 @@ const resources = {
|
||||
'topics.edit.placeholder': '输入新名称',
|
||||
'topics.delete.all.title': '删除所有话题',
|
||||
'topics.delete.all.content': '确定要删除所有话题吗?',
|
||||
'topics.list': '话题列表',
|
||||
'input.new_topic': '新话题',
|
||||
'input.topics': ' 话题 ',
|
||||
'input.clear': '清除',
|
||||
'input.clear': '清除会话消息',
|
||||
'input.new.context': '清除上下文',
|
||||
'input.expand': '展开',
|
||||
'input.collapse': '收起',
|
||||
'input.clear.title': '清除所有消息?',
|
||||
'input.clear.content': '确定要清除所有消息吗?',
|
||||
'input.clear.title': '清除消息?',
|
||||
'input.clear.content': '确定要清除当前会话所有消息吗?',
|
||||
'input.placeholder': '在这里输入消息...',
|
||||
'input.send': '发送',
|
||||
'input.pause': '暂停',
|
||||
'input.settings': '设置',
|
||||
'input.upload': '上传图片 png、jpg、jpeg',
|
||||
'input.context_count.tip': '上下文数',
|
||||
'input.estimated_tokens.tip': '预估 token 数',
|
||||
'settings.temperature': '模型温度',
|
||||
@@ -329,7 +352,8 @@ const resources = {
|
||||
'settings.set_as_default': '应用到默认助手',
|
||||
'settings.max': '不限',
|
||||
'suggestions.title': '建议的问题',
|
||||
'add.assistant.title': '添加智能体'
|
||||
'add.assistant.title': '添加智能体',
|
||||
'message.new.context': '清除上下文'
|
||||
},
|
||||
agents: {
|
||||
title: '智能体',
|
||||
@@ -385,6 +409,8 @@ const resources = {
|
||||
'general.user_name': '用户名',
|
||||
'general.user_name.placeholder': '请输入用户名',
|
||||
'general.backup.title': '数据备份与恢复',
|
||||
'general.backup.button': '备份',
|
||||
'general.restore.button': '恢复',
|
||||
'general.reset.title': '重置数据',
|
||||
'general.reset.button': '重置',
|
||||
'provider.api_key': 'API 密钥',
|
||||
@@ -436,7 +462,13 @@ const resources = {
|
||||
'theme.dark': '深色主题',
|
||||
'theme.light': '浅色主题',
|
||||
'theme.auto': '跟随系统',
|
||||
'font_size.title': '消息字体大小'
|
||||
'theme.window.style.title': '窗口样式',
|
||||
'theme.window.style.transparent': '透明窗口',
|
||||
'theme.window.style.opaque': '不透明窗口',
|
||||
'font_size.title': '消息字体大小',
|
||||
'topic.position': '话题位置',
|
||||
'topic.position.left': '左侧',
|
||||
'topic.position.right': '右侧'
|
||||
},
|
||||
translate: {
|
||||
title: '翻译',
|
||||
|
||||
@@ -1,27 +1,9 @@
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import * as Sentry from '@sentry/electron/renderer'
|
||||
import localforage from 'localforage'
|
||||
|
||||
import { APP_NAME } from './config/env'
|
||||
import { ThemeMode } from './store/settings'
|
||||
import { isProduction, loadScript } from './utils'
|
||||
|
||||
async function initSentry() {
|
||||
if (await isProduction()) {
|
||||
Sentry.init({
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// Capture Replay for 10% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0
|
||||
})
|
||||
}
|
||||
}
|
||||
import { loadScript } from './utils'
|
||||
|
||||
export async function initMermaid(theme: ThemeMode) {
|
||||
if (!window.mermaid) {
|
||||
@@ -41,13 +23,11 @@ function init() {
|
||||
name: 'CherryAI',
|
||||
version: 1.0,
|
||||
storeName: 'cherryai',
|
||||
description: 'Cherry Studio Storage'
|
||||
description: `${APP_NAME} Storage`
|
||||
})
|
||||
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
|
||||
initSentry()
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
@@ -33,6 +33,7 @@ const AppsPage: FC = () => {
|
||||
icon: null,
|
||||
closable: true,
|
||||
maskClosable: true,
|
||||
centered: true,
|
||||
okButtonProps: { type: 'primary', disabled: Boolean(added) },
|
||||
okText: added ? t('button.added') : t('button.add'),
|
||||
onOk: () => onAddAgent(agent)
|
||||
|
||||
@@ -83,14 +83,14 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ marginTop: '10vh' }}
|
||||
title={agent ? t('agents.edit.title') : t('agents.add.title')}
|
||||
open={open}
|
||||
onOk={() => formRef.current?.submit()}
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
afterClose={onClose}
|
||||
okText={agent ? t('common.save') : t('agents.add.button')}>
|
||||
okText={agent ? t('common.save') : t('agents.add.button')}
|
||||
centered>
|
||||
<Form
|
||||
ref={formRef}
|
||||
form={form}
|
||||
@@ -108,7 +108,7 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
|
||||
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item name="prompt" label={t('agents.add.prompt')} rules={[{ required: true }]}>
|
||||
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={4} />
|
||||
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -35,13 +35,13 @@ const PopupContainer: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ marginTop: '10vh' }}
|
||||
title={t('agents.manage.title')}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
footer={null}>
|
||||
footer={null}
|
||||
centered>
|
||||
<Container>
|
||||
{agents.length > 0 && (
|
||||
<DragableList list={agents} onUpdate={updateAgents}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@@ -12,13 +12,6 @@ const App: FC<Props> = ({ app }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const onClick = () => {
|
||||
const websiteReg = /claude|chatgpt|groq/i
|
||||
|
||||
if (websiteReg.test(app.url)) {
|
||||
window.api.minApp({ url: app.url, windowOptions: { title: app.name } })
|
||||
return
|
||||
}
|
||||
|
||||
MinApp.start(app)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
|
||||
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
|
||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
|
||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
|
||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { getAllMinApps } from '@renderer/config/minapp'
|
||||
import { Empty, Input } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useState } from 'react'
|
||||
@@ -20,63 +10,12 @@ import styled from 'styled-components'
|
||||
|
||||
import App from './App'
|
||||
|
||||
const _apps: MinAppType[] = [
|
||||
{
|
||||
name: 'AI 助手',
|
||||
logo: AiAssistantAppLogo,
|
||||
url: 'https://bot.360.com/'
|
||||
},
|
||||
{
|
||||
name: '文心一言',
|
||||
logo: BaiduAiAppLogo,
|
||||
url: 'https://yiyan.baidu.com/'
|
||||
},
|
||||
{
|
||||
name: 'SparkDesk',
|
||||
logo: SparkDeskAppLogo,
|
||||
url: 'https://xinghuo.xfyun.cn/desk'
|
||||
},
|
||||
{
|
||||
name: '腾讯元宝',
|
||||
logo: TencentYuanbaoAppLogo,
|
||||
url: 'https://yuanbao.tencent.com/chat'
|
||||
},
|
||||
{
|
||||
name: '商量',
|
||||
logo: SensetimeAppLogo,
|
||||
url: 'https://chat.sensetime.com/wb/chat'
|
||||
},
|
||||
{
|
||||
name: '360AI搜索',
|
||||
logo: AiSearchAppLogo,
|
||||
url: 'https://so.360.com/'
|
||||
},
|
||||
{
|
||||
name: '秘塔AI搜索',
|
||||
logo: MetasoAppLogo,
|
||||
url: 'https://metaso.cn/'
|
||||
},
|
||||
{
|
||||
name: '天工AI',
|
||||
logo: TiangongAiLogo,
|
||||
url: 'https://www.tiangong.cn/'
|
||||
},
|
||||
{
|
||||
name: 'DEVV_',
|
||||
logo: DevvAppLogo,
|
||||
url: 'https://devv.ai/'
|
||||
}
|
||||
]
|
||||
const list = getAllMinApps()
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const list: MinAppType[] = (Object.entries(PROVIDER_CONFIG) as any[])
|
||||
.filter(([, config]) => config.app)
|
||||
.map(([key, config]) => ({ id: key, ...config.app }))
|
||||
.concat(_apps)
|
||||
|
||||
const apps = search
|
||||
? list.filter(
|
||||
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { DeleteOutlined, EditOutlined, MinusCircleOutlined } from '@ant-design/icons'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { getDefaultTopic, syncAsistantToAgent } 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 { uuid } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
import { ItemType } from 'antd/es/menu/interface'
|
||||
import { last } from 'lodash'
|
||||
@@ -23,7 +24,7 @@ interface Props {
|
||||
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
||||
const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onDelete = useCallback(
|
||||
@@ -35,6 +36,15 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
|
||||
)
|
||||
|
||||
const onEditAssistant = useCallback(
|
||||
async (assistant: Assistant) => {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||
updateAssistant(_assistant)
|
||||
syncAsistantToAgent(_assistant)
|
||||
},
|
||||
[updateAssistant]
|
||||
)
|
||||
|
||||
const getMenuItems = useCallback(
|
||||
(assistant: Assistant) =>
|
||||
[
|
||||
@@ -42,22 +52,32 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||
updateAssistant(_assistant)
|
||||
syncAsistantToAgent(_assistant)
|
||||
}
|
||||
onClick: () => onEditAssistant(assistant)
|
||||
},
|
||||
{
|
||||
label: t('common.duplicate'),
|
||||
key: 'duplicate',
|
||||
icon: <CopyOutlined />,
|
||||
icon: <CopyIcon />,
|
||||
onClick: async () => {
|
||||
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
||||
addAssistant(_assistant)
|
||||
setActiveAssistant(_assistant)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.delete.all.title'),
|
||||
key: 'delete-all',
|
||||
icon: <MinusCircleOutlined />,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('chat.topics.delete.all.title'),
|
||||
content: t('chat.topics.delete.all.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: removeAllTopics
|
||||
})
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: t('common.delete'),
|
||||
@@ -67,19 +87,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
onClick: () => onDelete(assistant)
|
||||
}
|
||||
] as ItemType[],
|
||||
[addAssistant, onDelete, setActiveAssistant, t, updateAssistant]
|
||||
)
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
const reorderAssistants = droppableReorder<Assistant>(assistants, sourceIndex, destIndex)
|
||||
updateAssistants(reorderAssistants)
|
||||
}
|
||||
},
|
||||
[assistants, updateAssistants]
|
||||
[addAssistant, onDelete, onEditAssistant, removeAllTopics, setActiveAssistant, t]
|
||||
)
|
||||
|
||||
const onSwitchAssistant = useCallback(
|
||||
@@ -90,7 +98,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
key: 'switch-assistant'
|
||||
})
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||
|
||||
setActiveAssistant(assistant)
|
||||
},
|
||||
[generating, setActiveAssistant, t]
|
||||
@@ -98,33 +106,24 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{assistants.map((assistant, index) => (
|
||||
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
||||
<DragableList list={assistants} onUpdate={updateAssistants}>
|
||||
{(assistant) => {
|
||||
const isCurrent = assistant.id === activeAssistant?.id
|
||||
return (
|
||||
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
||||
<AssistantItem
|
||||
onClick={() => onSwitchAssistant(assistant)}
|
||||
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
|
||||
<AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}>
|
||||
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
|
||||
<ArrowRightButton
|
||||
className={`arrow-button ${isCurrent ? 'active' : ''}`}
|
||||
onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
|
||||
<i className="iconfont icon-gridlines" />
|
||||
</ArrowRightButton>
|
||||
{false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>}
|
||||
</AssistantItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)
|
||||
}}
|
||||
</DragableList>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -132,38 +131,39 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--assistants-width);
|
||||
max-width: var(--assistants-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
`
|
||||
|
||||
const AssistantItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 7px 10px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
margin: 0 10px;
|
||||
padding-right: 35px;
|
||||
cursor: pointer;
|
||||
font-family: Ubuntu;
|
||||
.anticon {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon {
|
||||
display: block;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.iconfont {
|
||||
opacity: 0;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
cursor: pointer;
|
||||
.name {
|
||||
font-weight: 500;
|
||||
}
|
||||
.topics-count {
|
||||
display: none;
|
||||
}
|
||||
.iconfont {
|
||||
opacity: 1;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -173,6 +173,46 @@ const AssistantName = styled.div`
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
const ArrowRightButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 5px;
|
||||
.anticon {
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
`
|
||||
|
||||
const TopicCount = styled.div`
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
margin-right: 3px;
|
||||
background-color: var(--color-background-mute);
|
||||
opacity: 0.8;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default Assistants
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Flex } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@@ -11,19 +12,31 @@ import RightSidebar from './RightSidebar'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
}
|
||||
|
||||
const Chat: FC<Props> = (props) => {
|
||||
const { assistant } = useAssistant(props.assistant.id)
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
|
||||
const { topicPosition } = useSettings()
|
||||
const { showTopics } = useShowTopics()
|
||||
|
||||
return (
|
||||
<Container id="chat">
|
||||
<Main vertical flex={1} justify="space-between">
|
||||
<Messages assistant={assistant} topic={activeTopic} />
|
||||
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
|
||||
<Messages assistant={assistant} topic={props.activeTopic} setActiveTopic={props.setActiveTopic} />
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} />
|
||||
</Main>
|
||||
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<RightSidebar
|
||||
activeAssistant={assistant}
|
||||
activeTopic={props.activeTopic}
|
||||
setActiveAssistant={props.setActiveAssistant}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
position="right"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { removeLeadingEmoji } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
import { NewButton } from './HomePage'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
}
|
||||
|
||||
const HomeHeader: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { t } = useTranslation()
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
|
||||
return (
|
||||
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
|
||||
{!showAssistants && (
|
||||
<NewButton onClick={toggleShowAssistants} style={{ marginRight: isMac ? 8 : 25 }}>
|
||||
<i className="iconfont icon-showsidebarhoriz" />
|
||||
</NewButton>
|
||||
)}
|
||||
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('chat.default.name')}</AssistantName>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</NavbarCenter>
|
||||
)
|
||||
}
|
||||
|
||||
const AssistantName = styled.span`
|
||||
margin-left: 5px;
|
||||
margin-right: 10px;
|
||||
font-family: Ubuntu;
|
||||
`
|
||||
|
||||
export default HomeHeader
|
||||
@@ -1,77 +1,43 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Switch } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddAssistantPopup from '../../components/Popups/AddAssistantPopup'
|
||||
import Assistants from './Assistants'
|
||||
import Chat from './Chat'
|
||||
import Navigation from './Header'
|
||||
import Navbar from './Navbar'
|
||||
import RightSidebar from './RightSidebar'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const { assistants } = useAssistants()
|
||||
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
|
||||
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { showAssistants } = useShowAssistants()
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant)
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
const onCreateDefaultAssistant = () => {
|
||||
const assistant = { ...defaultAssistant, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
const onCreateAssistant = async () => {
|
||||
const assistant = await AddAssistantPopup.show()
|
||||
assistant && setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
{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>
|
||||
<NewButton onClick={onCreateAssistant}>
|
||||
<i className="iconfont icon-a-addchat"></i>
|
||||
</NewButton>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<Navigation activeAssistant={activeAssistant} />
|
||||
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 12 }}>
|
||||
<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>
|
||||
<Navbar activeAssistant={activeAssistant} setActiveAssistant={setActiveAssistant} activeTopic={activeTopic} />
|
||||
<ContentContainer>
|
||||
{showAssistants && (
|
||||
<Assistants
|
||||
<RightSidebar
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
onCreateAssistant={onCreateDefaultAssistant}
|
||||
setActiveTopic={setActiveTopic}
|
||||
position="left"
|
||||
/>
|
||||
)}
|
||||
<Chat assistant={activeAssistant} />
|
||||
<Chat
|
||||
assistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
@@ -90,40 +56,4 @@ const ContentContainer = styled.div`
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
export const NewButton = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 4px;
|
||||
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);
|
||||
.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 19px;
|
||||
}
|
||||
.icon-showsidebarhoriz,
|
||||
.icon-hidesidebarhoriz {
|
||||
font-size: 17px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
cursor: pointer;
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const ThemeSwitch = styled(Switch)`
|
||||
-webkit-app-region: none;
|
||||
margin-right: 10px;
|
||||
.icon-theme {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default HomePage
|
||||
|
||||
31
src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx
Normal file
31
src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { PaperClipOutlined } from '@ant-design/icons'
|
||||
import { Tooltip, Upload } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
files: File[]
|
||||
setFiles: (files: File[]) => void
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const AttachmentButton: FC<Props> = ({ files, setFiles, ToolbarButton }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
|
||||
<Upload
|
||||
customRequest={() => {}}
|
||||
accept="image/*"
|
||||
itemRender={() => null}
|
||||
maxCount={1}
|
||||
onChange={async ({ file }) => file?.originFileObj && setFiles([file.originFileObj as File])}>
|
||||
<ToolbarButton type="text" className={files.length ? 'active' : ''}>
|
||||
<PaperClipOutlined style={{ rotate: '135deg' }} />
|
||||
</ToolbarButton>
|
||||
</Upload>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default AttachmentButton
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
PlusCircleOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { estimateInputTokenCount } from '@renderer/services/messages'
|
||||
@@ -18,15 +18,16 @@ import store, { useAppSelector } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import { delay, uuid } from '@renderer/utils'
|
||||
import { Button, Divider, Popconfirm, Tag, Tooltip } from 'antd'
|
||||
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 { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import TokenCount from './TokenCount'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -39,12 +40,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const [text, setText] = useState(_text)
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { addTopic } = useAssistant(assistant.id)
|
||||
const { sendMessageShortcut, showInputEstimatedTokens, fontSize } = useSettings()
|
||||
const { sendMessageShortcut, fontSize } = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
const [contextCount, setContextCount] = useState(0)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const inputRef = useRef<TextAreaRef>(null)
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef(null)
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
|
||||
_text = text
|
||||
|
||||
@@ -67,24 +72,32 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
status: 'success'
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
message.files = files
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
|
||||
setText('')
|
||||
setFiles([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
setExpend(false)
|
||||
}, [assistant.id, assistant.topics, generating, text])
|
||||
}, [assistant.id, assistant.topics, generating, files, text])
|
||||
|
||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const isEnterPressed = event.keyCode == 13
|
||||
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
return setExpend(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
|
||||
if (sendMessageShortcut === 'Enter' && isEnterPressed) {
|
||||
if (event.shiftKey) {
|
||||
return
|
||||
}
|
||||
@@ -92,7 +105,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
return event.preventDefault()
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Shift+Enter' && event.key === 'Enter' && event.shiftKey) {
|
||||
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
||||
sendMessage()
|
||||
return event.preventDefault()
|
||||
}
|
||||
@@ -117,6 +130,40 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
store.dispatch(setGenerating(false))
|
||||
}
|
||||
|
||||
const onNewContext = () => {
|
||||
if (generating) {
|
||||
onPause()
|
||||
return
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||
}
|
||||
|
||||
const resizeTextArea = () => {
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
textArea.style.height = 'auto'
|
||||
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
const onToggleExpended = () => {
|
||||
const isExpended = !expended
|
||||
setExpend(isExpended)
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
|
||||
if (textArea) {
|
||||
if (isExpended) {
|
||||
textArea.style.height = '70vh'
|
||||
} else {
|
||||
resizeTextArea()
|
||||
}
|
||||
}
|
||||
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
|
||||
const onInput = () => !expended && resizeTextArea()
|
||||
|
||||
// Command or Ctrl + N create new topic
|
||||
useEffect(() => {
|
||||
const onKeydown = (e) => {
|
||||
@@ -124,7 +171,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||
addNewTopic()
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
inputRef.current?.focus()
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,22 +184,38 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
||||
setText(message.content)
|
||||
inputRef.current?.focus()
|
||||
textareaRef.current?.focus()
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
|
||||
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
|
||||
_setEstimateTokenCount(tokensCount)
|
||||
setContextCount(contextCount)
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
textareaRef.current?.focus()
|
||||
}, [assistant])
|
||||
|
||||
return (
|
||||
<Container
|
||||
id="inputbar"
|
||||
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
|
||||
className={inputFocus ? 'focus' : ''}>
|
||||
<Container id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
rows={1}
|
||||
ref={textareaRef}
|
||||
style={{ fontSize }}
|
||||
styles={{ textarea: TextareaStyle }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
onInput={onInput}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||
@@ -174,41 +237,38 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
|
||||
<ToolbarButton
|
||||
type="text"
|
||||
onClick={() => {
|
||||
!showTopics && toggleShowTopics()
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}}>
|
||||
<HistoryOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
|
||||
<ToolbarButton
|
||||
type="text"
|
||||
onClick={() => {
|
||||
!showTopics && toggleShowTopics()
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
|
||||
}}>
|
||||
<ControlOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{/* <AttachmentButton files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} /> */}
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{showInputEstimatedTokens && (
|
||||
<TextCount>
|
||||
<Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
|
||||
<Tag
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '2px 8px',
|
||||
borderWidth: 0.5
|
||||
}}>
|
||||
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
|
||||
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
|
||||
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑{inputTokenCount}
|
||||
<span style={{ margin: '0 2px' }}>/</span>
|
||||
{estimateTokenCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</TextCount>
|
||||
)}
|
||||
<TokenCount
|
||||
estimateTokenCount={estimateTokenCount}
|
||||
inputTokenCount={inputTokenCount}
|
||||
contextCount={contextCount}
|
||||
ToolbarButton={ToolbarButton}
|
||||
onClick={onNewContext}
|
||||
/>
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{generating && (
|
||||
@@ -221,28 +281,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
ref={inputRef}
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
style={{ fontSize }}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const TextareaStyle: CSSProperties = {
|
||||
paddingLeft: 0,
|
||||
padding: '10px 15px 8px'
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: var(--input-bar-height);
|
||||
border: 1px solid var(--color-border-soft);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
@@ -257,11 +305,11 @@ const Textarea = styled(TextArea)`
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 0 15px 5px 15px;
|
||||
font-family: Ubuntu;
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
@@ -269,8 +317,9 @@ const Toolbar = styled.div`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 4px;
|
||||
height: 36px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
@@ -281,31 +330,30 @@ const ToolbarMenu = styled.div`
|
||||
`
|
||||
|
||||
const ToolbarButton = styled(Button)`
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 18px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 17px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
&.anticon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
&.anticon,
|
||||
&.iconfont {
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon {
|
||||
.anticon,
|
||||
.iconfont {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TextCount = styled.div`
|
||||
font-size: 11px;
|
||||
color: var(--color-text-3);
|
||||
z-index: 10;
|
||||
padding: 2px;
|
||||
border-top-left-radius: 7px;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
|
||||
83
src/renderer/src/pages/home/Inputbar/TokenCount.tsx
Normal file
83
src/renderer/src/pages/home/Inputbar/TokenCount.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ArrowUpOutlined, MenuOutlined, PicCenterOutlined } from '@ant-design/icons'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Divider, Popover, Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Props = {
|
||||
estimateTokenCount: number
|
||||
inputTokenCount: number
|
||||
contextCount: number
|
||||
ToolbarButton: any
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount, ToolbarButton, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
const { showInputEstimatedTokens } = useSettings()
|
||||
|
||||
if (!showInputEstimatedTokens) {
|
||||
return null
|
||||
}
|
||||
|
||||
const PopoverContent = () => {
|
||||
return (
|
||||
<VStack w="150px" background="100%">
|
||||
<HStack justifyContent="space-between" w="100%">
|
||||
<Text>{t('chat.input.context_count.tip')}</Text>
|
||||
<Text>{contextCount}</Text>
|
||||
</HStack>
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
<HStack justifyContent="space-between" w="100%">
|
||||
<Text>{t('chat.input.estimated_tokens.tip')}</Text>
|
||||
<Text>{estimateTokenCount}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarButton type="text" onClick={props.onClick}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||
<PicCenterOutlined />
|
||||
</Tooltip>
|
||||
</ToolbarButton>
|
||||
<Container {...props}>
|
||||
<Popover content={PopoverContent} title="" mouseEnterDelay={0.6}>
|
||||
<MenuOutlined /> {contextCount}
|
||||
<Divider type="vertical" style={{ marginTop: 0, marginLeft: 5, marginRight: 5 }} />
|
||||
<ArrowUpOutlined />
|
||||
{inputTokenCount} / {estimateTokenCount}
|
||||
</Popover>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
color: var(--color-text-2);
|
||||
z-index: 10;
|
||||
padding: 3px 10px;
|
||||
user-select: none;
|
||||
font-family: Ubuntu;
|
||||
border: 0.5px solid var(--color-text-3);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.anticon {
|
||||
font-size: 10px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
`
|
||||
|
||||
const Text = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
export default TokenCount
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CheckOutlined } from '@ant-design/icons'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
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'
|
||||
@@ -37,7 +37,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
|
||||
}
|
||||
|
||||
return match ? (
|
||||
<div>
|
||||
<>
|
||||
<CodeHeader>
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
{!copied && <CopyIcon className="copy" onClick={onCopy} />}
|
||||
@@ -48,10 +48,15 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
|
||||
language={match[1]}
|
||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||
wrapLongLines={true}
|
||||
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
|
||||
customStyle={{
|
||||
border: '0.5px solid var(--color-code-background)',
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
marginTop: 0
|
||||
}}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
@@ -67,7 +72,7 @@ const CodeHeader = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
background-color: var(--color-code-background);
|
||||
height: 40px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
|
||||
@@ -7,16 +7,19 @@ import {
|
||||
SaveOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons'
|
||||
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||
import { startMinAppById } from '@renderer/config/minapp'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useRuntime } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Alert, Avatar, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import { Alert, Avatar, Divider, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
@@ -25,6 +28,7 @@ import styled from 'styled-components'
|
||||
|
||||
import SelectModelDropdown from '../components/SelectModelDropdown'
|
||||
import Markdown from '../Markdown/Markdown'
|
||||
import MessageAttachments from './MessageAttachments'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -37,7 +41,8 @@ interface Props {
|
||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||
const avatar = useAvatar()
|
||||
const { t } = useTranslation()
|
||||
const { assistant, model, setModel } = useAssistant(message.assistantId)
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(message.modelId)
|
||||
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
||||
const { generating } = useRuntime()
|
||||
const [copied, setCopied] = useState(false)
|
||||
@@ -67,9 +72,9 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
|
||||
const getUserName = useCallback(() => {
|
||||
if (message.id === 'assistant') return assistant?.name
|
||||
if (message.role === 'assistant') return upperFirst(model.name || model.id)
|
||||
if (message.role === 'assistant') return upperFirst(model?.name || model?.id)
|
||||
return userName || t('common.you')
|
||||
}, [assistant?.name, message.id, message.role, model.id, model.name, t, userName])
|
||||
}, [assistant?.name, message.id, message.role, model?.id, model?.name, t, userName])
|
||||
|
||||
const fontFamily = useMemo(() => {
|
||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||
@@ -107,27 +112,51 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
if (message.status === 'error') {
|
||||
return (
|
||||
<Alert
|
||||
message={t('error.chat.response')}
|
||||
message={<div style={{ fontSize: 14 }}>{t('error.chat.response')}</div>}
|
||||
description={<Markdown message={message} />}
|
||||
type="error"
|
||||
style={{ marginBottom: 15 }}
|
||||
style={{ marginBottom: 15, padding: 10, fontSize: 12 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <Markdown message={message} />
|
||||
}, [message])
|
||||
return (
|
||||
<>
|
||||
<Markdown message={message} />
|
||||
<MessageAttachments message={message} />
|
||||
</>
|
||||
)
|
||||
}, [message, t])
|
||||
|
||||
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
|
||||
|
||||
if (message.type === 'clear') {
|
||||
return (
|
||||
<Divider dashed style={{ padding: '0 20px' }} plain>
|
||||
{t('chat.message.new.context')}
|
||||
</Divider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageContainer key={message.id} className="message">
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{isAssistantMessage ? (
|
||||
<Avatar src={avatarSource} size={35} style={{ borderRadius: '20%' }}>
|
||||
<Avatar
|
||||
src={avatarSource}
|
||||
size={35}
|
||||
style={{ borderRadius: '20%', cursor: 'pointer' }}
|
||||
onClick={showMiniApp}>
|
||||
{avatarName}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} style={{ borderRadius: '20%' }} />
|
||||
<Avatar
|
||||
src={avatar}
|
||||
size={35}
|
||||
style={{ borderRadius: '20%', cursor: 'pointer' }}
|
||||
onClick={() => UserPopup.show()}
|
||||
/>
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{username}</UserName>
|
||||
@@ -154,7 +183,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
|
||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topLeft">
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<SyncOutlined />
|
||||
|
||||
25
src/renderer/src/pages/home/Messages/MessageAttachments.tsx
Normal file
25
src/renderer/src/pages/home/Messages/MessageAttachments.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Message } from '@renderer/types'
|
||||
import { Image as AntdImage } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const MessageAttachments: FC<Props> = ({ message }) => {
|
||||
return <Container>{message.images?.map((image) => <Image src={image} key={image} width="33%" />)}</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const Image = styled(AntdImage)`
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
export default MessageAttachments
|
||||
@@ -3,7 +3,7 @@ import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
||||
import { getTopic } from '@renderer/hooks/useTopic'
|
||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { estimateHistoryTokenCount, filterMessages } from '@renderer/services/messages'
|
||||
import { estimateHistoryTokenCount, filterMessages, getContextCount } from '@renderer/services/messages'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
|
||||
@@ -20,9 +20,10 @@ import Prompt from './Prompt'
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
topic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||
const provider = useProviderByAssistant(assistant)
|
||||
@@ -33,7 +34,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
(message: Message) => {
|
||||
const _messages = [...messages, message]
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, { ...topic, messages: _messages })
|
||||
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages })
|
||||
},
|
||||
[messages, topic]
|
||||
)
|
||||
@@ -42,9 +43,13 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
const _topic = getTopic(assistant, topic.id)
|
||||
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
summaryText && updateTopic({ ..._topic, name: summaryText })
|
||||
if (summaryText) {
|
||||
const data = { ..._topic, name: summaryText }
|
||||
setActiveTopic(data)
|
||||
updateTopic(data)
|
||||
}
|
||||
}, [assistant, messages, topic, updateTopic])
|
||||
}
|
||||
}, [assistant, messages, setActiveTopic, topic.id, updateTopic])
|
||||
|
||||
const onDeleteMessage = useCallback(
|
||||
(message: Message) => {
|
||||
@@ -84,6 +89,28 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
setMessages([])
|
||||
updateTopic({ ...topic, messages: [] })
|
||||
LocalStorage.clearTopicMessages(topic.id)
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => {
|
||||
const lastMessage = last(messages)
|
||||
|
||||
if (lastMessage && lastMessage.type === 'clear') {
|
||||
return
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
onSendMessage({
|
||||
id: uuid(),
|
||||
assistantId: assistant.id,
|
||||
role: 'user',
|
||||
content: '',
|
||||
topicId: topic.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'success',
|
||||
type: 'clear'
|
||||
} as Message)
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
@@ -101,7 +128,10 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, {
|
||||
tokensCount: estimateHistoryTokenCount(assistant, messages),
|
||||
contextCount: getContextCount(assistant, messages)
|
||||
})
|
||||
}, [assistant, messages])
|
||||
|
||||
return (
|
||||
@@ -117,6 +147,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
138
src/renderer/src/pages/home/Navbar.tsx
Normal file
138
src/renderer/src/pages/home/Navbar.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
|
||||
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { syncAsistantToAgent } from '@renderer/services/assistant'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Switch } from 'antd'
|
||||
import { FC, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
}
|
||||
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant }) => {
|
||||
const { assistant, updateAssistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { topicPosition } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
|
||||
const onCreateAssistant = async () => {
|
||||
const assistant = await AddAssistantPopup.show()
|
||||
assistant && setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
const onEditAssistant = useCallback(async () => {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||
updateAssistant(_assistant)
|
||||
syncAsistantToAgent(_assistant)
|
||||
}, [assistant, updateAssistant])
|
||||
|
||||
return (
|
||||
<Navbar>
|
||||
{showAssistants && (
|
||||
<NavbarLeft
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
borderRight: 'none',
|
||||
padding: '0 8px',
|
||||
width: topicPosition === 'left' ? '300px' : 'var(--assistants-width)'
|
||||
}}>
|
||||
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
||||
<i className="iconfont icon-hide-sidebar" />
|
||||
</NewButton>
|
||||
<NewButton onClick={onCreateAssistant}>
|
||||
<i className="iconfont icon-a-addchat" />
|
||||
</NewButton>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
|
||||
<HStack alignItems="center">
|
||||
{!showAssistants && (
|
||||
<NewButton
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
|
||||
<i className="iconfont icon-show-sidebar" />
|
||||
</NewButton>
|
||||
)}
|
||||
<TitleText style={{ marginRight: 10, cursor: 'pointer' }} className="nodrag" onClick={onEditAssistant}>
|
||||
{assistant.name}
|
||||
</TitleText>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center">
|
||||
<ThemeSwitch
|
||||
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
|
||||
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
|
||||
checked={theme === 'dark'}
|
||||
onChange={toggleTheme}
|
||||
/>
|
||||
{topicPosition === 'right' && (
|
||||
<NewButton onClick={toggleShowTopics}>
|
||||
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
||||
</NewButton>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
export const NewButton = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 4px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 19px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 17px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const TitleText = styled.span`
|
||||
margin-left: 5px;
|
||||
font-family: Ubuntu;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const ThemeSwitch = styled(Switch)`
|
||||
-webkit-app-region: no-drag;
|
||||
margin-right: 10px;
|
||||
.icon-theme {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
130
src/renderer/src/pages/home/RightSidebar.tsx
Normal file
130
src/renderer/src/pages/home/RightSidebar.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { BarsOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Segmented, SegmentedProps } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Assistants from './Assistants'
|
||||
import Settings from './Settings'
|
||||
import Topics from './Topics'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveAssistant: (assistant: Assistant) => void
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
position: 'left' | 'right'
|
||||
}
|
||||
|
||||
const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, position }) => {
|
||||
const { addAssistant } = useAssistants()
|
||||
const [tab, setTab] = useState<'assistants' | 'topic' | 'settings'>(position === 'left' ? 'assistants' : 'topic')
|
||||
const { topicPosition } = useSettings()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { toggleShowTopics } = useShowTopics()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const borderStyle = '0.5px solid var(--color-border)'
|
||||
const border = position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle }
|
||||
|
||||
const showTab = !(position === 'left' && topicPosition === 'right')
|
||||
const assistantTab = {
|
||||
label: t('common.assistant'),
|
||||
value: 'assistants',
|
||||
icon: <i className="iconfont icon-business-smart-assistant" />
|
||||
}
|
||||
|
||||
const onCreateDefaultAssistant = () => {
|
||||
const assistant = { ...defaultAssistant, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
|
||||
showTab && setTab('assistants')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
|
||||
showTab && setTab('topic')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
|
||||
showTab && setTab('settings')
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
|
||||
showTab && setTab('topic')
|
||||
if (position === 'left' && topicPosition === 'right') {
|
||||
toggleShowTopics()
|
||||
}
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [position, showTab, tab, toggleShowTopics, topicPosition])
|
||||
|
||||
return (
|
||||
<Container style={{ ...border, width: topicPosition === 'left' ? '300px' : 'var(--assistants-width)' }}>
|
||||
{showTab && (
|
||||
<Segmented
|
||||
value={tab}
|
||||
className="segmented-tab"
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
padding: '10px',
|
||||
gap: 5,
|
||||
borderBottom: '0.5px solid var(--color-border)'
|
||||
}}
|
||||
options={
|
||||
[
|
||||
position === 'left' && topicPosition === 'left' ? assistantTab : undefined,
|
||||
{ label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> },
|
||||
{ label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> }
|
||||
].filter(Boolean) as SegmentedProps['options']
|
||||
}
|
||||
onChange={(value) => setTab(value as 'topic' | 'settings')}
|
||||
block
|
||||
/>
|
||||
)}
|
||||
<TabContent>
|
||||
{tab === 'assistants' && (
|
||||
<Assistants
|
||||
activeAssistant={activeAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
onCreateAssistant={onCreateDefaultAssistant}
|
||||
/>
|
||||
)}
|
||||
{tab === 'topic' && (
|
||||
<Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
)}
|
||||
{tab === 'settings' && <Settings assistant={activeAssistant} />}
|
||||
</TabContent>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--topic-list-width);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
.collapsed {
|
||||
width: 0;
|
||||
border-left: none;
|
||||
}
|
||||
`
|
||||
|
||||
const TabContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
export default RightSidebar
|
||||
@@ -1,96 +0,0 @@
|
||||
import { BarsOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Segmented } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SettingsTab from './SettingsTab'
|
||||
import TopicsTab from './TopicsTab'
|
||||
|
||||
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])
|
||||
|
||||
if (!rightSidebarShown) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Segmented
|
||||
value={tab}
|
||||
style={{ borderRadius: 0, padding: '10px', gap: 5, borderBottom: '0.5px solid var(--color-border)' }}
|
||||
options={[
|
||||
{ label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> },
|
||||
{ label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> }
|
||||
]}
|
||||
block
|
||||
onChange={(value) => setTab(value as 'topic' | 'settings')}
|
||||
/>
|
||||
<TabContent>
|
||||
{tab === 'topic' && <TopicsTab {...props} />}
|
||||
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
|
||||
</TabContent>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--topic-list-width);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
.collapsed {
|
||||
width: 0;
|
||||
border-left: none;
|
||||
}
|
||||
`
|
||||
|
||||
const TabContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
export default RightSidebar
|
||||
@@ -87,7 +87,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SettingSubtitle>
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>
|
||||
{t('settings.messages.model.title')}{' '}
|
||||
<Tooltip title={t('chat.settings.reset')}>
|
||||
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
|
||||
@@ -234,7 +234,9 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 0 15px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 10px;
|
||||
padding: 10px 15px;
|
||||
`
|
||||
|
||||
const Label = styled.p`
|
||||
@@ -1,13 +1,13 @@
|
||||
import { DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { CloseOutlined, DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { findIndex } from 'lodash'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -18,11 +18,33 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
|
||||
const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
|
||||
const { assistant, removeTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
|
||||
const { t } = useTranslation()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const onDeleteTopic = useCallback(
|
||||
(topic: Topic) => {
|
||||
if (assistant.topics.length > 1) {
|
||||
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
|
||||
removeTopic(topic)
|
||||
}
|
||||
},
|
||||
[assistant.topics, removeTopic, setActiveTopic]
|
||||
)
|
||||
|
||||
const onSwitchTopic = useCallback(
|
||||
(topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
},
|
||||
[generating, setActiveTopic, t]
|
||||
)
|
||||
|
||||
const getTopicMenuItems = useCallback(
|
||||
(topic: Topic) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
@@ -64,70 +86,39 @@ const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTop
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
removeTopic(topic)
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
onClick: () => onDeleteTopic(topic)
|
||||
})
|
||||
}
|
||||
|
||||
return menus
|
||||
},
|
||||
[assistant, removeTopic, setActiveTopic, t, updateTopic]
|
||||
)
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
|
||||
}
|
||||
},
|
||||
[assistant.topics, updateTopics]
|
||||
)
|
||||
|
||||
const onSwitchTopic = useCallback(
|
||||
(topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
},
|
||||
[generating, setActiveTopic, t]
|
||||
[assistant, onDeleteTopic, t, updateTopic]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<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}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
||||
<DragableList list={assistant.topics} onUpdate={updateTopics}>
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
return (
|
||||
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
|
||||
<TopicListItem
|
||||
className={topic.id === activeTopic?.id ? 'active' : ''}
|
||||
onClick={() => onSwitchTopic(topic)}>
|
||||
{topic.name}
|
||||
<TopicListItem className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)}>
|
||||
<TopicName>{topic.name}</TopicName>
|
||||
{assistant.topics.length > 1 && (
|
||||
<MenuButton
|
||||
className="menu"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDeleteTopic(topic)
|
||||
}}>
|
||||
<CloseOutlined />
|
||||
</MenuButton>
|
||||
)}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)
|
||||
}}
|
||||
</DragableList>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -136,23 +127,65 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 10px 10px;
|
||||
padding-top: 10px;
|
||||
overflow-y: scroll;
|
||||
max-height: calc(100vh - var(--navbar-height) - 140px);
|
||||
`
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 7px 10px;
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: Ubuntu;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.menu {
|
||||
opacity: 0;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
font-weight: 500;
|
||||
.menu {
|
||||
opacity: 1;
|
||||
background-color: var(--color-background-mute);
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default TopicsTab
|
||||
const TopicName = styled.div`
|
||||
color: var(--color-text);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
`
|
||||
|
||||
const MenuButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 5px;
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export default Topics
|
||||
@@ -1,4 +1,5 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
@@ -17,8 +18,12 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||
const { model, setModel } = useAssistant(assistant.id)
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isLocalAi) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectModelDropdown model={model} onSelect={setModel}>
|
||||
<SelectModelDropdown model={model} onSelect={setModel} placement="top">
|
||||
<DropdownButton size="small" type="default">
|
||||
<ModelAvatar model={model} size={20} />
|
||||
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/model'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
|
||||
import { first, reverse, sortBy, upperFirst } from 'lodash'
|
||||
@@ -8,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends DropdownProps {
|
||||
model: Model
|
||||
model?: Model
|
||||
onSelect: (model: Model) => void
|
||||
}
|
||||
|
||||
@@ -23,9 +24,9 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: reverse(sortBy(p.models, 'name')).map((m) => ({
|
||||
key: m?.id,
|
||||
key: getModelUniqId(m),
|
||||
label: upperFirst(m?.name),
|
||||
defaultSelectedKeys: [model?.id],
|
||||
defaultSelectedKeys: model ? [getModelUniqId(model)] : [],
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
||||
{first(m?.name)}
|
||||
@@ -37,7 +38,11 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' }, selectedKeys: [model?.id] }}
|
||||
menu={{
|
||||
items,
|
||||
style: { maxHeight: '55vh', overflow: 'auto' },
|
||||
selectedKeys: model ? [getModelUniqId(model)] : []
|
||||
}}
|
||||
trigger={['click']}
|
||||
arrow
|
||||
placement="bottom"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { Avatar, Button, Progress, Row, Tag } from 'antd'
|
||||
import { ProgressInfo } from 'electron-updater'
|
||||
@@ -44,7 +44,7 @@ const AboutSettings: FC = () => {
|
||||
|
||||
const mailto = async () => {
|
||||
const email = 'kangfenmao@qq.com'
|
||||
const subject = 'Cherry Studio Feedback'
|
||||
const subject = `${APP_NAME} 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}`
|
||||
@@ -117,10 +117,10 @@ const AboutSettings: FC = () => {
|
||||
strokeColor="#67ad5b"
|
||||
/>
|
||||
)}
|
||||
<Avatar src={Logo} size={80} style={{ minHeight: 80 }} />
|
||||
<Avatar src={AppLogo} size={80} style={{ minHeight: 80 }} />
|
||||
</AvatarWrapper>
|
||||
<VersionWrapper>
|
||||
<Title>Cherry Studio</Title>
|
||||
<Title>{APP_NAME}</Title>
|
||||
<Description>{t('settings.about.description')}</Description>
|
||||
<Tag
|
||||
onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { backup, reset, restore } from '@renderer/services/backup'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
|
||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
||||
import { Avatar, Button, Input, Select, Upload } from 'antd'
|
||||
import { isValidProxyUrl } from '@renderer/utils'
|
||||
import { Button, Input, Select } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const avatar = useAvatar()
|
||||
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
|
||||
const {
|
||||
language,
|
||||
proxyUrl: storeProxyUrl,
|
||||
userName,
|
||||
theme,
|
||||
windowStyle,
|
||||
topicPosition,
|
||||
setTheme,
|
||||
setWindowStyle,
|
||||
setTopicPosition
|
||||
} = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
@@ -48,7 +53,7 @@ const GeneralSettings: FC = () => {
|
||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={language || 'en-US'}
|
||||
style={{ width: 120 }}
|
||||
style={{ width: 180 }}
|
||||
onChange={onSelectLanguage}
|
||||
options={[
|
||||
{ value: 'zh-CN', label: '中文' },
|
||||
@@ -61,7 +66,7 @@ const GeneralSettings: FC = () => {
|
||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={theme}
|
||||
style={{ width: 120 }}
|
||||
style={{ width: 180 }}
|
||||
onChange={setTheme}
|
||||
options={[
|
||||
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
||||
@@ -72,24 +77,29 @@ const GeneralSettings: FC = () => {
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
|
||||
<Upload
|
||||
customRequest={() => {}}
|
||||
accept="image/png, image/jpeg"
|
||||
itemRender={() => null}
|
||||
maxCount={1}
|
||||
onChange={async ({ file }) => {
|
||||
try {
|
||||
const _file = file.originFileObj as File
|
||||
const compressedFile = await compressImage(_file)
|
||||
await LocalStorage.storeImage('avatar', compressedFile)
|
||||
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
|
||||
} catch (error: any) {
|
||||
window.message.error(error.message)
|
||||
}
|
||||
}}>
|
||||
<UserAvatar src={avatar} size="large" />
|
||||
</Upload>
|
||||
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={windowStyle || 'opaque'}
|
||||
style={{ width: 180 }}
|
||||
onChange={setWindowStyle}
|
||||
options={[
|
||||
{ value: 'transparent', label: t('settings.theme.window.style.transparent') },
|
||||
{ value: 'opaque', label: t('settings.theme.window.style.opaque') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||
<Select
|
||||
defaultValue={topicPosition || 'right'}
|
||||
style={{ width: 180 }}
|
||||
onChange={setTopicPosition}
|
||||
options={[
|
||||
{ value: 'left', label: t('settings.topic.position.left') },
|
||||
{ value: 'right', label: t('settings.topic.position.right') }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
@@ -98,7 +108,7 @@ const GeneralSettings: FC = () => {
|
||||
placeholder={t('settings.general.user_name.placeholder')}
|
||||
value={userName}
|
||||
onChange={(e) => dispatch(setUserName(e.target.value))}
|
||||
style={{ width: 150 }}
|
||||
style={{ width: 180 }}
|
||||
maxLength={30}
|
||||
/>
|
||||
</SettingRow>
|
||||
@@ -109,7 +119,7 @@ const GeneralSettings: FC = () => {
|
||||
placeholder="socks5://127.0.0.1:6153"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
style={{ width: 180 }}
|
||||
onBlur={() => onSetProxyUrl()}
|
||||
type="url"
|
||||
/>
|
||||
@@ -117,12 +127,12 @@ const GeneralSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button onClick={backup} icon={<SaveOutlined />}>
|
||||
备份
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
||||
恢复
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
@@ -140,8 +150,4 @@ const GeneralSettings: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const UserAvatar = styled(Avatar)`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export default GeneralSettings
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId, hasModel } from '@renderer/services/model'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Select } from 'antd'
|
||||
import { find, sortBy, upperFirst } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import { FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingTitle } from '.'
|
||||
@@ -23,11 +24,24 @@ const ModelSettings: FC = () => {
|
||||
title: p.name,
|
||||
options: sortBy(p.models, 'name').map((m) => ({
|
||||
label: upperFirst(m.name),
|
||||
value: m.id
|
||||
value: getModelUniqId(m)
|
||||
}))
|
||||
}))
|
||||
|
||||
const iconStyle = { fontSize: 16, marginRight: 8 }
|
||||
const defaultModelValue = useMemo(
|
||||
() => (hasModel(defaultModel) ? getModelUniqId(defaultModel) : undefined),
|
||||
[defaultModel]
|
||||
)
|
||||
|
||||
const defaultTopicNamingModel = useMemo(
|
||||
() => (hasModel(topicNamingModel) ? getModelUniqId(topicNamingModel) : undefined),
|
||||
[topicNamingModel]
|
||||
)
|
||||
|
||||
const defaultTranslateModel = useMemo(
|
||||
() => (hasModel(translateModel) ? getModelUniqId(translateModel) : undefined),
|
||||
[translateModel]
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
@@ -39,10 +53,12 @@ const ModelSettings: FC = () => {
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={defaultModel.id}
|
||||
value={defaultModelValue}
|
||||
defaultValue={defaultModelValue}
|
||||
style={{ width: 360 }}
|
||||
onChange={(id) => setDefaultModel(find(allModels, { id }) as Model)}
|
||||
onChange={(value) => setDefaultModel(find(allModels, JSON.parse(value)) as Model)}
|
||||
options={selectOptions}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
<div style={{ height: 30 }} />
|
||||
<SettingTitle>
|
||||
@@ -53,10 +69,12 @@ const ModelSettings: FC = () => {
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={topicNamingModel.id}
|
||||
value={defaultTopicNamingModel}
|
||||
defaultValue={defaultTopicNamingModel}
|
||||
style={{ width: 360 }}
|
||||
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
|
||||
onChange={(value) => setTopicNamingModel(find(allModels, JSON.parse(value)) as Model)}
|
||||
options={selectOptions}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
<div style={{ height: 30 }} />
|
||||
<SettingTitle>
|
||||
@@ -67,9 +85,10 @@ const ModelSettings: FC = () => {
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={translateModel?.id}
|
||||
value={defaultTranslateModel}
|
||||
defaultValue={defaultTranslateModel}
|
||||
style={{ width: 360 }}
|
||||
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
|
||||
onChange={(value) => setTranslateModel(find(allModels, JSON.parse(value)) as Model)}
|
||||
options={selectOptions}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
@@ -77,4 +96,6 @@ const ModelSettings: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const iconStyle = { fontSize: 16, marginRight: 8 }
|
||||
|
||||
export default ModelSettings
|
||||
|
||||
@@ -67,7 +67,8 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
afterClose={onClose}
|
||||
footer={null}>
|
||||
footer={null}
|
||||
centered>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ flex: '110px' }}
|
||||
|
||||
@@ -38,6 +38,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
afterClose={onClose}
|
||||
width={360}
|
||||
closable={false}
|
||||
centered
|
||||
title={t('settings.provider.edit.name')}
|
||||
okButtonProps={{ disabled: buttonDisabled }}>
|
||||
<Input
|
||||
|
||||
@@ -107,7 +107,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
styles={{
|
||||
content: { padding: 0 },
|
||||
header: { padding: 22, paddingBottom: 15 }
|
||||
}}>
|
||||
}}
|
||||
centered>
|
||||
<SearchContainer>
|
||||
<Search placeholder={t('settings.provider.search_placeholder')} allowClear onSearch={setSearchText} />
|
||||
</SearchContainer>
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { checkApi } from '@renderer/services/api'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { getProviderLogo } from '@renderer/config/provider'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
@@ -72,6 +71,7 @@ const ProvidersList: FC = () => {
|
||||
content: t('settings.provider.delete.content'),
|
||||
okButtonProps: { danger: true },
|
||||
okText: t('common.delete'),
|
||||
centered: true,
|
||||
onOk: () => {
|
||||
setSelectedProvider(providers.filter((p) => p.isSystem)[0])
|
||||
removeProvider(provider)
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import {
|
||||
CloudOutlined,
|
||||
CodeSandboxOutlined,
|
||||
InfoCircleOutlined,
|
||||
MessageOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { CloudOutlined, InfoCircleOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom'
|
||||
@@ -30,6 +25,8 @@ const SettingsPage: FC = () => {
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<SettingMenus>
|
||||
{!isLocalAi && (
|
||||
<>
|
||||
<MenuItemLink to="/settings/provider">
|
||||
<MenuItem className={isRoute('/settings/provider')}>
|
||||
<CloudOutlined />
|
||||
@@ -38,10 +35,12 @@ const SettingsPage: FC = () => {
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/model">
|
||||
<MenuItem className={isRoute('/settings/model')}>
|
||||
<CodeSandboxOutlined />
|
||||
<i className="iconfont icon-ai-model" />
|
||||
{t('settings.model')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
</>
|
||||
)}
|
||||
<MenuItemLink to="/settings/assistant">
|
||||
<MenuItem className={isRoute('/settings/assistant')}>
|
||||
<MessageOutlined />
|
||||
@@ -117,6 +116,11 @@ const MenuItem = styled.li`
|
||||
font-size: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
opacity: 0.7;
|
||||
margin-left: -1px;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Divider } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SettingsPage from './SettingsPage'
|
||||
|
||||
export const SettingContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -69,5 +67,3 @@ export const SettingHelpLink = styled(Link)`
|
||||
font-size: 11px;
|
||||
padding: 0 5px;
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/api'
|
||||
import { getDefaultAssistant } from '@renderer/services/assistant'
|
||||
@@ -133,6 +134,31 @@ const TranslatePage: FC = () => {
|
||||
isEmpty(text) && setResult('')
|
||||
}, [text])
|
||||
|
||||
const SettingButton = () => {
|
||||
if (isLocalAi) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (translateModel) {
|
||||
return (
|
||||
<Link to="/settings/model" style={{ color: 'var(--color-text-2)' }}>
|
||||
<SettingOutlined />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
@@ -165,21 +191,7 @@ const TranslatePage: FC = () => {
|
||||
</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>
|
||||
)}
|
||||
<SettingButton />
|
||||
</MenuContainer>
|
||||
<TranslateInputWrapper>
|
||||
<InputContainer>
|
||||
|
||||
40
src/renderer/src/providers/AiProvider.ts
Normal file
40
src/renderer/src/providers/AiProvider.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import BaseProvider from '@renderer/providers/BaseProvider'
|
||||
import ProviderFactory from '@renderer/providers/ProviderFactory'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
export default class AiProvider {
|
||||
private sdk: BaseProvider
|
||||
|
||||
constructor(provider: Provider) {
|
||||
this.sdk = ProviderFactory.create(provider)
|
||||
}
|
||||
|
||||
public async completions(
|
||||
messages: Message[],
|
||||
assistant: Assistant,
|
||||
onChunk: ({ text, usage }: { text?: string; usage?: OpenAI.Completions.CompletionUsage }) => void
|
||||
): Promise<void> {
|
||||
return this.sdk.completions(messages, assistant, onChunk)
|
||||
}
|
||||
|
||||
public async translate(message: Message, assistant: Assistant): Promise<string> {
|
||||
return this.sdk.translate(message, assistant)
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
|
||||
return this.sdk.summaries(messages, assistant)
|
||||
}
|
||||
|
||||
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
|
||||
return this.sdk.suggestions(messages, assistant)
|
||||
}
|
||||
|
||||
public async check(): Promise<{ valid: boolean; error: Error | null }> {
|
||||
return this.sdk.check()
|
||||
}
|
||||
|
||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||
return this.sdk.models()
|
||||
}
|
||||
}
|
||||
152
src/renderer/src/providers/AnthropicProvider.ts
Normal file
152
src/renderer/src/providers/AnthropicProvider.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES } from '@renderer/services/event'
|
||||
import { filterContextMessages, filterMessages } from '@renderer/services/messages'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import { first, sum, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
export default class AnthropicProvider extends BaseProvider {
|
||||
private sdk: Anthropic
|
||||
|
||||
constructor(provider: Provider) {
|
||||
super(provider)
|
||||
this.sdk = new Anthropic({ apiKey: provider.apiKey, baseURL: this.getBaseURL() })
|
||||
}
|
||||
|
||||
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, maxTokens } = getAssistantSettings(assistant)
|
||||
|
||||
const userMessages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 2))).map((message) => {
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}
|
||||
})
|
||||
|
||||
if (first(userMessages)?.role === 'assistant') {
|
||||
userMessages.shift()
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = this.sdk.messages
|
||||
.stream({
|
||||
model: model.id,
|
||||
messages: userMessages.filter(Boolean) as MessageParam[],
|
||||
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
system: assistant.prompt,
|
||||
stream: true
|
||||
})
|
||||
.on('text', (text) => {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||
resolve()
|
||||
return stream.controller.abort()
|
||||
}
|
||||
onChunk({ text })
|
||||
})
|
||||
.on('finalMessage', (message) => {
|
||||
onChunk({
|
||||
text: '',
|
||||
usage: {
|
||||
prompt_tokens: message.usage.input_tokens,
|
||||
completion_tokens: message.usage.output_tokens,
|
||||
total_tokens: sum(Object.values(message.usage))
|
||||
}
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
.on('error', (error) => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
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 }
|
||||
]
|
||||
|
||||
const response = await this.sdk.messages.create({
|
||||
model: model.id,
|
||||
messages: messages.filter((m) => m.role === 'user') as MessageParam[],
|
||||
max_tokens: 4096,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
system: assistant.prompt,
|
||||
stream: false
|
||||
})
|
||||
|
||||
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
|
||||
const userMessages = takeRight(messages, 5).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
if (first(userMessages)?.role === 'assistant') {
|
||||
userMessages.shift()
|
||||
}
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
}
|
||||
|
||||
const message = await this.sdk.messages.create({
|
||||
messages: userMessages as Anthropic.Messages.MessageParam[],
|
||||
model: model.id,
|
||||
system: systemMessage.content,
|
||||
stream: false,
|
||||
max_tokens: 4096
|
||||
})
|
||||
|
||||
return message.content[0].type === 'text' ? message.content[0].text : null
|
||||
}
|
||||
|
||||
public async suggestions(): Promise<Suggestion[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
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 {
|
||||
const message = await this.sdk.messages.create(body as MessageCreateParamsNonStreaming)
|
||||
return {
|
||||
valid: message.content.length > 0,
|
||||
error: null
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
valid: false,
|
||||
error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||
return []
|
||||
}
|
||||
}
|
||||
33
src/renderer/src/providers/BaseProvider.ts
Normal file
33
src/renderer/src/providers/BaseProvider.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
export default abstract class BaseProvider {
|
||||
protected provider: Provider
|
||||
protected host: string
|
||||
|
||||
constructor(provider: Provider) {
|
||||
this.provider = provider
|
||||
this.host = this.getBaseURL()
|
||||
}
|
||||
|
||||
public getBaseURL(): string {
|
||||
const host = this.provider.apiHost
|
||||
return host.endsWith('/') ? host : `${host}/v1/`
|
||||
}
|
||||
|
||||
public get keepAliveTime() {
|
||||
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
|
||||
}
|
||||
|
||||
abstract completions(
|
||||
messages: Message[],
|
||||
assistant: Assistant,
|
||||
onChunk: ({ text, usage }: { text?: string; usage?: OpenAI.Completions.CompletionUsage }) => void
|
||||
): Promise<void>
|
||||
abstract translate(message: Message, assistant: Assistant): Promise<string>
|
||||
abstract summaries(messages: Message[], assistant: Assistant): Promise<string | null>
|
||||
abstract suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]>
|
||||
abstract check(): Promise<{ valid: boolean; error: Error | null }>
|
||||
abstract models(): Promise<OpenAI.Models.Model[]>
|
||||
}
|
||||
171
src/renderer/src/providers/GeminiProvider.ts
Normal file
171
src/renderer/src/providers/GeminiProvider.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES } from '@renderer/services/event'
|
||||
import { filterContextMessages, filterMessages } from '@renderer/services/messages'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import axios from 'axios'
|
||||
import { isEmpty, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
export default class GeminiProvider extends BaseProvider {
|
||||
private sdk: GoogleGenerativeAI
|
||||
|
||||
constructor(provider: Provider) {
|
||||
super(provider)
|
||||
this.sdk = new GoogleGenerativeAI(provider.apiKey)
|
||||
}
|
||||
|
||||
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, maxTokens } = getAssistantSettings(assistant)
|
||||
|
||||
const userMessages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 1))).map((message) => {
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}
|
||||
})
|
||||
|
||||
const geminiModel = this.sdk.getGenerativeModel({
|
||||
model: model.id,
|
||||
systemInstruction: assistant.prompt,
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature
|
||||
}
|
||||
})
|
||||
|
||||
const userLastMessage = userMessages.pop()
|
||||
|
||||
const chat = geminiModel.startChat({
|
||||
history: userMessages.map((message) => ({
|
||||
role: message.role === 'user' ? 'user' : 'model',
|
||||
parts: [{ text: message.content }]
|
||||
}))
|
||||
})
|
||||
|
||||
const userMessagesStream = await chat.sendMessageStream(userLastMessage?.content!)
|
||||
|
||||
for await (const chunk of userMessagesStream.stream) {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
||||
onChunk({
|
||||
text: chunk.text(),
|
||||
usage: {
|
||||
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async translate(message: Message, assistant: Assistant) {
|
||||
const defaultModel = getDefaultModel()
|
||||
const { maxTokens } = getAssistantSettings(assistant)
|
||||
const model = assistant.model || defaultModel
|
||||
|
||||
const geminiModel = this.sdk.getGenerativeModel({
|
||||
model: model.id,
|
||||
systemInstruction: assistant.prompt,
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature
|
||||
}
|
||||
})
|
||||
|
||||
const { response } = await geminiModel.generateContent(message.content)
|
||||
|
||||
return response.text()
|
||||
}
|
||||
|
||||
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
|
||||
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||
|
||||
const userMessages = takeRight(messages, 5).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
}
|
||||
|
||||
const geminiModel = this.sdk.getGenerativeModel({
|
||||
model: model.id,
|
||||
systemInstruction: systemMessage.content,
|
||||
generationConfig: {
|
||||
temperature: assistant?.settings?.temperature
|
||||
}
|
||||
})
|
||||
|
||||
const lastUserMessage = userMessages.pop()
|
||||
|
||||
const chat = await geminiModel.startChat({
|
||||
history: userMessages.map((message) => ({
|
||||
role: message.role === 'user' ? 'user' : 'model',
|
||||
parts: [{ text: message.content }]
|
||||
}))
|
||||
})
|
||||
|
||||
const { response } = await chat.sendMessage(lastUserMessage?.content!)
|
||||
|
||||
return response.text()
|
||||
}
|
||||
|
||||
public async suggestions(): Promise<Suggestion[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
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 {
|
||||
const geminiModel = this.sdk.getGenerativeModel({ model: body.model })
|
||||
const result = await geminiModel.generateContent(body.messages[0].content)
|
||||
return {
|
||||
valid: !isEmpty(result.response.text()),
|
||||
error: null
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
valid: false,
|
||||
error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||
try {
|
||||
const api = this.provider.apiHost + '/v1beta/models'
|
||||
const { data } = await axios.get(api, { params: { key: this.provider.apiKey } })
|
||||
return data.models.map(
|
||||
(m: any) =>
|
||||
({
|
||||
id: m.name.replace('models/', ''),
|
||||
name: m.displayName,
|
||||
description: m.description,
|
||||
object: 'model',
|
||||
created: Date.now(),
|
||||
owned_by: 'gemini'
|
||||
}) as OpenAI.Models.Model
|
||||
)
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
186
src/renderer/src/providers/OpenAIProvider.ts
Normal file
186
src/renderer/src/providers/OpenAIProvider.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES } from '@renderer/services/event'
|
||||
import { filterContextMessages, filterMessages } from '@renderer/services/messages'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import { fileToBase64, removeQuotes } from '@renderer/utils'
|
||||
import { first, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
import {
|
||||
ChatCompletionContentPart,
|
||||
ChatCompletionCreateParamsNonStreaming,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources'
|
||||
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
export default class OpenAIProvider extends BaseProvider {
|
||||
private sdk: OpenAI
|
||||
|
||||
constructor(provider: Provider) {
|
||||
super(provider)
|
||||
this.sdk = new OpenAI({
|
||||
dangerouslyAllowBrowser: true,
|
||||
apiKey: provider.apiKey,
|
||||
baseURL: this.getBaseURL()
|
||||
})
|
||||
}
|
||||
|
||||
private async getMessageContent(message: Message): Promise<string | ChatCompletionContentPart[]> {
|
||||
const file = first(message.files)
|
||||
|
||||
if (!file) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
if (file.type.includes('image')) {
|
||||
return [
|
||||
{ type: 'text', text: message.content },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: await fileToBase64(file)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return message.content
|
||||
}
|
||||
|
||||
async completions(
|
||||
messages: Message[],
|
||||
assistant: Assistant,
|
||||
onChunk: ({ text, usage }: { text?: string; usage?: OpenAI.Completions.CompletionUsage }) => void
|
||||
): Promise<void> {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount, maxTokens } = getAssistantSettings(assistant)
|
||||
|
||||
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
|
||||
|
||||
const userMessages: ChatCompletionMessageParam[] = []
|
||||
|
||||
for (const message of filterMessages(filterContextMessages(takeRight(messages, contextCount + 1)))) {
|
||||
userMessages.push({
|
||||
role: message.role,
|
||||
content: await this.getMessageContent(message)
|
||||
} as ChatCompletionMessageParam)
|
||||
}
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const stream = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||
stream: true,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
max_tokens: maxTokens,
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
]
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: messages as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
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: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
}
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.sdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...(isLocalAi ? [first(userMessages)] : userMessages)] as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
max_tokens: 50,
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
return removeQuotes(response.choices[0].message?.content?.substring(0, 50) || '')
|
||||
}
|
||||
|
||||
async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
|
||||
const model = assistant.model
|
||||
|
||||
if (!model) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response: any = await this.sdk.request({
|
||||
method: 'post',
|
||||
path: '/advice_questions',
|
||||
body: {
|
||||
messages: messages.filter((m) => m.role === 'user').map((m) => ({ role: m.role, content: m.content })),
|
||||
model: model.id,
|
||||
max_tokens: 0,
|
||||
temperature: 0,
|
||||
n: 0
|
||||
}
|
||||
})
|
||||
|
||||
return response?.questions?.filter(Boolean)?.map((q: any) => ({ content: q })) || []
|
||||
}
|
||||
|
||||
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 {
|
||||
const response = await this.sdk.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 {
|
||||
const response = await this.sdk.models.list()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/renderer/src/providers/ProviderFactory.ts
Normal file
19
src/renderer/src/providers/ProviderFactory.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
|
||||
import AnthropicProvider from './AnthropicProvider'
|
||||
import BaseProvider from './BaseProvider'
|
||||
import GeminiProvider from './GeminiProvider'
|
||||
import OpenAIProvider from './OpenAIProvider'
|
||||
|
||||
export default class ProviderFactory {
|
||||
static create(provider: Provider): BaseProvider {
|
||||
switch (provider.id) {
|
||||
case 'anthropic':
|
||||
return new AnthropicProvider(provider)
|
||||
case 'gemini':
|
||||
return new GeminiProvider(provider)
|
||||
default:
|
||||
return new OpenAIProvider(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai'
|
||||
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeQuotes } from '@renderer/utils'
|
||||
import axios from 'axios'
|
||||
import { isEmpty, sum, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
|
||||
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from './assistant'
|
||||
import { EVENT_NAMES } from './event'
|
||||
|
||||
export default class ProviderSDK {
|
||||
provider: Provider
|
||||
openaiSdk: OpenAI
|
||||
anthropicSdk: Anthropic
|
||||
geminiSdk: GoogleGenerativeAI
|
||||
|
||||
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 })
|
||||
this.geminiSdk = new GoogleGenerativeAI(provider.apiKey)
|
||||
}
|
||||
|
||||
private get isAnthropic() {
|
||||
return this.provider.id === 'anthropic'
|
||||
}
|
||||
|
||||
private get isGemini() {
|
||||
return this.provider.id === 'gemini'
|
||||
}
|
||||
|
||||
private get keepAliveTime() {
|
||||
return this.provider.id === 'ollama' ? getOllamaKeepAliveTime() : undefined
|
||||
}
|
||||
|
||||
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, maxTokens } = 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) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = this.anthropicSdk.messages
|
||||
.stream({
|
||||
model: model.id,
|
||||
messages: userMessages.filter(Boolean) as MessageParam[],
|
||||
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
system: assistant.prompt,
|
||||
stream: true
|
||||
})
|
||||
.on('text', (text) => {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||
resolve()
|
||||
return stream.controller.abort()
|
||||
}
|
||||
onChunk({ text })
|
||||
})
|
||||
.on('finalMessage', (message) => {
|
||||
onChunk({
|
||||
text: '',
|
||||
usage: {
|
||||
prompt_tokens: message.usage.input_tokens,
|
||||
completion_tokens: message.usage.output_tokens,
|
||||
total_tokens: sum(Object.values(message.usage))
|
||||
}
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
.on('error', (error) => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
if (this.isGemini) {
|
||||
const geminiModel = this.geminiSdk.getGenerativeModel({
|
||||
model: model.id,
|
||||
systemInstruction: assistant.prompt,
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature
|
||||
}
|
||||
})
|
||||
|
||||
const userLastMessage = userMessages.pop()
|
||||
|
||||
const chat = geminiModel.startChat({
|
||||
history: userMessages.map((message) => ({
|
||||
role: message.role === 'user' ? 'user' : 'model',
|
||||
parts: [{ text: message.content }]
|
||||
}))
|
||||
})
|
||||
|
||||
const userMessagesStream = await chat.sendMessageStream(userLastMessage?.content!)
|
||||
|
||||
for await (const chunk of userMessagesStream.stream) {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
||||
onChunk({
|
||||
text: chunk.text(),
|
||||
usage: {
|
||||
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const stream = await this.openaiSdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||
stream: true,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
max_tokens: maxTokens,
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
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 { maxTokens } = getAssistantSettings(assistant)
|
||||
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.filter((m) => m.role === 'user') as MessageParam[],
|
||||
max_tokens: 4096,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
system: assistant.prompt,
|
||||
stream: false
|
||||
})
|
||||
|
||||
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||
}
|
||||
|
||||
if (this.isGemini) {
|
||||
const geminiModel = this.geminiSdk.getGenerativeModel({
|
||||
model: model.id,
|
||||
systemInstruction: assistant.prompt,
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature
|
||||
}
|
||||
})
|
||||
|
||||
const { response } = await geminiModel.generateContent(message.content)
|
||||
|
||||
return response.text()
|
||||
}
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.openaiSdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: messages as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
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: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||
}
|
||||
|
||||
if (this.isAnthropic) {
|
||||
const message = await this.anthropicSdk.messages.create({
|
||||
messages: userMessages as Anthropic.Messages.MessageParam[],
|
||||
model: model.id,
|
||||
system: systemMessage.content,
|
||||
stream: false,
|
||||
max_tokens: 4096
|
||||
})
|
||||
|
||||
return message.content[0].type === 'text' ? message.content[0].text : null
|
||||
}
|
||||
|
||||
if (this.isGemini) {
|
||||
const geminiModel = this.geminiSdk.getGenerativeModel({
|
||||
model: model.id,
|
||||
systemInstruction: systemMessage.content,
|
||||
generationConfig: {
|
||||
temperature: assistant?.settings?.temperature
|
||||
}
|
||||
})
|
||||
|
||||
const lastUserMessage = userMessages.pop()
|
||||
|
||||
const chat = await geminiModel.startChat({
|
||||
history: userMessages.map((message) => ({
|
||||
role: message.role === 'user' ? 'user' : 'model',
|
||||
parts: [{ text: message.content }]
|
||||
}))
|
||||
})
|
||||
|
||||
const { response } = await chat.sendMessage(lastUserMessage?.content!)
|
||||
|
||||
return response.text()
|
||||
}
|
||||
|
||||
// @ts-ignore key is not typed
|
||||
const response = await this.openaiSdk.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [systemMessage, ...userMessages] as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
max_tokens: 50,
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
return removeQuotes(response.choices[0].message?.content || '')
|
||||
}
|
||||
|
||||
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
|
||||
const model = assistant.model
|
||||
|
||||
if (!model) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response: any = await this.openaiSdk.request({
|
||||
method: 'post',
|
||||
path: '/advice_questions',
|
||||
body: {
|
||||
messages: messages.filter((m) => m.role === 'user').map((m) => ({ role: m.role, content: m.content })),
|
||||
model: model.id,
|
||||
max_tokens: 0,
|
||||
temperature: 0,
|
||||
n: 0
|
||||
}
|
||||
})
|
||||
|
||||
return response?.questions?.filter(Boolean)?.map((q: any) => ({ content: q })) || []
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isGemini) {
|
||||
const geminiModel = this.geminiSdk.getGenerativeModel({ model: body.model })
|
||||
const result = await geminiModel.generateContent(body.messages[0].content)
|
||||
return {
|
||||
valid: !isEmpty(result.response.text()),
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
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 []
|
||||
}
|
||||
|
||||
if (this.isGemini) {
|
||||
const api = this.provider.apiHost + '/v1beta/models'
|
||||
const { data } = await axios.get(api, { params: { key: this.provider.apiKey } })
|
||||
return data.models.map(
|
||||
(m: any) =>
|
||||
({
|
||||
id: m.name.replace('models/', ''),
|
||||
name: m.displayName,
|
||||
description: m.description,
|
||||
object: 'model',
|
||||
created: Date.now(),
|
||||
owned_by: 'gemini'
|
||||
}) as OpenAI.Models.Model
|
||||
)
|
||||
}
|
||||
|
||||
const response = await this.openaiSdk.models.list()
|
||||
return response.data
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import AiProvider from '../providers/AiProvider'
|
||||
import {
|
||||
getAssistantProvider,
|
||||
getDefaultModel,
|
||||
@@ -15,7 +16,6 @@ import {
|
||||
} from './assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from './event'
|
||||
import { filterMessages } from './messages'
|
||||
import ProviderSDK from './ProviderSDK'
|
||||
|
||||
export async function fetchChatCompletion({
|
||||
messages,
|
||||
@@ -33,7 +33,7 @@ export async function fetchChatCompletion({
|
||||
const provider = getAssistantProvider(assistant)
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
store.dispatch(setGenerating(true))
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function fetchChatCompletion({
|
||||
}, 1000)
|
||||
|
||||
try {
|
||||
await providerSdk.completions(filterMessages(messages), assistant, ({ text, usage }) => {
|
||||
await AI.completions(messages, assistant, ({ text, usage }) => {
|
||||
message.content = message.content + text || ''
|
||||
message.usage = usage
|
||||
onResponse({ ...message, status: 'pending' })
|
||||
@@ -103,10 +103,10 @@ export async function fetchTranslate({ message, assistant }: { message: Message;
|
||||
return ''
|
||||
}
|
||||
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
try {
|
||||
return await providerSdk.translate(message, assistant)
|
||||
return await AI.translate(message, assistant)
|
||||
} catch (error: any) {
|
||||
return ''
|
||||
}
|
||||
@@ -120,10 +120,10 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
|
||||
return null
|
||||
}
|
||||
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
try {
|
||||
return await providerSdk.summaries(filterMessages(messages), assistant)
|
||||
return await AI.summaries(filterMessages(messages), assistant)
|
||||
} catch (error: any) {
|
||||
return null
|
||||
}
|
||||
@@ -136,10 +136,8 @@ export async function fetchSuggestions({
|
||||
messages: Message[]
|
||||
assistant: Assistant
|
||||
}): Promise<Suggestion[]> {
|
||||
console.debug('fetchSuggestions', messages, assistant)
|
||||
const provider = getAssistantProvider(assistant)
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
console.debug('fetchSuggestions', provider)
|
||||
const AI = new AiProvider(provider)
|
||||
const model = assistant.model
|
||||
|
||||
if (!model) {
|
||||
@@ -155,7 +153,7 @@ export async function fetchSuggestions({
|
||||
}
|
||||
|
||||
try {
|
||||
return await providerSdk.suggestions(messages, assistant)
|
||||
return await AI.suggestions(filterMessages(messages), assistant)
|
||||
} catch (error: any) {
|
||||
return []
|
||||
}
|
||||
@@ -166,10 +164,12 @@ export async function checkApi(provider: Provider) {
|
||||
const key = 'api-check'
|
||||
const style = { marginTop: '3vh' }
|
||||
|
||||
if (provider.id !== 'ollama') {
|
||||
if (!provider.apiKey) {
|
||||
window.message.error({ content: i18n.t('message.error.enter.api.key'), key, style })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider.apiHost) {
|
||||
window.message.error({ content: i18n.t('message.error.enter.api.host'), key, style })
|
||||
@@ -181,9 +181,9 @@ export async function checkApi(provider: Provider) {
|
||||
return false
|
||||
}
|
||||
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
const { valid } = await providerSdk.check()
|
||||
const { valid } = await AI.check()
|
||||
|
||||
window.message[valid ? 'success' : 'error']({
|
||||
key: 'api-check',
|
||||
@@ -202,10 +202,10 @@ function hasApiKey(provider: Provider) {
|
||||
}
|
||||
|
||||
export async function fetchModels(provider: Provider) {
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
try {
|
||||
return await providerSdk.models()
|
||||
return await AI.models()
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ export function getDefaultAssistant(): Assistant {
|
||||
export function getDefaultTopic(): Topic {
|
||||
return {
|
||||
id: uuid(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
name: i18n.t('chat.default.topic.name'),
|
||||
messages: []
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function reset() {
|
||||
window.modal.confirm({
|
||||
title: i18n.t('message.reset.double.confirm.title'),
|
||||
content: i18n.t('message.reset.double.confirm.content'),
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await localStorage.clear()
|
||||
await localforage.clear()
|
||||
|
||||
@@ -12,7 +12,9 @@ export const EVENT_NAMES = {
|
||||
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE',
|
||||
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED',
|
||||
ESTIMATED_TOKEN_COUNT: 'ESTIMATED_TOKEN_COUNT',
|
||||
SHOW_ASSISTANTS: 'SHOW_ASSISTANTS',
|
||||
SHOW_CHAT_SETTINGS: 'SHOW_CHAT_SETTINGS',
|
||||
SHOW_TOPIC_SIDEBAR: 'SHOW_TOPIC_SIDEBAR',
|
||||
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR'
|
||||
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR',
|
||||
NEW_CONTEXT: 'NEW_CONTEXT'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
||||
import { Assistant, Message } from '@renderer/types'
|
||||
import { GPTTokens } from 'gpt-tokens'
|
||||
import { isEmpty, takeRight } from 'lodash'
|
||||
@@ -5,7 +6,32 @@ import { isEmpty, takeRight } from 'lodash'
|
||||
import { getAssistantSettings } from './assistant'
|
||||
|
||||
export const filterMessages = (messages: Message[]) => {
|
||||
return messages.filter((message) => message.type !== '@').filter((message) => !isEmpty(message.content.trim()))
|
||||
return messages
|
||||
.filter((message) => !['@', 'clear'].includes(message.type!))
|
||||
.filter((message) => !isEmpty(message.content.trim()))
|
||||
}
|
||||
|
||||
export function filterContextMessages(messages: Message[]): Message[] {
|
||||
const clearIndex = messages.findLastIndex((message) => message.type === 'clear')
|
||||
|
||||
if (clearIndex === -1) {
|
||||
return messages
|
||||
}
|
||||
|
||||
return messages.slice(clearIndex + 1)
|
||||
}
|
||||
|
||||
export function getContextCount(assistant: Assistant, messages: Message[]) {
|
||||
const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
|
||||
const _messages = takeRight(messages, contextCount)
|
||||
const clearIndex = _messages.findLastIndex((message) => message.type === 'clear')
|
||||
const messagesCount = _messages.length
|
||||
|
||||
if (clearIndex === -1) {
|
||||
return contextCount
|
||||
}
|
||||
|
||||
return messagesCount - (clearIndex + 1)
|
||||
}
|
||||
|
||||
export function estimateInputTokenCount(text: string) {
|
||||
@@ -24,7 +50,7 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{ role: 'system', content: assistant.prompt },
|
||||
...filterMessages(takeRight(msgs, contextCount)).map((message) => ({
|
||||
...filterMessages(filterContextMessages(takeRight(msgs, contextCount))).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
|
||||
16
src/renderer/src/services/model.ts
Normal file
16
src/renderer/src/services/model.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import store from '@renderer/store'
|
||||
import { Model } from '@renderer/types'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
export const getModelUniqId = (m?: Model) => {
|
||||
return m?.id ? JSON.stringify(pick(m, ['id', 'provider'])) : ''
|
||||
}
|
||||
|
||||
export const hasModel = (m?: Model) => {
|
||||
const allModels = store
|
||||
.getState()
|
||||
.llm.providers.map((p) => p.models)
|
||||
.flat()
|
||||
|
||||
return allModels.find((model) => model.id === m?.id)
|
||||
}
|
||||
@@ -44,11 +44,14 @@ const assistantsSlice = createSlice({
|
||||
)
|
||||
},
|
||||
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
|
||||
const topic = action.payload.topic
|
||||
topic.createdAt = new Date().toISOString()
|
||||
topic.updatedAt = new Date().toISOString()
|
||||
state.assistants = state.assistants.map((assistant) =>
|
||||
assistant.id === action.payload.assistantId
|
||||
? {
|
||||
...assistant,
|
||||
topics: uniqBy([action.payload.topic, ...assistant.topics], 'id')
|
||||
topics: uniqBy([topic, ...assistant.topics], 'id')
|
||||
}
|
||||
: assistant
|
||||
)
|
||||
@@ -64,13 +67,13 @@ const assistantsSlice = createSlice({
|
||||
)
|
||||
},
|
||||
updateTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
|
||||
const newTopic = action.payload.topic
|
||||
newTopic.updatedAt = new Date().toISOString()
|
||||
state.assistants = state.assistants.map((assistant) =>
|
||||
assistant.id === action.payload.assistantId
|
||||
? {
|
||||
...assistant,
|
||||
topics: assistant.topics.map((topic) =>
|
||||
topic.id === action.payload.topic.id ? action.payload.topic : topic
|
||||
)
|
||||
topics: assistant.topics.map((topic) => (topic.id === newTopic.id ? newTopic : topic))
|
||||
}
|
||||
: assistant
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 22,
|
||||
version: 24,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { uniqBy } from 'lodash'
|
||||
@@ -18,34 +19,52 @@ export interface LlmState {
|
||||
}
|
||||
|
||||
const initialState: LlmState = {
|
||||
defaultModel: SYSTEM_MODELS.openai[0],
|
||||
topicNamingModel: SYSTEM_MODELS.openai[0],
|
||||
translateModel: SYSTEM_MODELS.openai[0],
|
||||
defaultModel: SYSTEM_MODELS.silicon[0],
|
||||
topicNamingModel: SYSTEM_MODELS.silicon[0],
|
||||
translateModel: SYSTEM_MODELS.silicon[0],
|
||||
providers: [
|
||||
{
|
||||
id: 'silicon',
|
||||
name: 'Silicon',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.siliconflow.cn',
|
||||
models: SYSTEM_MODELS.silicon,
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
apiKey: '',
|
||||
apiHost: 'http://localhost:11434/v1/',
|
||||
models: SYSTEM_MODELS.ollama,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.anthropic.com/',
|
||||
models: SYSTEM_MODELS.anthropic,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: SYSTEM_MODELS.openai.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.openai,
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'gemini',
|
||||
name: 'Gemini',
|
||||
apiKey: '',
|
||||
apiHost: 'https://generativelanguage.googleapis.com',
|
||||
models: SYSTEM_MODELS.gemini.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'silicon',
|
||||
name: 'Silicon',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.siliconflow.cn',
|
||||
models: SYSTEM_MODELS.silicon.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.gemini,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -54,7 +73,7 @@ const initialState: LlmState = {
|
||||
name: 'deepseek',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.deepseek.com',
|
||||
models: SYSTEM_MODELS.deepseek.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.deepseek,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -63,7 +82,7 @@ const initialState: LlmState = {
|
||||
name: 'Yi',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.lingyiwanwu.com',
|
||||
models: SYSTEM_MODELS.yi.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.yi,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -72,7 +91,7 @@ const initialState: LlmState = {
|
||||
name: 'ZhiPu',
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.zhipu,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -81,7 +100,7 @@ const initialState: LlmState = {
|
||||
name: 'Moonshot AI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.moonshot.cn',
|
||||
models: SYSTEM_MODELS.moonshot.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.moonshot,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -90,7 +109,7 @@ const initialState: LlmState = {
|
||||
name: 'BAICHUAN AI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.baichuan-ai.com',
|
||||
models: SYSTEM_MODELS.baichuan.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.baichuan,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -99,7 +118,7 @@ const initialState: LlmState = {
|
||||
name: 'DashScope',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
models: SYSTEM_MODELS.dashscope.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.dashscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -108,7 +127,7 @@ const initialState: LlmState = {
|
||||
name: 'StepFun',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.stepfun.com',
|
||||
models: SYSTEM_MODELS.stepfun.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.stepfun,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -117,7 +136,7 @@ const initialState: LlmState = {
|
||||
name: 'doubao',
|
||||
apiKey: '',
|
||||
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
|
||||
models: SYSTEM_MODELS.doubao.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.doubao,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -126,16 +145,7 @@ const initialState: LlmState = {
|
||||
name: 'MiniMax',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.minimax.chat/v1/',
|
||||
models: SYSTEM_MODELS.minimax.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),
|
||||
models: SYSTEM_MODELS.minimax,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -153,7 +163,7 @@ const initialState: LlmState = {
|
||||
name: 'OpenRouter',
|
||||
apiKey: '',
|
||||
apiHost: 'https://openrouter.ai/api/v1/',
|
||||
models: SYSTEM_MODELS.openrouter.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.openrouter,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -162,25 +172,16 @@ const initialState: LlmState = {
|
||||
name: 'Groq',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.groq.com/openai',
|
||||
models: SYSTEM_MODELS.groq.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.groq,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic',
|
||||
id: 'aihubmix',
|
||||
name: 'AiHubMix',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.anthropic.com/',
|
||||
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
apiKey: '',
|
||||
apiHost: 'http://localhost:11434/v1/',
|
||||
models: [],
|
||||
apiHost: 'https://aihubmix.com',
|
||||
models: SYSTEM_MODELS.aihubmix,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
@@ -192,9 +193,35 @@ const initialState: LlmState = {
|
||||
}
|
||||
}
|
||||
|
||||
const getIntegratedInitialState = () => {
|
||||
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)
|
||||
|
||||
return {
|
||||
defaultModel: model,
|
||||
topicNamingModel: model,
|
||||
translateModel: model,
|
||||
providers: [
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
apiKey: 'ollama',
|
||||
apiHost: 'http://localhost:15537/v1/',
|
||||
models: [model],
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
ollama: {
|
||||
keepAliveTime: 3600
|
||||
}
|
||||
}
|
||||
} as LlmState
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: 'llm',
|
||||
initialState,
|
||||
initialState: isLocalAi ? getIntegratedInitialState() : initialState,
|
||||
reducers: {
|
||||
updateProvider: (state, action: PayloadAction<Provider>) => {
|
||||
state.providers = state.providers.map((p) => (p.id === action.payload.id ? { ...p, ...action.payload } : p))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { isEmpty } from 'lodash'
|
||||
import localforage from 'localforage'
|
||||
import { isEmpty, pick } from 'lodash'
|
||||
import { createMigrate } from 'redux-persist'
|
||||
|
||||
import { RootState } from '.'
|
||||
@@ -20,7 +21,7 @@ const migrateConfig = {
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.lingyiwanwu.com',
|
||||
isSystem: true,
|
||||
models: SYSTEM_MODELS.yi.filter((m) => m.enabled)
|
||||
models: SYSTEM_MODELS.yi
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -39,7 +40,7 @@ const migrateConfig = {
|
||||
apiKey: '',
|
||||
apiHost: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
isSystem: true,
|
||||
models: SYSTEM_MODELS.zhipu.filter((m) => m.enabled)
|
||||
models: SYSTEM_MODELS.zhipu
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -77,7 +78,7 @@ const migrateConfig = {
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.moonshot.cn',
|
||||
isSystem: true,
|
||||
models: SYSTEM_MODELS.moonshot.filter((m) => m.enabled)
|
||||
models: SYSTEM_MODELS.moonshot
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -95,7 +96,7 @@ const migrateConfig = {
|
||||
name: 'OpenRouter',
|
||||
apiKey: '',
|
||||
apiHost: 'https://openrouter.ai/api/v1/',
|
||||
models: SYSTEM_MODELS.openrouter.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.openrouter,
|
||||
isSystem: true
|
||||
}
|
||||
]
|
||||
@@ -143,7 +144,7 @@ const migrateConfig = {
|
||||
...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.enabled)
|
||||
provider.models = SYSTEM_MODELS.zhipu
|
||||
}
|
||||
return provider
|
||||
})
|
||||
@@ -162,7 +163,7 @@ const migrateConfig = {
|
||||
name: 'BAICHUAN AI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.baichuan-ai.com',
|
||||
models: SYSTEM_MODELS.baichuan.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.baichuan,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
@@ -182,7 +183,7 @@ const migrateConfig = {
|
||||
name: 'DashScope',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
models: SYSTEM_MODELS.dashscope.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.dashscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -191,7 +192,7 @@ const migrateConfig = {
|
||||
name: 'Anthropic',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.anthropic.com/',
|
||||
models: SYSTEM_MODELS.anthropic.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.anthropic,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
@@ -211,7 +212,7 @@ const migrateConfig = {
|
||||
name: 'AiHubMix',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.aihubmix,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
@@ -309,7 +310,7 @@ const migrateConfig = {
|
||||
name: 'Gemini',
|
||||
apiKey: '',
|
||||
apiHost: 'https://generativelanguage.googleapis.com',
|
||||
models: SYSTEM_MODELS.gemini.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.gemini,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -318,7 +319,7 @@ const migrateConfig = {
|
||||
name: 'StepFun',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.stepfun.com',
|
||||
models: SYSTEM_MODELS.stepfun.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.stepfun,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -327,7 +328,7 @@ const migrateConfig = {
|
||||
name: 'doubao',
|
||||
apiKey: '',
|
||||
apiHost: 'https://ark.cn-beijing.volces.com/api/v3/',
|
||||
models: SYSTEM_MODELS.doubao.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.doubao,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
@@ -356,13 +357,48 @@ const migrateConfig = {
|
||||
name: 'MiniMax',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.minimax.chat/v1/',
|
||||
models: SYSTEM_MODELS.minimax.filter((m) => m.enabled),
|
||||
models: SYSTEM_MODELS.minimax,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'23': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
showTopics: true,
|
||||
windowStyle: 'transparent'
|
||||
}
|
||||
}
|
||||
},
|
||||
'24': async (state: RootState) => {
|
||||
for (const key of await localforage.keys()) {
|
||||
if (key.startsWith('topic:')) {
|
||||
localforage.getItem(key).then((topic) => localforage.setItem(key, pick(topic, ['id', 'messages'])))
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
assistants: {
|
||||
...state.assistants,
|
||||
assistants: state.assistants.assistants.map((assistant) => ({
|
||||
...assistant,
|
||||
topics: assistant.topics.map((topic) => ({
|
||||
...topic,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}))
|
||||
}))
|
||||
},
|
||||
settings: {
|
||||
...state.settings,
|
||||
topicPosition: 'right'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||
|
||||
export interface RuntimeState {
|
||||
avatar: string
|
||||
@@ -8,7 +8,7 @@ export interface RuntimeState {
|
||||
}
|
||||
|
||||
const initialState: RuntimeState = {
|
||||
avatar: Logo,
|
||||
avatar: UserAvatar,
|
||||
generating: false,
|
||||
minappShow: false
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const runtimeSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
setAvatar: (state, action: PayloadAction<string | null>) => {
|
||||
state.avatar = action.payload || Logo
|
||||
state.avatar = action.payload || AppLogo
|
||||
},
|
||||
setGenerating: (state, action: PayloadAction<boolean>) => {
|
||||
state.generating = action.payload
|
||||
|
||||
@@ -9,8 +9,8 @@ export enum ThemeMode {
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
showRightSidebar: boolean
|
||||
showAssistants: boolean
|
||||
showTopics: boolean
|
||||
sendMessageShortcut: SendMessageShortcut
|
||||
language: string
|
||||
proxyUrl?: string
|
||||
@@ -19,12 +19,14 @@ export interface SettingsState {
|
||||
messageFont: 'system' | 'serif'
|
||||
showInputEstimatedTokens: boolean
|
||||
theme: ThemeMode
|
||||
windowStyle: 'transparent' | 'opaque'
|
||||
fontSize: number
|
||||
topicPosition: 'left' | 'right'
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
showRightSidebar: true,
|
||||
showAssistants: true,
|
||||
showTopics: true,
|
||||
sendMessageShortcut: 'Enter',
|
||||
language: navigator.language,
|
||||
proxyUrl: undefined,
|
||||
@@ -33,22 +35,27 @@ const initialState: SettingsState = {
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false,
|
||||
theme: ThemeMode.light,
|
||||
fontSize: 14
|
||||
windowStyle: 'opaque',
|
||||
fontSize: 14,
|
||||
topicPosition: 'right'
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: 'settings',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleRightSidebar: (state) => {
|
||||
state.showRightSidebar = !state.showRightSidebar
|
||||
},
|
||||
setShowRightSidebar: (state, action: PayloadAction<boolean>) => {
|
||||
state.showRightSidebar = action.payload
|
||||
setShowAssistants: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAssistants = action.payload
|
||||
},
|
||||
toggleShowAssistants: (state) => {
|
||||
state.showAssistants = !state.showAssistants
|
||||
},
|
||||
setShowTopics: (state, action: PayloadAction<boolean>) => {
|
||||
state.showTopics = action.payload
|
||||
},
|
||||
toggleShowTopics: (state) => {
|
||||
state.showTopics = !state.showTopics
|
||||
},
|
||||
setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => {
|
||||
state.sendMessageShortcut = action.payload
|
||||
},
|
||||
@@ -75,14 +82,22 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setFontSize: (state, action: PayloadAction<number>) => {
|
||||
state.fontSize = action.payload
|
||||
},
|
||||
setWindowStyle: (state, action: PayloadAction<'transparent' | 'opaque'>) => {
|
||||
state.windowStyle = action.payload
|
||||
console.log(state.windowStyle)
|
||||
},
|
||||
setTopicPosition: (state, action: PayloadAction<'left' | 'right'>) => {
|
||||
state.topicPosition = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const {
|
||||
setShowRightSidebar,
|
||||
toggleRightSidebar,
|
||||
setShowAssistants,
|
||||
toggleShowAssistants,
|
||||
setShowTopics,
|
||||
toggleShowTopics,
|
||||
setSendMessageShortcut,
|
||||
setLanguage,
|
||||
setProxyUrl,
|
||||
@@ -91,7 +106,9 @@ export const {
|
||||
setMessageFont,
|
||||
setShowInputEstimatedTokens,
|
||||
setTheme,
|
||||
setFontSize
|
||||
setFontSize,
|
||||
setWindowStyle,
|
||||
setTopicPosition
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user