Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a2dc325896 | ||
|
|
b131d320ea | ||
|
|
b88f4a869e | ||
|
|
461458e5ec | ||
|
|
4c2014f1d6 | ||
|
|
647dd3e751 | ||
|
|
4225312d4a | ||
|
|
c2a4613e32 | ||
|
|
5d5c1eee74 | ||
|
|
c1b5e6b183 | ||
|
|
fd37ba18dc | ||
|
|
4a26f7ce78 | ||
|
|
8b38ebcac4 | ||
|
|
e8dac28787 | ||
|
|
3ccebb503f | ||
|
|
42327836de | ||
|
|
4d7a3bb8c3 | ||
|
|
1996e163c9 | ||
|
|
e43f7f87ab | ||
|
|
47a83fa67f | ||
|
|
5e954566c9 | ||
|
|
b8960ef02c | ||
|
|
1866b00265 |
@@ -6,4 +6,4 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -45,3 +45,6 @@ out
|
|||||||
# ENV
|
# ENV
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
# Local
|
||||||
|
local
|
||||||
|
|||||||
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
|
httpTimeout: 300000
|
||||||
|
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ productName: Cherry Studio
|
|||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
files:
|
files:
|
||||||
- '!**/.vscode/*'
|
- '!{.vscode,.yarn,.github}'
|
||||||
- '!src/*'
|
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
|
- '!src/*'
|
||||||
|
- '!local'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
@@ -56,6 +57,6 @@ electronDownload:
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
增加 Gemini、豆包、阶跃星辰等服务商支持
|
智能助理和消息列表合并
|
||||||
修复 Anthropic 回复问题
|
优化输入框样式
|
||||||
新的 Windows, Linux 侧边栏样式
|
提升小程序稳定性
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { resolve } from 'path'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
ollama: resolve('ollama/src')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
|||||||
22
package.json
@@ -1,10 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "cherry-studio",
|
"name": "CherryStudio",
|
||||||
"version": "0.5.6",
|
"version": "0.6.7",
|
||||||
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "kangfenmao@qq.com",
|
"author": "kangfenmao@qq.com",
|
||||||
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
||||||
|
"workspaces": {
|
||||||
|
"packages": [
|
||||||
|
"local"
|
||||||
|
]
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
@@ -25,12 +31,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@sentry/electron": "^5.2.0",
|
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.1.7",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3"
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.24.3",
|
"@anthropic-ai/sdk": "^0.24.3",
|
||||||
@@ -51,15 +55,15 @@
|
|||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"electron": "^28.2.0",
|
"electron": "^28.3.3",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"emittery": "^1.0.3",
|
"emittery": "^1.0.3",
|
||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-react": "^7.34.3",
|
"eslint-plugin-react": "^7.34.3",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
"gpt-tokens": "^1.3.6",
|
"gpt-tokens": "^1.3.6",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
@@ -76,6 +80,7 @@
|
|||||||
"react-router-dom": "6",
|
"react-router-dom": "6",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"rehype-katex": "^7.0.0",
|
"rehype-katex": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
@@ -91,7 +96,8 @@
|
|||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"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"
|
"packageManager": "yarn@4.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
68
resources/graphrag.html
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://unpkg.com/3d-force-graph"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="3d-graph"></div>
|
||||||
|
<script src="./js/bridge.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { getQueryParam } from './js/utils.js'
|
||||||
|
|
||||||
|
const apiUrl = getQueryParam('apiUrl')
|
||||||
|
const modelId = getQueryParam('modelId')
|
||||||
|
const jsonUrl = `${apiUrl}/v1/global_graph/${modelId}`
|
||||||
|
|
||||||
|
const infoCard = document.createElement('div')
|
||||||
|
infoCard.style.position = 'fixed'
|
||||||
|
infoCard.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'
|
||||||
|
infoCard.style.padding = '8px'
|
||||||
|
infoCard.style.borderRadius = '4px'
|
||||||
|
infoCard.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'
|
||||||
|
infoCard.style.fontSize = '12px'
|
||||||
|
infoCard.style.maxWidth = '200px'
|
||||||
|
infoCard.style.display = 'none'
|
||||||
|
infoCard.style.zIndex = '1000'
|
||||||
|
document.body.appendChild(infoCard)
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (event) => {
|
||||||
|
infoCard.style.left = `${event.clientX + 10}px`
|
||||||
|
infoCard.style.top = `${event.clientY + 10}px`
|
||||||
|
})
|
||||||
|
|
||||||
|
const elem = document.getElementById('3d-graph')
|
||||||
|
const Graph = ForceGraph3D()(elem)
|
||||||
|
.jsonUrl(jsonUrl)
|
||||||
|
.nodeAutoColorBy((node) => node.properties.type || 'default')
|
||||||
|
.nodeVal((node) => node.properties.degree)
|
||||||
|
.linkWidth((link) => link.properties.weight)
|
||||||
|
.onNodeHover((node) => {
|
||||||
|
if (node) {
|
||||||
|
infoCard.innerHTML = `
|
||||||
|
<div style="font-weight: bold; margin-bottom: 4px; color: #333;">
|
||||||
|
${node.properties.title}
|
||||||
|
</div>
|
||||||
|
<div style="color: #666;">
|
||||||
|
${node.properties.description}
|
||||||
|
</div>`
|
||||||
|
infoCard.style.display = 'block'
|
||||||
|
} else {
|
||||||
|
infoCard.style.display = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onNodeClick((node) => {
|
||||||
|
const url = `${apiUrl}/v1/references/${modelId}/entities/${node.properties.human_readable_id}`
|
||||||
|
window.api.minApp({
|
||||||
|
url,
|
||||||
|
windowOptions: {
|
||||||
|
title: node.properties.title,
|
||||||
|
width: 500,
|
||||||
|
height: 800
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
36
resources/js/bridge.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
;(() => {
|
||||||
|
let messageId = 0
|
||||||
|
const pendingCalls = new Map()
|
||||||
|
|
||||||
|
function api(method, ...args) {
|
||||||
|
const id = messageId++
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingCalls.set(id, { resolve, reject })
|
||||||
|
window.parent.postMessage({ id, type: 'api-call', method, args }, '*')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
if (event.data.type === 'api-response') {
|
||||||
|
const { id, result, error } = event.data
|
||||||
|
const pendingCall = pendingCalls.get(id)
|
||||||
|
if (pendingCall) {
|
||||||
|
if (error) {
|
||||||
|
pendingCall.reject(new Error(error))
|
||||||
|
} else {
|
||||||
|
pendingCall.resolve(result)
|
||||||
|
}
|
||||||
|
pendingCalls.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.api = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (target, prop) => {
|
||||||
|
return (...args) => api(prop, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})()
|
||||||
5
resources/js/utils.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export function getQueryParam(paramName) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const params = new URLSearchParams(url.search)
|
||||||
|
return params.get(paramName)
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
|
||||||
import logger from 'electron-log'
|
|
||||||
import { writeFile } from 'fs'
|
|
||||||
|
|
||||||
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const options: SaveDialogOptions = {
|
|
||||||
title: '保存文件',
|
|
||||||
defaultPath: fileName
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: SaveDialogReturnValue = await dialog.showSaveDialog(options)
|
|
||||||
|
|
||||||
if (!result.canceled && result.filePath) {
|
|
||||||
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +1,16 @@
|
|||||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import * as Sentry from '@sentry/electron/main'
|
import { app, BrowserWindow } from 'electron'
|
||||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
|
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
|
||||||
import windowStateKeeper from 'electron-window-state'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import icon from '../../build/icon.png?asset'
|
import { registerIpc } from './ipc'
|
||||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
import { updateUserDataPath } from './utils/upgrade'
|
||||||
import { saveFile } from './event'
|
import { createMainWindow } from './window'
|
||||||
import AppUpdater from './updater'
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
// Load the previous state with fallback to defaults
|
|
||||||
const mainWindowState = windowStateKeeper({
|
|
||||||
defaultWidth: 1080,
|
|
||||||
defaultHeight: 670
|
|
||||||
})
|
|
||||||
|
|
||||||
const theme = appConfig.get('theme') || 'light'
|
|
||||||
|
|
||||||
// Create the browser window.
|
|
||||||
const mainWindow = new BrowserWindow({
|
|
||||||
x: mainWindowState.x,
|
|
||||||
y: mainWindowState.y,
|
|
||||||
width: mainWindowState.width,
|
|
||||||
height: mainWindowState.height,
|
|
||||||
minWidth: 1080,
|
|
||||||
minHeight: 600,
|
|
||||||
show: true,
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
transparent: process.platform === 'darwin',
|
|
||||||
vibrancy: 'fullscreen-ui',
|
|
||||||
titleBarStyle: 'hidden',
|
|
||||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
|
||||||
trafficLightPosition: { x: 8, y: 12 },
|
|
||||||
...(process.platform === 'linux' ? { icon } : {}),
|
|
||||||
webPreferences: {
|
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
|
||||||
sandbox: false,
|
|
||||||
webSecurity: false
|
|
||||||
// devTools: !app.isPackaged,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindowState.manage(mainWindow)
|
|
||||||
|
|
||||||
mainWindow.webContents.on('context-menu', () => {
|
|
||||||
const menu = new Menu()
|
|
||||||
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
|
|
||||||
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
|
|
||||||
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
|
|
||||||
menu.append(new MenuItem({ type: 'separator' }))
|
|
||||||
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
|
|
||||||
menu.popup()
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
|
||||||
event.preventDefault()
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
|
||||||
mainWindow.show()
|
|
||||||
})
|
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
|
||||||
shell.openExternal(details.url)
|
|
||||||
return { action: 'deny' }
|
|
||||||
})
|
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
|
||||||
// Load the remote URL for development or the local html file for production.
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
|
||||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return mainWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// 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
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
|
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
|
||||||
|
|
||||||
@@ -97,45 +24,12 @@ app.whenReady().then(() => {
|
|||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
// On macOS it's common to re-create a window in the app when the
|
||||||
// dock icon is clicked and there are no other windows open.
|
// dock icon is clicked and there are no other windows open.
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainWindow = createWindow()
|
const mainWindow = createMainWindow()
|
||||||
|
|
||||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
registerIpc(mainWindow, app)
|
||||||
|
|
||||||
// IPC
|
|
||||||
ipcMain.handle('get-app-info', () => ({
|
|
||||||
version: app.getVersion(),
|
|
||||||
isPackaged: app.isPackaged
|
|
||||||
}))
|
|
||||||
|
|
||||||
ipcMain.handle('open-website', (_, url: string) => {
|
|
||||||
shell.openExternal(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
|
||||||
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('save-file', saveFile)
|
|
||||||
|
|
||||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
|
||||||
appConfig.set('theme', theme)
|
|
||||||
mainWindow?.setTitleBarOverlay &&
|
|
||||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
|
||||||
ipcMain.handle('check-for-update', async () => {
|
|
||||||
autoUpdater.logger?.info('触发检查更新')
|
|
||||||
return {
|
|
||||||
currentVersion: autoUpdater.currentVersion,
|
|
||||||
update: await autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
installExtension(REDUX_DEVTOOLS)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
@@ -149,6 +43,3 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
// In this file you can include the rest of your app"s specific main process
|
// 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.
|
// code. You can also put them in separate files and require them here.
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://f0e972deff79c2df3e887e232d8a46a3@o4507610668007424.ingest.us.sentry.io/4507610670563328'
|
|
||||||
})
|
|
||||||
|
|||||||
58
src/main/ipc.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||||
|
|
||||||
|
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
|
import AppUpdater from './updater'
|
||||||
|
import { openFile, saveFile } from './utils/file'
|
||||||
|
import { compress, decompress } from './utils/zip'
|
||||||
|
import { createMinappWindow } from './window'
|
||||||
|
|
||||||
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||||
|
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||||
|
|
||||||
|
// IPC
|
||||||
|
ipcMain.handle('get-app-info', () => ({
|
||||||
|
version: app.getVersion(),
|
||||||
|
isPackaged: app.isPackaged,
|
||||||
|
appPath: app.getAppPath()
|
||||||
|
}))
|
||||||
|
|
||||||
|
ipcMain.handle('open-website', (_, url: string) => {
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
||||||
|
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('save-file', saveFile)
|
||||||
|
ipcMain.handle('open-file', openFile)
|
||||||
|
ipcMain.handle('reload', () => mainWindow.reload())
|
||||||
|
|
||||||
|
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||||
|
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||||
|
|
||||||
|
ipcMain.handle('minapp', (_, args) => {
|
||||||
|
createMinappWindow({
|
||||||
|
url: args.url,
|
||||||
|
parent: mainWindow,
|
||||||
|
windowOptions: {
|
||||||
|
...mainWindow.getBounds(),
|
||||||
|
...args.windowOptions
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||||
|
appConfig.set('theme', theme)
|
||||||
|
mainWindow?.setTitleBarOverlay &&
|
||||||
|
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||||
|
ipcMain.handle('check-for-update', async () => {
|
||||||
|
return {
|
||||||
|
currentVersion: autoUpdater.currentVersion,
|
||||||
|
update: await autoUpdater.checkForUpdates()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,11 +17,6 @@ export default class AppUpdater {
|
|||||||
mainWindow.webContents.send('update-error', error)
|
mainWindow.webContents.send('update-error', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检测是否需要更新
|
|
||||||
autoUpdater.on('checking-for-update', () => {
|
|
||||||
logger.info('正在检查更新……')
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||||
mainWindow.webContents.send('update-available', releaseInfo)
|
mainWindow.webContents.send('update-available', releaseInfo)
|
||||||
@@ -59,7 +54,6 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
// 检测到不需要更新时
|
// 检测到不需要更新时
|
||||||
autoUpdater.on('update-not-available', () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
logger.info('现在使用的就是最新版本,不用更新')
|
|
||||||
mainWindow.webContents.send('update-not-available')
|
mainWindow.webContents.send('update-not-available')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
24
src/main/utils/aes.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
|
// 定义密钥和初始化向量(IV)
|
||||||
|
const secretKey = 'kDQvWz5slot3syfucoo53X6KKsEUJoeFikpiUWRJTLIo3zcUPpFvEa009kK13KCr'
|
||||||
|
const iv = Buffer.from('Cherry Studio', 'hex')
|
||||||
|
|
||||||
|
// 加密函数
|
||||||
|
export function encrypt(text: string): { iv: string; encryptedData: string } {
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), iv)
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex')
|
||||||
|
encrypted += cipher.final('hex')
|
||||||
|
return {
|
||||||
|
iv: iv.toString('hex'),
|
||||||
|
encryptedData: encrypted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密函数
|
||||||
|
export function decrypt(encryptedData: string, iv: string): string {
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(secretKey), Buffer.from(iv, 'hex'))
|
||||||
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
|
||||||
|
decrypted += decipher.final('utf8')
|
||||||
|
return decrypted
|
||||||
|
}
|
||||||
55
src/main/utils/file.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
|
import logger from 'electron-log'
|
||||||
|
import { writeFile } from 'fs'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
|
export async function saveFile(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
fileName: string,
|
||||||
|
content: string,
|
||||||
|
options?: SaveDialogOptions
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
|
||||||
|
title: '保存文件',
|
||||||
|
defaultPath: fileName,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePath) {
|
||||||
|
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFile(
|
||||||
|
_: Electron.IpcMainInvokeEvent,
|
||||||
|
options: OpenDialogOptions
|
||||||
|
): Promise<{ fileName: string; content: Buffer } | null> {
|
||||||
|
try {
|
||||||
|
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
|
||||||
|
title: '打开文件',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: '所有文件', extensions: ['*'] }],
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const filePath = result.filePaths[0]
|
||||||
|
const fileName = filePath.split('/').pop() || ''
|
||||||
|
const content = await readFile(filePath)
|
||||||
|
return { fileName, content }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
39
src/main/utils/zip.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import util from 'node:util'
|
||||||
|
import zlib from 'node:zlib'
|
||||||
|
|
||||||
|
import logger from 'electron-log'
|
||||||
|
|
||||||
|
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
|
||||||
|
const gzipPromise = util.promisify(zlib.gzip)
|
||||||
|
const gunzipPromise = util.promisify(zlib.gunzip)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩字符串
|
||||||
|
* @param {string} string - 要压缩的 JSON 字符串
|
||||||
|
* @returns {Promise<Buffer>} 压缩后的 Buffer
|
||||||
|
*/
|
||||||
|
export async function compress(str) {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(str, 'utf-8')
|
||||||
|
const compressedBuffer = await gzipPromise(buffer)
|
||||||
|
return compressedBuffer
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Compression failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压缩 Buffer 到 JSON 字符串
|
||||||
|
* @param {Buffer} compressedBuffer - 压缩的 Buffer
|
||||||
|
* @returns {Promise<string>} 解压缩后的 JSON 字符串
|
||||||
|
*/
|
||||||
|
export async function decompress(compressedBuffer) {
|
||||||
|
try {
|
||||||
|
const buffer = await gunzipPromise(compressedBuffer)
|
||||||
|
return buffer.toString('utf-8')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Decompression failed:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/main/window.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||||
|
import windowStateKeeper from 'electron-window-state'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import icon from '../../build/icon.png?asset'
|
||||||
|
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
|
|
||||||
|
export function createMainWindow() {
|
||||||
|
// Load the previous state with fallback to defaults
|
||||||
|
const mainWindowState = windowStateKeeper({
|
||||||
|
defaultWidth: 1080,
|
||||||
|
defaultHeight: 670
|
||||||
|
})
|
||||||
|
|
||||||
|
const theme = appConfig.get('theme') || 'light'
|
||||||
|
|
||||||
|
// Create the browser window.
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
x: mainWindowState.x,
|
||||||
|
y: mainWindowState.y,
|
||||||
|
width: mainWindowState.width,
|
||||||
|
height: mainWindowState.height,
|
||||||
|
minWidth: 1080,
|
||||||
|
minHeight: 600,
|
||||||
|
show: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
transparent: process.platform === 'darwin',
|
||||||
|
vibrancy: 'fullscreen-ui',
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||||
|
trafficLightPosition: { x: 8, y: 12 },
|
||||||
|
...(process.platform === 'linux' ? { icon } : {}),
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
webSecurity: false,
|
||||||
|
webviewTag: true
|
||||||
|
// devTools: !app.isPackaged,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindowState.manage(mainWindow)
|
||||||
|
|
||||||
|
mainWindow.webContents.on('context-menu', () => {
|
||||||
|
const menu = new Menu()
|
||||||
|
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
|
||||||
|
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
|
||||||
|
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
|
||||||
|
menu.append(new MenuItem({ type: 'separator' }))
|
||||||
|
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
|
||||||
|
menu.popup()
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||||
|
event.preventDefault()
|
||||||
|
shell.openExternal(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
|
||||||
|
if (details.responseHeaders?.['X-Frame-Options']) {
|
||||||
|
delete details.responseHeaders['X-Frame-Options']
|
||||||
|
}
|
||||||
|
if (details.responseHeaders?.['x-frame-options']) {
|
||||||
|
delete details.responseHeaders['x-frame-options']
|
||||||
|
}
|
||||||
|
if (details.responseHeaders?.['Content-Security-Policy']) {
|
||||||
|
delete details.responseHeaders['Content-Security-Policy']
|
||||||
|
}
|
||||||
|
if (details.responseHeaders?.['content-security-policy']) {
|
||||||
|
delete details.responseHeaders['content-security-policy']
|
||||||
|
}
|
||||||
|
callback({ cancel: false, responseHeaders: details.responseHeaders })
|
||||||
|
})
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMinappWindow({
|
||||||
|
url,
|
||||||
|
parent,
|
||||||
|
windowOptions
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
parent?: BrowserWindow
|
||||||
|
windowOptions?: Electron.BrowserWindowConstructorOptions
|
||||||
|
}) {
|
||||||
|
const width = windowOptions?.width || 1000
|
||||||
|
const height = windowOptions?.height || 680
|
||||||
|
|
||||||
|
const minappWindow = new BrowserWindow({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
title: 'Cherry Studio',
|
||||||
|
...windowOptions,
|
||||||
|
parent,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/minapp.js'),
|
||||||
|
sandbox: false,
|
||||||
|
contextIsolation: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
minappWindow.loadURL(url)
|
||||||
|
|
||||||
|
return minappWindow
|
||||||
|
}
|
||||||
9
src/preload/index.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
import type { OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -7,12 +8,18 @@ declare global {
|
|||||||
getAppInfo: () => Promise<{
|
getAppInfo: () => Promise<{
|
||||||
version: string
|
version: string
|
||||||
isPackaged: boolean
|
isPackaged: boolean
|
||||||
|
appPath: string
|
||||||
}>
|
}>
|
||||||
checkForUpdate: () => void
|
checkForUpdate: () => void
|
||||||
openWebsite: (url: string) => void
|
openWebsite: (url: string) => void
|
||||||
setProxy: (proxy: string | undefined) => void
|
setProxy: (proxy: string | undefined) => void
|
||||||
saveFile: (path: string, content: string) => void
|
saveFile: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
|
||||||
|
openFile: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null>
|
||||||
setTheme: (theme: 'light' | 'dark') => void
|
setTheme: (theme: 'light' | 'dark') => void
|
||||||
|
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||||
|
reload: () => void
|
||||||
|
compress: (text: string) => Promise<Buffer>
|
||||||
|
decompress: (text: Buffer) => Promise<string>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,15 @@ const api = {
|
|||||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
||||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
|
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||||
|
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
||||||
|
reload: () => ipcRenderer.invoke('reload'),
|
||||||
|
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
||||||
|
ipcRenderer.invoke('save-file', path, content, options)
|
||||||
|
},
|
||||||
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Cherry Studio</title>
|
|
||||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
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 *" />
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { PersistGate } from 'redux-persist/integration/react'
|
|||||||
|
|
||||||
import Sidebar from './components/app/Sidebar'
|
import Sidebar from './components/app/Sidebar'
|
||||||
import TopViewContainer from './components/TopView'
|
import TopViewContainer from './components/TopView'
|
||||||
|
import AntdProvider from './context/AntdProvider'
|
||||||
|
import { ThemeProvider } from './context/ThemeProvider'
|
||||||
import AgentsPage from './pages/agents/AgentsPage'
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
import AntdProvider from './providers/AntdProvider'
|
|
||||||
import { ThemeProvider } from './providers/ThemeProvider'
|
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -23,8 +24,9 @@ function App(): JSX.Element {
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/apps" element={<AgentsPage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -1,63 +1,80 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 4563475 */
|
font-family: 'iconfont'; /* Project id 4563475 */
|
||||||
src: url('iconfont.woff2?t=1723186111414') format('woff2'),
|
src: url('iconfont.woff2?t=1725450669049') format('woff2');
|
||||||
url('iconfont.woff?t=1723186111414') format('woff'),
|
|
||||||
url('iconfont.ttf?t=1723186111414') format('truetype');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
font-family: "iconfont" !important;
|
font-family: 'iconfont' !important;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-gridlines:before {
|
||||||
|
content: '\e942';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-frame:before {
|
||||||
|
content: '\e680';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-row-2copy:before {
|
||||||
|
content: '\e681';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-sidebar-left:before {
|
||||||
|
content: '\e6ab';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-sidebar-right:before {
|
||||||
|
content: '\e6ac';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-inbox:before {
|
||||||
|
content: '\e869';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-business-smart-assistant:before {
|
||||||
|
content: '\e601';
|
||||||
|
}
|
||||||
|
|
||||||
.icon-copy:before {
|
.icon-copy:before {
|
||||||
content: "\e6ae";
|
content: '\e6ae';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-ic_send:before {
|
.icon-ic_send:before {
|
||||||
content: "\e795";
|
content: '\e795';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-dark1:before {
|
.icon-dark1:before {
|
||||||
content: "\e72f";
|
content: '\e72f';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-theme-light:before {
|
.icon-theme-light:before {
|
||||||
content: "\e6b7";
|
content: '\e6b7';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-translate_line:before {
|
.icon-translate_line:before {
|
||||||
content: "\e7de";
|
content: '\e7de';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-history:before {
|
.icon-history:before {
|
||||||
content: "\e758";
|
content: '\e758';
|
||||||
}
|
|
||||||
|
|
||||||
.icon-hidesidebarhoriz:before {
|
|
||||||
content: "\e8eb";
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-showsidebarhoriz:before {
|
|
||||||
content: "\e944";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-a-addchat:before {
|
.icon-a-addchat:before {
|
||||||
content: "\e658";
|
content: '\e658';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-appstore:before {
|
.icon-appstore:before {
|
||||||
content: "\e792";
|
content: '\e792';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-chat:before {
|
.icon-chat:before {
|
||||||
content: "\e615";
|
content: '\e615';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-setting:before {
|
.icon-setting:before {
|
||||||
content: "\e78e";
|
content: '\e78e';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
src/renderer/src/assets/images/apps/360-ai.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/renderer/src/assets/images/apps/ai-search.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/baidu-ai.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/renderer/src/assets/images/apps/baixiaoying.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/renderer/src/assets/images/apps/devv.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/renderer/src/assets/images/apps/kimi.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/renderer/src/assets/images/apps/metaso.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/perplexity.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/apps/sensetime.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/renderer/src/assets/images/apps/sparkdesk.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/apps/tiangong.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/renderer/src/assets/images/apps/yuanbao.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/apps/yuewen.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/renderer/src/assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/avatar.webp
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
src/renderer/src/assets/images/models/chatglm.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/renderer/src/assets/images/models/hailuo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/models/yi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
|||||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="300" cy="300" r="300" fill="white"/>
|
|
||||||
<rect x="409.733" y="340.032" width="42.3862" height="151.648" rx="21.1931" fill="#003425"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.768L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="#003425"/>
|
|
||||||
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="#003425"/>
|
|
||||||
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00DD20"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 869 B |
BIN
src/renderer/src/assets/images/providers/minimax.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
BIN
src/renderer/src/assets/images/providers/moonshot.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/renderer/src/assets/images/providers/yi.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +0,0 @@
|
|||||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="300" cy="300" r="300" fill="#003425"/>
|
|
||||||
<rect x="409.733" y="340.031" width="42.3862" height="151.648" rx="21.1931" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.767L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="white"/>
|
|
||||||
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="white"/>
|
|
||||||
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00FF25"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 865 B |
@@ -1,7 +1,7 @@
|
|||||||
@import './markdown.scss';
|
@import './markdown.scss';
|
||||||
@import './scrollbar.scss';
|
@import './scrollbar.scss';
|
||||||
@import '../fonts/icon-fonts/iconfont.css';
|
@import '../fonts/icon-fonts/iconfont.css';
|
||||||
@import '../fonts/Ubuntu/Ubuntu.css';
|
@import '../fonts/ubuntu/ubuntu.css';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-white: #ffffff;
|
--color-white: #ffffff;
|
||||||
@@ -31,24 +31,26 @@
|
|||||||
--color-text: var(--color-text-1);
|
--color-text: var(--color-text-1);
|
||||||
--color-icon: #ffffff99;
|
--color-icon: #ffffff99;
|
||||||
--color-icon-white: #ffffff;
|
--color-icon-white: #ffffff;
|
||||||
--color-border: #ffffff20;
|
--color-border: #000;
|
||||||
|
--color-border-soft: #ffffff20;
|
||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
--color-link: #1677ff;
|
--color-link: #1677ff;
|
||||||
--color-code-background: #323232;
|
--color-code-background: #323232;
|
||||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
|
||||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
|
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
--navbar-background: rgba(30, 30, 30, 0.75);
|
--navbar-background-mac: rgba(30, 30, 30, 0.8);
|
||||||
--sidebar-background: rgba(30, 30, 30, 0.75);
|
--navbar-background: rgba(30, 30, 30);
|
||||||
--input-bar-background: rgba(255, 255, 255, 0.02);
|
--input-bar-background: rgba(255, 255, 255, 0.02);
|
||||||
|
|
||||||
--navbar-height: 42px;
|
--navbar-height: 42px;
|
||||||
--sidebar-width: 55px;
|
--sidebar-width: 52px;
|
||||||
--assistants-width: 245px;
|
|
||||||
--topic-list-width: 260px;
|
|
||||||
--settings-width: var(--assistants-width);
|
|
||||||
--status-bar-height: 40px;
|
--status-bar-height: 40px;
|
||||||
--input-bar-height: 85px;
|
--input-bar-height: 85px;
|
||||||
|
|
||||||
|
--assistants-width: 240px;
|
||||||
|
--topic-list-width: 270px;
|
||||||
|
--settings-width: var(--assistants-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme-mode='light'] {
|
body[theme-mode='light'] {
|
||||||
@@ -80,14 +82,15 @@ body[theme-mode='light'] {
|
|||||||
--color-icon: #00000099;
|
--color-icon: #00000099;
|
||||||
--color-icon-white: #000000;
|
--color-icon-white: #000000;
|
||||||
--color-border: #00000028;
|
--color-border: #00000028;
|
||||||
|
--color-border-soft: #00000028;
|
||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
--color-link: #1677ff;
|
--color-link: #1677ff;
|
||||||
--color-code-background: #e3e3e3;
|
--color-code-background: #e3e3e3;
|
||||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
|
||||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
|
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
--navbar-background: rgba(255, 255, 255, 0.8);
|
--navbar-background-mac: rgba(255, 255, 255, 0.75);
|
||||||
--sidebar-background: rgba(255, 255, 255, 0.8);
|
--navbar-background: rgba(255, 255, 255);
|
||||||
--input-bar-background: rgba(0, 0, 0, 0.02);
|
--input-bar-background: rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +102,14 @@ body[theme-mode='light'] {
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
@@ -163,39 +174,37 @@ body,
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragable {
|
.drag {
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dragdisable {
|
.nodrag {
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.minapp-drawer {
|
.minapp-drawer {
|
||||||
.ant-drawer-header-title {
|
.ant-drawer-content-wrapper {
|
||||||
flex-direction: row-reverse;
|
box-shadow: none;
|
||||||
}
|
|
||||||
.ant-drawer-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
right: 15px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-right: -5px;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
z-index: 100000;
|
|
||||||
}
|
}
|
||||||
.ant-drawer-header {
|
.ant-drawer-header {
|
||||||
height: calc(var(--navbar-height) + 0.5px);
|
position: absolute;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
min-height: calc(var(--navbar-height) + 0.5px);
|
||||||
background: var(--navbar-background);
|
background: var(--navbar-background);
|
||||||
width: calc(100vw - var(--sidebar-width));
|
width: calc(100vw - var(--sidebar-width));
|
||||||
padding-right: 10px !important;
|
|
||||||
border-bottom: 0.5px solid var(--color-border);
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
margin-top: -0.5px;
|
margin-top: -0.5px;
|
||||||
}
|
}
|
||||||
.ant-drawer-body {
|
.ant-drawer-body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin-top: var(--navbar-height);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.minapp-mask {
|
.minapp-mask {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-drawer-header {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* 全局初始化滚动条样式 */
|
/* 全局初始化滚动条样式 */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 2px;
|
||||||
height: 3px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
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 { droppableReorder } from '@renderer/utils'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
list: T[]
|
list: T[]
|
||||||
|
style?: React.CSSProperties
|
||||||
children: (item: T, index: number) => React.ReactNode
|
children: (item: T, index: number) => React.ReactNode
|
||||||
onUpdate: (list: T[]) => void
|
onUpdate: (list: T[]) => void
|
||||||
onDragStart?: () => void
|
onDragStart?: OnDragStartResponder
|
||||||
onDragEnd?: () => void
|
onDragEnd?: OnDragEndResponder
|
||||||
}
|
}
|
||||||
|
|
||||||
const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => {
|
const DragableList: FC<Props<any>> = ({ children, list, style, onDragStart, onUpdate, onDragEnd }) => {
|
||||||
const _onDragEnd = (result: DropResult) => {
|
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||||
onDragEnd?.()
|
onDragEnd?.(result, provided)
|
||||||
if (result.destination) {
|
if (result.destination) {
|
||||||
const sourceIndex = result.source.index
|
const sourceIndex = result.source.index
|
||||||
const destIndex = result.destination.index
|
const destIndex = result.destination.index
|
||||||
@@ -33,7 +42,7 @@ const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, o
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
style={{ ...provided.draggableProps.style, marginBottom: 8 }}>
|
style={{ ...provided.draggableProps.style, marginBottom: 8, ...style }}>
|
||||||
{children(item, index)}
|
{children(item, index)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { FC, useEffect, useRef } from 'react'
|
import { FC, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
7
src/renderer/src/components/Icons/CopyIcon.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
const CopyIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
return <i {...props} className={`iconfont icon-copy ${props.className}`} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyIcon
|
||||||
@@ -1,24 +1,29 @@
|
|||||||
|
/* eslint-disable react/no-unknown-property */
|
||||||
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
|
import { useBridge } from '@renderer/hooks/useBridge'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setMinappShow } from '@renderer/store/runtime'
|
import { setMinappShow } from '@renderer/store/runtime'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
import { Drawer } from 'antd'
|
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 styled from 'styled-components'
|
||||||
|
|
||||||
import { TopView } from '../TopView'
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
interface ShowParams {
|
interface Props {
|
||||||
title: string
|
app: MinAppType
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props extends ShowParams {
|
|
||||||
resolve: (data: any) => void
|
resolve: (data: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
const webviewRef = useRef<WebviewTag | null>(null)
|
||||||
|
|
||||||
|
useBridge()
|
||||||
|
|
||||||
|
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@@ -26,18 +31,60 @@ const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onReload = () => {
|
const onReload = () => {
|
||||||
if (iframeRef.current) {
|
if (webviewRef.current) {
|
||||||
iframeRef.current.src = url
|
webviewRef.current.src = app.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOpenLink = () => {
|
const onOpenLink = () => {
|
||||||
window.api.openWebsite(url)
|
window.api.openWebsite(app.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Title = () => {
|
||||||
|
return (
|
||||||
|
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||||
|
<TitleText>{app.name}</TitleText>
|
||||||
|
<ButtonsGroup className={isWindows ? 'windows' : ''}>
|
||||||
|
<Button onClick={onReload}>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
{canOpenExternalLink && (
|
||||||
|
<Button onClick={onOpenLink}>
|
||||||
|
<ExportOutlined />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={onClose}>
|
||||||
|
<CloseOutlined />
|
||||||
|
</Button>
|
||||||
|
</ButtonsGroup>
|
||||||
|
</TitleContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={title}
|
title={<Title />}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
open={open}
|
open={open}
|
||||||
@@ -48,42 +95,55 @@ const PopupContainer: React.FC<Props> = ({ title, url, resolve }) => {
|
|||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||||
<Frame src={url} ref={iframeRef} />
|
<webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />
|
||||||
<ButtonsGroup>
|
|
||||||
<Button onClick={onReload}>
|
|
||||||
<ReloadOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onOpenLink}>
|
|
||||||
<ExportOutlined />
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onClose}>
|
|
||||||
<CloseOutlined />
|
|
||||||
</Button>
|
|
||||||
</ButtonsGroup>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Frame = styled.iframe`
|
const WebviewStyle: React.CSSProperties = {
|
||||||
width: calc(100vw - var(--sidebar-width));
|
width: 'calc(100vw - var(--sidebar-width))',
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: 'calc(100vh - var(--navbar-height))',
|
||||||
border: none;
|
backgroundColor: 'white',
|
||||||
|
display: 'inline-flex'
|
||||||
|
}
|
||||||
|
|
||||||
|
const TitleContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: ${isMac ? '20px' : '15px'};
|
||||||
|
padding-right: 10px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitleText = styled.div`
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
margin-right: 10px;
|
||||||
|
user-select: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ButtonsGroup = styled.div`
|
const ButtonsGroup = styled.div`
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: var(--navbar-height);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 0 10px;
|
-webkit-app-region: no-drag;
|
||||||
|
&.windows {
|
||||||
|
margin-right: ${isWindows ? '130px' : 0};
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 0 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Button = styled.div`
|
const Button = styled.div`
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -107,12 +167,12 @@ export default class MinApp {
|
|||||||
TopView.hide('MinApp')
|
TopView.hide('MinApp')
|
||||||
store.dispatch(setMinappShow(false))
|
store.dispatch(setMinappShow(false))
|
||||||
}
|
}
|
||||||
static start(props: ShowParams) {
|
static start(app: MinAppType) {
|
||||||
store.dispatch(setMinappShow(true))
|
store.dispatch(setMinappShow(true))
|
||||||
return new Promise<any>((resolve) => {
|
return new Promise<any>((resolve) => {
|
||||||
TopView.show(
|
TopView.show(
|
||||||
<PopupContainer
|
<PopupContainer
|
||||||
{...props}
|
app={app}
|
||||||
resolve={(v) => {
|
resolve={(v) => {
|
||||||
resolve(v)
|
resolve(v)
|
||||||
this.close()
|
this.close()
|
||||||
|
|||||||
@@ -25,24 +25,30 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
() => ({
|
() => ({
|
||||||
id: defaultAssistant.id,
|
id: defaultAssistant.id,
|
||||||
name: defaultAssistant.name,
|
name: defaultAssistant.name,
|
||||||
emoji: '',
|
emoji: defaultAssistant.emoji || '',
|
||||||
prompt: defaultAssistant.prompt,
|
prompt: defaultAssistant.prompt,
|
||||||
group: 'system'
|
group: 'system'
|
||||||
}),
|
}),
|
||||||
[defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
|
[defaultAssistant.emoji, defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
|
||||||
)
|
)
|
||||||
|
|
||||||
const agents = useMemo(() => {
|
const agents = useMemo(() => {
|
||||||
const allAgents = [defaultAgent, ...userAgents, ...systemAgents] as Agent[]
|
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||||
const list = allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))
|
const list = [defaultAgent, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
|
||||||
return searchText
|
return searchText
|
||||||
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
|
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
|
||||||
: list
|
: list
|
||||||
}, [assistants, defaultAgent, searchText, userAgents])
|
}, [assistants, defaultAgent, searchText, userAgents])
|
||||||
|
|
||||||
const onCreateAssistant = (agent: Agent) => {
|
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)
|
const assistant = covertAgentToAssistant(agent)
|
||||||
|
|
||||||
addAssistant(assistant)
|
addAssistant(assistant)
|
||||||
resolve(assistant)
|
resolve(assistant)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@@ -64,8 +70,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
open={open}
|
open={open}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
transitionName=""
|
transitionName="ant-move-down"
|
||||||
maskTransitionName=""
|
maskTransitionName="ant-fade"
|
||||||
footer={null}>
|
footer={null}>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.search')}
|
placeholder={t('common.search')}
|
||||||
@@ -34,7 +34,14 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
||||||
<Box mb={8}>{t('common.name')}</Box>
|
<Box mb={8}>{t('common.name')}</Box>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.assistant') + t('common.name')}
|
placeholder={t('common.assistant') + t('common.name')}
|
||||||
@@ -45,7 +52,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
|||||||
{t('common.prompt')}
|
{t('common.prompt')}
|
||||||
</Box>
|
</Box>
|
||||||
<TextArea
|
<TextArea
|
||||||
rows={4}
|
rows={10}
|
||||||
placeholder={t('common.assistant') + t('common.prompt')}
|
placeholder={t('common.assistant') + t('common.prompt')}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
|||||||
108
src/renderer/src/components/Popups/UserPopup.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import LocalStorage from '@renderer/services/storage'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setAvatar } from '@renderer/store/runtime'
|
||||||
|
import { setUserName } from '@renderer/store/settings'
|
||||||
|
import { compressImage } from '@renderer/utils'
|
||||||
|
import { Avatar, Input, Modal, Upload } from 'antd'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { Center, HStack } from '../Layout'
|
||||||
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { userName } = useSettings()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const avatar = useAvatar()
|
||||||
|
|
||||||
|
const onOk = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
width="300px"
|
||||||
|
open={open}
|
||||||
|
footer={null}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="ant-move-down">
|
||||||
|
<Center mt="30px">
|
||||||
|
<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} />
|
||||||
|
</Upload>
|
||||||
|
</Center>
|
||||||
|
<HStack alignItems="center" gap="10px" p="20px">
|
||||||
|
<Input
|
||||||
|
placeholder={t('settings.general.user_name.placeholder')}
|
||||||
|
value={userName}
|
||||||
|
onChange={(e) => dispatch(setUserName(e.target.value))}
|
||||||
|
style={{ flex: 1, textAlign: 'center', width: '100%' }}
|
||||||
|
maxLength={30}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserAvatar = styled(Avatar)`
|
||||||
|
cursor: pointer;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default class UserPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide('UserPopup')
|
||||||
|
}
|
||||||
|
static show() {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
this.hide()
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
'UserPopup'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useRuntime } from '@renderer/hooks/useStore'
|
import { useRuntime } from '@renderer/hooks/useStore'
|
||||||
import { FC, PropsWithChildren } from 'react'
|
import { FC, PropsWithChildren } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@@ -7,7 +8,11 @@ type Props = PropsWithChildren & JSX.IntrinsicElements['div']
|
|||||||
|
|
||||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||||
const { minappShow } = useRuntime()
|
const { minappShow } = useRuntime()
|
||||||
const backgroundColor = minappShow ? 'var(--color-background)' : 'var(--navbar-background)'
|
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 (
|
return (
|
||||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
<NavbarContainer {...props} style={{ backgroundColor }}>
|
||||||
@@ -37,7 +42,6 @@ const NavbarContainer = styled.div`
|
|||||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
||||||
border-bottom: 0.5px solid var(--color-border);
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
background-color: var(--navbar-background);
|
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,46 +1,75 @@
|
|||||||
import { TranslationOutlined } from '@ant-design/icons'
|
import { TranslationOutlined } from '@ant-design/icons'
|
||||||
import Logo from '@renderer/assets/images/logo.png'
|
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
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 { Avatar } from 'antd'
|
||||||
import { FC } from 'react'
|
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 styled from 'styled-components'
|
||||||
|
|
||||||
|
import UserPopup from '../Popups/UserPopup'
|
||||||
|
|
||||||
const Sidebar: FC = () => {
|
const Sidebar: FC = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { minappShow } = useRuntime()
|
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 isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Container style={{ backgroundColor: minappShow ? 'var(--color-background)' : 'var(--sidebar-background)' }}>
|
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBgColor }}>
|
||||||
<StyledLink to="/">
|
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||||
<AvatarImg src={avatar || Logo} draggable={false} />
|
|
||||||
</StyledLink>
|
|
||||||
<MainMenus>
|
<MainMenus>
|
||||||
<Menus>
|
<Menus>
|
||||||
<StyledLink to="/">
|
<StyledLink onClick={onToggleShowAssistants}>
|
||||||
<Icon className={isRoute('/')}>
|
<Icon className={isRoute('/')}>
|
||||||
<i className="iconfont icon-chat"></i>
|
<i className="iconfont icon-chat"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
<StyledLink to="/apps">
|
<StyledLink onClick={() => to('/agents')}>
|
||||||
<Icon className={isRoute('/apps')}>
|
<Icon className={isRoute('/agents')}>
|
||||||
<i className="iconfont icon-appstore"></i>
|
<i className="iconfont icon-business-smart-assistant"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
<StyledLink to="/translate">
|
<StyledLink onClick={() => to('/translate')}>
|
||||||
<Icon className={isRoute('/translate')}>
|
<Icon className={isRoute('/translate')}>
|
||||||
<TranslationOutlined />
|
<TranslationOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
<StyledLink onClick={() => to('/apps')}>
|
||||||
|
<Icon className={isRoute('/apps')}>
|
||||||
|
<i className="iconfont icon-appstore"></i>
|
||||||
|
</Icon>
|
||||||
|
</StyledLink>
|
||||||
</Menus>
|
</Menus>
|
||||||
</MainMenus>
|
</MainMenus>
|
||||||
<Menus>
|
<Menus>
|
||||||
<StyledLink to="/settings/provider">
|
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||||
<i className="iconfont icon-setting"></i>
|
<i className="iconfont icon-setting"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
@@ -56,11 +85,11 @@ const Container = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
|
min-width: var(--sidebar-width);
|
||||||
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
|
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
|
||||||
-webkit-app-region: drag !important;
|
-webkit-app-region: drag !important;
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
|
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
|
||||||
background-color: var(--sidebar-background);
|
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -71,6 +100,7 @@ const AvatarImg = styled(Avatar)`
|
|||||||
margin-bottom: ${isMac ? '12px' : '12px'};
|
margin-bottom: ${isMac ? '12px' : '12px'};
|
||||||
margin-top: ${isMac ? '5px' : '2px'};
|
margin-top: ${isMac ? '5px' : '2px'};
|
||||||
border: none;
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
`
|
`
|
||||||
const MainMenus = styled.div`
|
const MainMenus = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -120,7 +150,7 @@ const Icon = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledLink = styled(Link)`
|
const StyledLink = styled.div`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
&* {
|
&* {
|
||||||
|
|||||||
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
|
||||||
79
src/renderer/src/config/minapp.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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 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/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
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'
|
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: [
|
openai: [
|
||||||
{
|
{
|
||||||
id: 'gpt-4o',
|
id: 'gpt-4o',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4o',
|
name: ' GPT-4o',
|
||||||
group: 'GPT 4o',
|
group: 'GPT 4o'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4o-mini',
|
id: 'gpt-4o-mini',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4o-mini',
|
name: ' GPT-4o-mini',
|
||||||
group: 'GPT 4o',
|
group: 'GPT 4o'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4-turbo',
|
id: 'gpt-4-turbo',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4 Turbo',
|
name: ' GPT-4 Turbo',
|
||||||
group: 'GPT 4',
|
group: 'GPT 4'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4',
|
id: 'gpt-4',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4',
|
name: ' GPT-4',
|
||||||
group: 'GPT 4',
|
group: 'GPT 4'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
gemini: [
|
gemini: [
|
||||||
@@ -38,402 +68,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'gemini-1.5-flash',
|
id: 'gemini-1.5-flash',
|
||||||
provider: 'gemini',
|
provider: 'gemini',
|
||||||
name: 'Gemini 1.5 Flash',
|
name: 'Gemini 1.5 Flash',
|
||||||
group: 'Gemini 1.5',
|
group: 'Gemini 1.5'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemini-1.5-pro-exp-0801',
|
id: 'gemini-1.5-pro-exp-0801',
|
||||||
provider: 'gemini',
|
provider: 'gemini',
|
||||||
name: 'Gemini 1.5 Pro Experimental 0801',
|
name: 'Gemini 1.5 Pro Experimental 0801',
|
||||||
group: 'Gemini 1.5',
|
group: 'Gemini 1.5'
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
silicon: [
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-7B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-7B-Instruct',
|
|
||||||
group: 'Qwen2',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-1.5B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-1.5B-Instruct',
|
|
||||||
group: 'Qwen2',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen1.5-7B-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen1.5-7B-Chat',
|
|
||||||
group: 'Qwen1.5',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
],
|
|
||||||
deepseek: [
|
|
||||||
{
|
|
||||||
id: 'deepseek-chat',
|
|
||||||
provider: 'deepseek',
|
|
||||||
name: 'DeepSeek Chat',
|
|
||||||
group: 'DeepSeek Chat',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deepseek-coder',
|
|
||||||
provider: 'deepseek',
|
|
||||||
name: 'DeepSeek Coder',
|
|
||||||
group: 'DeepSeek Coder',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
yi: [
|
|
||||||
{
|
|
||||||
id: 'yi-large',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Large',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-large-turbo',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Large-Turbo',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-large-rag',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Large-Rag',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-medium',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Medium',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-medium-200k',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Medium-200k',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-spark',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Spark',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
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
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'glm-4-airx',
|
|
||||||
provider: 'zhipu',
|
|
||||||
name: 'GLM-4-AirX',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'glm-4-air',
|
|
||||||
provider: 'zhipu',
|
|
||||||
name: 'GLM-4-Air',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'glm-4v',
|
|
||||||
provider: 'zhipu',
|
|
||||||
name: 'GLM-4V',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'glm-4-alltools',
|
|
||||||
provider: 'zhipu',
|
|
||||||
name: 'GLM-4-AllTools',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
moonshot: [
|
|
||||||
{
|
|
||||||
id: 'moonshot-v1-8k',
|
|
||||||
provider: 'moonshot',
|
|
||||||
name: 'Moonshot V1 8k',
|
|
||||||
group: 'Moonshot V1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'moonshot-v1-32k',
|
|
||||||
provider: 'moonshot',
|
|
||||||
name: 'Moonshot V1 32k',
|
|
||||||
group: 'Moonshot V1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'moonshot-v1-128k',
|
|
||||||
provider: 'moonshot',
|
|
||||||
name: 'Moonshot V1 128k',
|
|
||||||
group: 'Moonshot V1',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
baichuan: [
|
|
||||||
{
|
|
||||||
id: 'Baichuan4',
|
|
||||||
provider: 'baichuan',
|
|
||||||
name: 'Baichuan4',
|
|
||||||
group: 'Baichuan4',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Baichuan3-Turbo',
|
|
||||||
provider: 'baichuan',
|
|
||||||
name: 'Baichuan3 Turbo',
|
|
||||||
group: 'Baichuan3',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Baichuan3-Turbo-128k',
|
|
||||||
provider: 'baichuan',
|
|
||||||
name: 'Baichuan3 Turbo 128k',
|
|
||||||
group: 'Baichuan3',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
dashscope: [
|
|
||||||
{
|
|
||||||
id: 'qwen-turbo',
|
|
||||||
provider: 'dashscope',
|
|
||||||
name: 'Qwen Turbo',
|
|
||||||
group: 'Qwen',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'qwen-plus',
|
|
||||||
provider: 'dashscope',
|
|
||||||
name: 'Qwen Plus',
|
|
||||||
group: 'Qwen',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'qwen-max',
|
|
||||||
provider: 'dashscope',
|
|
||||||
name: 'Qwen Max',
|
|
||||||
group: 'Qwen',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
stepfun: [
|
|
||||||
{
|
|
||||||
id: 'step-1-8k',
|
|
||||||
provider: 'stepfun',
|
|
||||||
name: 'Step 1 8K',
|
|
||||||
group: 'Step 1',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'step-1-flash',
|
|
||||||
provider: 'stepfun',
|
|
||||||
name: 'Step 1 Flash',
|
|
||||||
group: 'Step 1',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
doubao: [],
|
|
||||||
aihubmix: [
|
|
||||||
{
|
|
||||||
id: 'gpt-4o-mini',
|
|
||||||
provider: 'aihubmix',
|
|
||||||
name: 'GPT-4o Mini',
|
|
||||||
group: 'GPT-4o',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'aihubmix-Llama-3-70B-Instruct',
|
|
||||||
provider: 'aihubmix',
|
|
||||||
name: 'Llama 3 70B Instruct',
|
|
||||||
group: 'Llama3',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
openrouter: [
|
|
||||||
{
|
|
||||||
id: 'google/gemma-2-9b-it:free',
|
|
||||||
provider: 'openrouter',
|
|
||||||
name: 'Google: Gemma 2 9B',
|
|
||||||
group: 'Gemma',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'microsoft/phi-3-mini-128k-instruct:free',
|
|
||||||
provider: 'openrouter',
|
|
||||||
name: 'Phi-3 Mini 128K Instruct',
|
|
||||||
group: 'Phi',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'microsoft/phi-3-medium-128k-instruct:free',
|
|
||||||
provider: 'openrouter',
|
|
||||||
name: 'Phi-3 Medium 128K Instruct',
|
|
||||||
group: 'Phi',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'meta-llama/llama-3-8b-instruct:free',
|
|
||||||
provider: 'openrouter',
|
|
||||||
name: 'Meta: Llama 3 8B Instruct',
|
|
||||||
group: 'Llama3',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mistralai/mistral-7b-instruct:free',
|
|
||||||
provider: 'openrouter',
|
|
||||||
name: 'Mistral: Mistral 7B Instruct',
|
|
||||||
group: 'Mistral',
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
groq: [
|
|
||||||
{
|
|
||||||
id: 'llama3-8b-8192',
|
|
||||||
provider: 'groq',
|
|
||||||
name: 'LLaMA3 8B',
|
|
||||||
group: 'Llama3',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'llama3-70b-8192',
|
|
||||||
provider: 'groq',
|
|
||||||
name: 'LLaMA3 70B',
|
|
||||||
group: 'Llama3',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mixtral-8x7b-32768',
|
|
||||||
provider: 'groq',
|
|
||||||
name: 'Mixtral 8x7B',
|
|
||||||
group: 'Mixtral',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gemma-7b-it',
|
|
||||||
provider: 'groq',
|
|
||||||
name: 'Gemma 7B',
|
|
||||||
group: 'Gemma',
|
|
||||||
enabled: false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
anthropic: [
|
anthropic: [
|
||||||
@@ -441,29 +82,308 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'claude-3-5-sonnet-20240620',
|
id: 'claude-3-5-sonnet-20240620',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3.5 Sonnet',
|
name: 'Claude 3.5 Sonnet',
|
||||||
group: 'Claude 3.5',
|
group: 'Claude 3.5'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-3-opus-20240229',
|
id: 'claude-3-opus-20240229',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3 Opus',
|
name: 'Claude 3 Opus',
|
||||||
group: 'Claude 3',
|
group: 'Claude 3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-3-sonnet-20240229',
|
id: 'claude-3-sonnet-20240229',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3 Sonnet',
|
name: 'Claude 3 Sonnet',
|
||||||
group: 'Claude 3',
|
group: 'Claude 3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-3-haiku-20240307',
|
id: 'claude-3-haiku-20240307',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3 Haiku',
|
name: 'Claude 3 Haiku',
|
||||||
group: 'Claude 3',
|
group: 'Claude 3'
|
||||||
enabled: true
|
}
|
||||||
|
],
|
||||||
|
deepseek: [
|
||||||
|
{
|
||||||
|
id: 'deepseek-chat',
|
||||||
|
provider: 'deepseek',
|
||||||
|
name: 'DeepSeek Chat',
|
||||||
|
group: 'DeepSeek Chat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepseek-coder',
|
||||||
|
provider: 'deepseek',
|
||||||
|
name: 'DeepSeek Coder',
|
||||||
|
group: 'DeepSeek Coder'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yi: [
|
||||||
|
{
|
||||||
|
id: 'yi-large',
|
||||||
|
provider: 'yi',
|
||||||
|
name: 'Yi-Large',
|
||||||
|
group: 'Yi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yi-large-turbo',
|
||||||
|
provider: 'yi',
|
||||||
|
name: 'Yi-Large-Turbo',
|
||||||
|
group: 'Yi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yi-large-rag',
|
||||||
|
provider: 'yi',
|
||||||
|
name: 'Yi-Large-Rag',
|
||||||
|
group: 'Yi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yi-medium',
|
||||||
|
provider: 'yi',
|
||||||
|
name: 'Yi-Medium',
|
||||||
|
group: 'Yi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yi-medium-200k',
|
||||||
|
provider: 'yi',
|
||||||
|
name: 'Yi-Medium-200k',
|
||||||
|
group: 'Yi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yi-spark',
|
||||||
|
provider: 'yi',
|
||||||
|
name: 'Yi-Spark',
|
||||||
|
group: 'Yi'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
zhipu: [
|
||||||
|
{
|
||||||
|
id: 'glm-4',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4',
|
||||||
|
group: 'GLM-4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'glm-4-plus',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4-Plus',
|
||||||
|
group: 'GLM-4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'glm-4-air',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4-Air',
|
||||||
|
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-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-4-AllTools'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
moonshot: [
|
||||||
|
{
|
||||||
|
id: 'moonshot-v1-8k',
|
||||||
|
provider: 'moonshot',
|
||||||
|
name: 'Moonshot V1 8k',
|
||||||
|
group: 'Moonshot V1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'moonshot-v1-32k',
|
||||||
|
provider: 'moonshot',
|
||||||
|
name: 'Moonshot V1 32k',
|
||||||
|
group: 'Moonshot V1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'moonshot-v1-128k',
|
||||||
|
provider: 'moonshot',
|
||||||
|
name: 'Moonshot V1 128k',
|
||||||
|
group: 'Moonshot V1'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
baichuan: [
|
||||||
|
{
|
||||||
|
id: 'Baichuan4',
|
||||||
|
provider: 'baichuan',
|
||||||
|
name: 'Baichuan4',
|
||||||
|
group: 'Baichuan4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Baichuan3-Turbo',
|
||||||
|
provider: 'baichuan',
|
||||||
|
name: 'Baichuan3 Turbo',
|
||||||
|
group: 'Baichuan3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Baichuan3-Turbo-128k',
|
||||||
|
provider: 'baichuan',
|
||||||
|
name: 'Baichuan3 Turbo 128k',
|
||||||
|
group: 'Baichuan3'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dashscope: [
|
||||||
|
{
|
||||||
|
id: 'qwen-turbo',
|
||||||
|
provider: 'dashscope',
|
||||||
|
name: 'Qwen Turbo',
|
||||||
|
group: 'Qwen'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qwen-plus',
|
||||||
|
provider: 'dashscope',
|
||||||
|
name: 'Qwen Plus',
|
||||||
|
group: 'Qwen'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qwen-max',
|
||||||
|
provider: 'dashscope',
|
||||||
|
name: 'Qwen Max',
|
||||||
|
group: 'Qwen'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stepfun: [
|
||||||
|
{
|
||||||
|
id: 'step-1-8k',
|
||||||
|
provider: 'stepfun',
|
||||||
|
name: 'Step 1 8K',
|
||||||
|
group: 'Step 1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step-1-flash',
|
||||||
|
provider: 'stepfun',
|
||||||
|
name: 'Step 1 Flash',
|
||||||
|
group: 'Step 1'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
doubao: [],
|
||||||
|
minimax: [
|
||||||
|
{
|
||||||
|
id: 'abab6.5s-chat',
|
||||||
|
provider: 'minimax',
|
||||||
|
name: 'abab6.5s',
|
||||||
|
group: 'abab6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'abab6.5g-chat',
|
||||||
|
provider: 'minimax',
|
||||||
|
name: 'abab6.5g',
|
||||||
|
group: 'abab6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'abab6.5t-chat',
|
||||||
|
provider: 'minimax',
|
||||||
|
name: 'abab6.5t',
|
||||||
|
group: 'abab6'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'abab5.5s-chat',
|
||||||
|
provider: 'minimax',
|
||||||
|
name: 'abab5.5s',
|
||||||
|
group: 'abab5'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
aihubmix: [
|
||||||
|
{
|
||||||
|
id: 'gpt-4o-mini',
|
||||||
|
provider: 'aihubmix',
|
||||||
|
name: 'GPT-4o Mini',
|
||||||
|
group: 'GPT-4o'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'aihubmix-Llama-3-70B-Instruct',
|
||||||
|
provider: 'aihubmix',
|
||||||
|
name: 'Llama 3 70B Instruct',
|
||||||
|
group: 'Llama3'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
openrouter: [
|
||||||
|
{
|
||||||
|
id: 'google/gemma-2-9b-it:free',
|
||||||
|
provider: 'openrouter',
|
||||||
|
name: 'Google: Gemma 2 9B',
|
||||||
|
group: 'Gemma'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'microsoft/phi-3-mini-128k-instruct:free',
|
||||||
|
provider: 'openrouter',
|
||||||
|
name: 'Phi-3 Mini 128K Instruct',
|
||||||
|
group: 'Phi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'microsoft/phi-3-medium-128k-instruct:free',
|
||||||
|
provider: 'openrouter',
|
||||||
|
name: 'Phi-3 Medium 128K Instruct',
|
||||||
|
group: 'Phi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meta-llama/llama-3-8b-instruct:free',
|
||||||
|
provider: 'openrouter',
|
||||||
|
name: 'Meta: Llama 3 8B Instruct',
|
||||||
|
group: 'Llama3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mistralai/mistral-7b-instruct:free',
|
||||||
|
provider: 'openrouter',
|
||||||
|
name: 'Mistral: Mistral 7B Instruct',
|
||||||
|
group: 'Mistral'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
groq: [
|
||||||
|
{
|
||||||
|
id: 'llama3-8b-8192',
|
||||||
|
provider: 'groq',
|
||||||
|
name: 'LLaMA3 8B',
|
||||||
|
group: 'Llama3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llama3-70b-8192',
|
||||||
|
provider: 'groq',
|
||||||
|
name: 'LLaMA3 70B',
|
||||||
|
group: 'Llama3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mixtral-8x7b-32768',
|
||||||
|
provider: 'groq',
|
||||||
|
name: 'Mixtral 8x7B',
|
||||||
|
group: 'Mixtral'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemma-7b-it',
|
||||||
|
provider: 'groq',
|
||||||
|
name: 'Gemma 7B',
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
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 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 ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||||
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
||||||
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
||||||
@@ -7,13 +10,14 @@ import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
|
|||||||
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
|
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
|
||||||
import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
|
import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
|
||||||
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
||||||
|
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png'
|
||||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||||
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
|
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
|
||||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
||||||
import StepModelLogo from '@renderer/assets/images/models/step.jpg'
|
import StepModelLogo from '@renderer/assets/images/models/step.jpg'
|
||||||
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
|
import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
||||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
|
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
|
||||||
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
|
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
|
||||||
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
|
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
|
||||||
@@ -23,14 +27,15 @@ import DoubaoProviderLogo from '@renderer/assets/images/providers/doubao.png'
|
|||||||
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
|
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
|
||||||
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
|
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
|
||||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
||||||
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpg'
|
||||||
|
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpg'
|
||||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
|
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
|
||||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||||
import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png'
|
import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png'
|
||||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
|
import YiProviderLogo from '@renderer/assets/images/providers/yi.png'
|
||||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||||
|
|
||||||
export function getProviderLogo(providerId: string) {
|
export function getProviderLogo(providerId: string) {
|
||||||
@@ -69,6 +74,8 @@ export function getProviderLogo(providerId: string) {
|
|||||||
return DoubaoProviderLogo
|
return DoubaoProviderLogo
|
||||||
case 'graphrag-kylin-mountain':
|
case 'graphrag-kylin-mountain':
|
||||||
return GraphRagProviderLogo
|
return GraphRagProviderLogo
|
||||||
|
case 'minimax':
|
||||||
|
return MinimaxProviderLogo
|
||||||
default:
|
default:
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -98,6 +105,7 @@ export function getModelLogo(modelId: string) {
|
|||||||
bison: PalmModelLogo,
|
bison: PalmModelLogo,
|
||||||
palm: PalmModelLogo,
|
palm: PalmModelLogo,
|
||||||
step: StepModelLogo,
|
step: StepModelLogo,
|
||||||
|
abab: HailuoModelLogo,
|
||||||
'ep-202': DoubaoModelLogo
|
'ep-202': DoubaoModelLogo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +129,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://platform.openai.com/api-keys',
|
apiKey: 'https://platform.openai.com/api-keys',
|
||||||
docs: 'https://platform.openai.com/docs',
|
docs: 'https://platform.openai.com/docs',
|
||||||
models: 'https://platform.openai.com/docs/models'
|
models: 'https://platform.openai.com/docs/models'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'ChatGPT',
|
||||||
|
url: 'https://chatgpt.com/',
|
||||||
|
logo: OpenAiProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
@@ -133,6 +146,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://aistudio.google.com/app/apikey',
|
apiKey: 'https://aistudio.google.com/app/apikey',
|
||||||
docs: 'https://ai.google.dev/gemini-api/docs',
|
docs: 'https://ai.google.dev/gemini-api/docs',
|
||||||
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
|
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'Gemini',
|
||||||
|
url: 'https://gemini.google.com/',
|
||||||
|
logo: GeminiProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
silicon: {
|
silicon: {
|
||||||
@@ -145,6 +163,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
|
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
|
||||||
docs: 'https://docs.siliconflow.cn/',
|
docs: 'https://docs.siliconflow.cn/',
|
||||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'SiliconFlow',
|
||||||
|
url: 'https://cloud.siliconflow.cn/playground/chat',
|
||||||
|
logo: SiliconFlowProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deepseek: {
|
deepseek: {
|
||||||
@@ -157,6 +180,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://platform.deepseek.com/api_keys',
|
apiKey: 'https://platform.deepseek.com/api_keys',
|
||||||
docs: 'https://platform.deepseek.com/api-docs/',
|
docs: 'https://platform.deepseek.com/api-docs/',
|
||||||
models: 'https://platform.deepseek.com/api-docs/'
|
models: 'https://platform.deepseek.com/api-docs/'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'DeepSeek',
|
||||||
|
url: 'https://chat.deepseek.com/',
|
||||||
|
logo: DeepSeekProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
yi: {
|
yi: {
|
||||||
@@ -169,6 +197,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
|
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
|
||||||
docs: 'https://platform.lingyiwanwu.com/docs',
|
docs: 'https://platform.lingyiwanwu.com/docs',
|
||||||
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
|
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'Yi',
|
||||||
|
url: 'https://www.wanzhi.com/',
|
||||||
|
logo: YiProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zhipu: {
|
zhipu: {
|
||||||
@@ -181,6 +214,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
|
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
|
||||||
docs: 'https://open.bigmodel.cn/dev/howuse/introduction',
|
docs: 'https://open.bigmodel.cn/dev/howuse/introduction',
|
||||||
models: 'https://open.bigmodel.cn/modelcenter/square'
|
models: 'https://open.bigmodel.cn/modelcenter/square'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: '智谱',
|
||||||
|
url: 'https://chatglm.cn/main/alltoolsdetail',
|
||||||
|
logo: ZhipuProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
moonshot: {
|
moonshot: {
|
||||||
@@ -193,6 +231,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://platform.moonshot.cn/console/api-keys',
|
apiKey: 'https://platform.moonshot.cn/console/api-keys',
|
||||||
docs: 'https://platform.moonshot.cn/docs/',
|
docs: 'https://platform.moonshot.cn/docs/',
|
||||||
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
|
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'Kimi',
|
||||||
|
url: 'https://kimi.moonshot.cn/',
|
||||||
|
logo: KimiAppLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
baichuan: {
|
baichuan: {
|
||||||
@@ -205,6 +248,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
|
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
|
||||||
docs: 'https://platform.baichuan-ai.com/docs',
|
docs: 'https://platform.baichuan-ai.com/docs',
|
||||||
models: 'https://platform.baichuan-ai.com/price'
|
models: 'https://platform.baichuan-ai.com/price'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: '百小应',
|
||||||
|
url: 'https://ying.baichuan-ai.com/chat',
|
||||||
|
logo: BaicuanAppLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dashscope: {
|
dashscope: {
|
||||||
@@ -217,6 +265,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
|
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
|
||||||
docs: 'https://help.aliyun.com/zh/dashscope/',
|
docs: 'https://help.aliyun.com/zh/dashscope/',
|
||||||
models: 'https://dashscope.console.aliyun.com/model'
|
models: 'https://dashscope.console.aliyun.com/model'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: '通义千问',
|
||||||
|
url: 'https://tongyi.aliyun.com/qianwen/',
|
||||||
|
logo: QwenModelLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stepfun: {
|
stepfun: {
|
||||||
@@ -229,6 +282,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://platform.stepfun.com/interface-key',
|
apiKey: 'https://platform.stepfun.com/interface-key',
|
||||||
docs: 'https://platform.stepfun.com/docs/overview/concept',
|
docs: 'https://platform.stepfun.com/docs/overview/concept',
|
||||||
models: 'https://platform.stepfun.com/docs/llm/text'
|
models: 'https://platform.stepfun.com/docs/llm/text'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: '跃问',
|
||||||
|
url: 'https://yuewen.cn/chats/new',
|
||||||
|
logo: YuewenAppLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
doubao: {
|
doubao: {
|
||||||
@@ -241,6 +299,28 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
|
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
|
||||||
docs: 'https://www.volcengine.com/docs/82379/1182403',
|
docs: 'https://www.volcengine.com/docs/82379/1182403',
|
||||||
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
|
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: '豆包',
|
||||||
|
url: 'https://www.doubao.com/chat/',
|
||||||
|
logo: DoubaoProviderLogo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
minimax: {
|
||||||
|
api: {
|
||||||
|
url: 'https://api.minimax.chat/v1/',
|
||||||
|
editable: true
|
||||||
|
},
|
||||||
|
websites: {
|
||||||
|
official: 'https://platform.minimaxi.com/',
|
||||||
|
apiKey: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
|
||||||
|
docs: 'https://platform.minimaxi.com/document/Announcement',
|
||||||
|
models: 'https://platform.minimaxi.com/document/Models'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: '海螺',
|
||||||
|
url: 'https://hailuoai.com/',
|
||||||
|
logo: HailuoModelLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'graphrag-kylin-mountain': {
|
'graphrag-kylin-mountain': {
|
||||||
@@ -271,6 +351,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://console.groq.com/keys',
|
apiKey: 'https://console.groq.com/keys',
|
||||||
docs: 'https://console.groq.com/docs/quickstart',
|
docs: 'https://console.groq.com/docs/quickstart',
|
||||||
models: 'https://console.groq.com/docs/models'
|
models: 'https://console.groq.com/docs/models'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'Groq',
|
||||||
|
url: 'https://chat.groq.com/',
|
||||||
|
logo: GroqProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ollama: {
|
ollama: {
|
||||||
@@ -294,6 +379,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
apiKey: 'https://console.anthropic.com/settings/keys',
|
apiKey: 'https://console.anthropic.com/settings/keys',
|
||||||
docs: 'https://docs.anthropic.com/en/docs',
|
docs: 'https://docs.anthropic.com/en/docs',
|
||||||
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
|
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
name: 'Claude',
|
||||||
|
url: 'https://claude.ai/',
|
||||||
|
logo: AnthropicProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
aihubmix: {
|
aihubmix: {
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#00b96b',
|
colorPrimary: '#00b96b'
|
||||||
borderRadius: 8
|
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
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 { MessageInstance } from 'antd/es/message/interface'
|
||||||
import { HookAPI } from 'antd/es/modal/useModal'
|
import { HookAPI } from 'antd/es/modal/useModal'
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
message: MessageInstance
|
message: MessageInstance
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
import LocalStorage from '@renderer/services/storage'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
@@ -5,12 +6,14 @@ import { setAvatar } from '@renderer/store/runtime'
|
|||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { useDefaultModel } from './useAssistant'
|
||||||
import { useSettings } from './useSettings'
|
import { useSettings } from './useSettings'
|
||||||
|
|
||||||
export function useAppInit() {
|
export function useAppInit() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { proxyUrl } = useSettings()
|
const { proxyUrl } = useSettings()
|
||||||
const { language } = useSettings()
|
const { language } = useSettings()
|
||||||
|
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
@@ -33,4 +36,14 @@ export function useAppInit() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(language || navigator.language || 'en-US')
|
i18n.changeLanguage(language || navigator.language || 'en-US')
|
||||||
}, [language])
|
}, [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
|
||||||
|
}, [])
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/renderer/src/hooks/useBridge.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export function useBridge() {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = async (event: MessageEvent) => {
|
||||||
|
const targetOrigin = { targetOrigin: '*' }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (event.origin !== 'file://') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, method, args, id } = event.data
|
||||||
|
|
||||||
|
if (type !== 'api-call' || !window.api) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiMethod = window.api[method]
|
||||||
|
|
||||||
|
if (typeof apiMethod !== 'function') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.source?.postMessage(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
type: 'api-response',
|
||||||
|
result: await apiMethod(...args)
|
||||||
|
},
|
||||||
|
targetOrigin
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
event.source?.postMessage(
|
||||||
|
{
|
||||||
|
id: event.data?.id,
|
||||||
|
type: 'api-response',
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
},
|
||||||
|
targetOrigin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
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,
|
SendMessageShortcut,
|
||||||
setSendMessageShortcut as _setSendMessageShortcut,
|
setSendMessageShortcut as _setSendMessageShortcut,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
setTopicPosition,
|
||||||
|
setWindowStyle,
|
||||||
ThemeMode
|
ThemeMode
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
|
|
||||||
@@ -17,6 +19,12 @@ export function useSettings() {
|
|||||||
},
|
},
|
||||||
setTheme(theme: ThemeMode) {
|
setTheme(theme: ThemeMode) {
|
||||||
dispatch(setTheme(theme))
|
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 { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setShowRightSidebar, toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
|
import { setShowTopics, toggleShowAssistants, toggleShowTopics } 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useShowAssistants() {
|
export function useShowAssistants() {
|
||||||
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
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() {
|
export function useRuntime() {
|
||||||
return useAppSelector((state) => state.runtime)
|
return useAppSelector((state) => state.runtime)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { Assistant, Topic } from '@renderer/types'
|
|||||||
import { find } from 'lodash'
|
import { find } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { useAssistant } from './useAssistant'
|
||||||
|
|
||||||
let _activeTopic: Topic
|
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])
|
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ const resources = {
|
|||||||
footnotes: 'References',
|
footnotes: 'References',
|
||||||
select: 'Select',
|
select: 'Select',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
default: 'Default'
|
default: 'Default',
|
||||||
|
warning: 'Warning',
|
||||||
|
back: 'Back',
|
||||||
|
chat: 'Chat'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
@@ -48,11 +51,15 @@ const resources = {
|
|||||||
'api.connection.failed': 'Connection failed',
|
'api.connection.failed': 'Connection failed',
|
||||||
'api.connection.success': 'Connection successful',
|
'api.connection.success': 'Connection successful',
|
||||||
'chat.completion.paused': 'Chat completion paused',
|
'chat.completion.paused': 'Chat completion paused',
|
||||||
'switch.disabled': 'Switching is disabled while the assistant is generating'
|
'switch.disabled': 'Switching is disabled while the assistant is generating',
|
||||||
|
'restore.success': 'Restored successfully',
|
||||||
|
'reset.confirm.content': 'Are you sure you want to clear all data?',
|
||||||
|
'reset.double.confirm.title': 'DATA LOST !!!',
|
||||||
|
'reset.double.confirm.content': 'All data will be lost, do you want to continue?'
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
save: 'Save',
|
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.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||||
'default.topic.name': 'Default Topic',
|
'default.topic.name': 'Default Topic',
|
||||||
'topics.title': 'Topics',
|
'topics.title': 'Topics',
|
||||||
@@ -61,6 +68,7 @@ const resources = {
|
|||||||
'topics.edit.placeholder': 'Enter new name',
|
'topics.edit.placeholder': 'Enter new name',
|
||||||
'topics.delete.all.title': 'Delete all topics',
|
'topics.delete.all.title': 'Delete all topics',
|
||||||
'topics.delete.all.content': 'Are you sure you want to 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.new_topic': 'New Topic',
|
||||||
'input.topics': ' Topics ',
|
'input.topics': ' Topics ',
|
||||||
'input.clear': 'Clear',
|
'input.clear': 'Clear',
|
||||||
@@ -72,6 +80,7 @@ const resources = {
|
|||||||
'input.send': 'Send',
|
'input.send': 'Send',
|
||||||
'input.pause': 'Pause',
|
'input.pause': 'Pause',
|
||||||
'input.settings': 'Settings',
|
'input.settings': 'Settings',
|
||||||
|
'input.upload': 'Upload image png、jpg、jpeg',
|
||||||
'input.context_count.tip': 'Context Count',
|
'input.context_count.tip': 'Context Count',
|
||||||
'input.estimated_tokens.tip': 'Estimated tokens',
|
'input.estimated_tokens.tip': 'Estimated tokens',
|
||||||
'settings.temperature': 'Temperature',
|
'settings.temperature': 'Temperature',
|
||||||
@@ -121,6 +130,7 @@ const resources = {
|
|||||||
aihubmix: 'AiHubMix',
|
aihubmix: 'AiHubMix',
|
||||||
stepfun: 'StepFun',
|
stepfun: 'StepFun',
|
||||||
doubao: 'Doubao',
|
doubao: 'Doubao',
|
||||||
|
minimax: 'MiniMax',
|
||||||
'graphrag-kylin-mountain': 'GraphRAG'
|
'graphrag-kylin-mountain': 'GraphRAG'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
@@ -140,6 +150,11 @@ const resources = {
|
|||||||
'general.title': 'General Settings',
|
'general.title': 'General Settings',
|
||||||
'general.user_name': 'User Name',
|
'general.user_name': 'User Name',
|
||||||
'general.user_name.placeholder': 'Enter your 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',
|
'provider.api_key': 'API Key',
|
||||||
'provider.check': 'Check',
|
'provider.check': 'Check',
|
||||||
'provider.get_api_key': 'Get API Key',
|
'provider.get_api_key': 'Get API Key',
|
||||||
@@ -189,7 +204,13 @@ const resources = {
|
|||||||
'theme.dark': 'Dark',
|
'theme.dark': 'Dark',
|
||||||
'theme.light': 'Light',
|
'theme.light': 'Light',
|
||||||
'theme.auto': 'Auto',
|
'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: {
|
translate: {
|
||||||
title: 'Translation',
|
title: 'Translation',
|
||||||
@@ -218,8 +239,16 @@ const resources = {
|
|||||||
'keep_alive_time.placeholder': 'Minutes',
|
'keep_alive_time.placeholder': 'Minutes',
|
||||||
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
|
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
|
||||||
},
|
},
|
||||||
|
minapp: {
|
||||||
|
title: 'MinApp'
|
||||||
|
},
|
||||||
error: {
|
error: {
|
||||||
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers'
|
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers',
|
||||||
|
'backup.file_format': 'Backup file format error'
|
||||||
|
},
|
||||||
|
words: {
|
||||||
|
knowledgeGraph: 'Knowledge Graph',
|
||||||
|
visualization: 'Visualization'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -248,7 +277,10 @@ const resources = {
|
|||||||
footnote: '引用内容',
|
footnote: '引用内容',
|
||||||
select: '选择',
|
select: '选择',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
default: '默认'
|
default: '默认',
|
||||||
|
warning: '警告',
|
||||||
|
back: '返回',
|
||||||
|
chat: '聊天'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: '添加',
|
add: '添加',
|
||||||
@@ -268,11 +300,15 @@ const resources = {
|
|||||||
'api.connection.failed': '连接失败',
|
'api.connection.failed': '连接失败',
|
||||||
'api.connection.success': '连接成功',
|
'api.connection.success': '连接成功',
|
||||||
'chat.completion.paused': '会话已停止',
|
'chat.completion.paused': '会话已停止',
|
||||||
'switch.disabled': '模型回复完成后才能切换'
|
'switch.disabled': '模型回复完成后才能切换',
|
||||||
|
'restore.success': '恢复成功',
|
||||||
|
'reset.confirm.content': '确定要重置所有数据吗?',
|
||||||
|
'reset.double.confirm.title': '数据丢失!!!',
|
||||||
|
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?'
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
save: '保存',
|
save: '保存',
|
||||||
'default.name': '🔆 默认助手 - Assistant',
|
'default.name': '默认助手',
|
||||||
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
|
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
|
||||||
'default.topic.name': '默认话题',
|
'default.topic.name': '默认话题',
|
||||||
'topics.title': '话题',
|
'topics.title': '话题',
|
||||||
@@ -281,6 +317,7 @@ const resources = {
|
|||||||
'topics.edit.placeholder': '输入新名称',
|
'topics.edit.placeholder': '输入新名称',
|
||||||
'topics.delete.all.title': '删除所有话题',
|
'topics.delete.all.title': '删除所有话题',
|
||||||
'topics.delete.all.content': '确定要删除所有话题吗?',
|
'topics.delete.all.content': '确定要删除所有话题吗?',
|
||||||
|
'topics.list': '话题列表',
|
||||||
'input.new_topic': '新话题',
|
'input.new_topic': '新话题',
|
||||||
'input.topics': ' 话题 ',
|
'input.topics': ' 话题 ',
|
||||||
'input.clear': '清除',
|
'input.clear': '清除',
|
||||||
@@ -292,6 +329,7 @@ const resources = {
|
|||||||
'input.send': '发送',
|
'input.send': '发送',
|
||||||
'input.pause': '暂停',
|
'input.pause': '暂停',
|
||||||
'input.settings': '设置',
|
'input.settings': '设置',
|
||||||
|
'input.upload': '上传图片 png、jpg、jpeg',
|
||||||
'input.context_count.tip': '上下文数',
|
'input.context_count.tip': '上下文数',
|
||||||
'input.estimated_tokens.tip': '预估 token 数',
|
'input.estimated_tokens.tip': '预估 token 数',
|
||||||
'settings.temperature': '模型温度',
|
'settings.temperature': '模型温度',
|
||||||
@@ -342,6 +380,7 @@ const resources = {
|
|||||||
aihubmix: 'AiHubMix',
|
aihubmix: 'AiHubMix',
|
||||||
stepfun: '阶跃星辰',
|
stepfun: '阶跃星辰',
|
||||||
doubao: '豆包',
|
doubao: '豆包',
|
||||||
|
minimax: 'MiniMax',
|
||||||
'graphrag-kylin-mountain': 'GraphRAG'
|
'graphrag-kylin-mountain': 'GraphRAG'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
@@ -361,6 +400,11 @@ const resources = {
|
|||||||
'general.title': '常规设置',
|
'general.title': '常规设置',
|
||||||
'general.user_name': '用户名',
|
'general.user_name': '用户名',
|
||||||
'general.user_name.placeholder': '请输入用户名',
|
'general.user_name.placeholder': '请输入用户名',
|
||||||
|
'general.backup.title': '数据备份与恢复',
|
||||||
|
'general.backup.button': '备份',
|
||||||
|
'general.restore.button': '恢复',
|
||||||
|
'general.reset.title': '重置数据',
|
||||||
|
'general.reset.button': '重置',
|
||||||
'provider.api_key': 'API 密钥',
|
'provider.api_key': 'API 密钥',
|
||||||
'provider.check': '检查',
|
'provider.check': '检查',
|
||||||
'provider.get_api_key': '点击这里获取密钥',
|
'provider.get_api_key': '点击这里获取密钥',
|
||||||
@@ -410,7 +454,13 @@ const resources = {
|
|||||||
'theme.dark': '深色主题',
|
'theme.dark': '深色主题',
|
||||||
'theme.light': '浅色主题',
|
'theme.light': '浅色主题',
|
||||||
'theme.auto': '跟随系统',
|
'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: {
|
translate: {
|
||||||
title: '翻译',
|
title: '翻译',
|
||||||
@@ -439,8 +489,16 @@ const resources = {
|
|||||||
'keep_alive_time.placeholder': '分钟',
|
'keep_alive_time.placeholder': '分钟',
|
||||||
'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)'
|
'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)'
|
||||||
},
|
},
|
||||||
|
minapp: {
|
||||||
|
title: '小程序'
|
||||||
|
},
|
||||||
error: {
|
error: {
|
||||||
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥'
|
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥',
|
||||||
|
'backup.file_format': '备份文件格式错误'
|
||||||
|
},
|
||||||
|
words: {
|
||||||
|
knowledgeGraph: '知识图谱',
|
||||||
|
visualization: '可视化'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,9 @@
|
|||||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||||
import * as Sentry from '@sentry/electron/renderer'
|
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
|
|
||||||
|
import { APP_NAME } from './config/env'
|
||||||
import { ThemeMode } from './store/settings'
|
import { ThemeMode } from './store/settings'
|
||||||
import { isProduction, loadScript } from './utils'
|
import { 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initMermaid(theme: ThemeMode) {
|
export async function initMermaid(theme: ThemeMode) {
|
||||||
if (!window.mermaid) {
|
if (!window.mermaid) {
|
||||||
@@ -41,13 +23,11 @@ function init() {
|
|||||||
name: 'CherryAI',
|
name: 'CherryAI',
|
||||||
version: 1.0,
|
version: 1.0,
|
||||||
storeName: 'cherryai',
|
storeName: 'cherryai',
|
||||||
description: 'Cherry Studio Storage'
|
description: `${APP_NAME} Storage`
|
||||||
})
|
})
|
||||||
|
|
||||||
window.keyv = new KeyvStorage()
|
window.keyv = new KeyvStorage()
|
||||||
window.keyv.init()
|
window.keyv.init()
|
||||||
|
|
||||||
initSentry()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import './assets/styles/index.scss'
|
import './assets/styles/index.scss'
|
||||||
import './init'
|
import './init'
|
||||||
import './i18n'
|
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ const AppsPage: FC = () => {
|
|||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<AssistantsContainer>
|
<AssistantsContainer>
|
||||||
<HStack alignItems="center" style={{ marginBottom: 16 }}>
|
<HStack alignItems="center" style={{ marginBottom: 16 }}>
|
||||||
<Title level={3}>{t('agents.my_agents')}</Title>
|
<Title level={4}>{t('agents.my_agents')}</Title>
|
||||||
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
|
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
|
||||||
</HStack>
|
</HStack>
|
||||||
<UserAgents onAdd={onAddAgentConfirm} />
|
<UserAgents onAdd={onAddAgentConfirm} />
|
||||||
{Object.keys(agentGroups).map((group) => (
|
{Object.keys(agentGroups).map((group) => (
|
||||||
<div key={group}>
|
<div key={group}>
|
||||||
<Title level={3} key={group} style={{ marginBottom: 16 }}>
|
<Title level={4} key={group} style={{ marginBottom: 16 }}>
|
||||||
{group}
|
{group}
|
||||||
</Title>
|
</Title>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
import { Col, Typography } from 'antd'
|
import { Col } from 'antd'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -7,17 +7,13 @@ interface Props {
|
|||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Title } = Typography
|
|
||||||
|
|
||||||
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
|
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<Container onClick={onClick}>
|
<Container onClick={onClick}>
|
||||||
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
|
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
|
||||||
<Col>
|
<Col>
|
||||||
<AgentHeader>
|
<AgentHeader>
|
||||||
<AgentName level={5} style={{ marginBottom: 0 }}>
|
<AgentName style={{ marginBottom: 0 }}>{agent.name}</AgentName>
|
||||||
{agent.name}
|
|
||||||
</AgentName>
|
|
||||||
</AgentHeader>
|
</AgentHeader>
|
||||||
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
|
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -41,14 +37,14 @@ const Container = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
const EmojiHeader = styled.div`
|
const EmojiHeader = styled.div`
|
||||||
width: 25px;
|
width: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
font-size: 25px;
|
font-size: 20px;
|
||||||
line-height: 25px;
|
line-height: 20px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AgentHeader = styled.div`
|
const AgentHeader = styled.div`
|
||||||
@@ -58,15 +54,13 @@ const AgentHeader = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AgentName = styled(Title)`
|
const AgentName = styled.div`
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--color-white);
|
color: var(--color-text-1);
|
||||||
font-weight: 900;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const AgentCardPrompt = styled.div`
|
const AgentCardPrompt = styled.div`
|
||||||
@@ -76,6 +70,7 @@ const AgentCardPrompt = styled.div`
|
|||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default AgentCard
|
export default AgentCard
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ const AssistantCardContainer = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px dashed var(--color-border);
|
border: 1px dashed var(--color-border-soft);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 84px;
|
min-height: 72px;
|
||||||
.anticon {
|
.anticon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
|
|||||||
51
src/renderer/src/pages/apps/App.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import MinApp from '@renderer/components/MinApp'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: MinAppType
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: FC<Props> = ({ app }) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
MinApp.start(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container onClick={onClick}>
|
||||||
|
<AppIcon src={app.logo} style={{ border: theme === 'dark' ? 'none' : '0.5px solid var(--color-border' }} />
|
||||||
|
<AppTitle>{app.name}</AppTitle>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 65px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AppIcon = styled.img`
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 16px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AppTitle = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--color-text-soft);
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default App
|
||||||
87
src/renderer/src/pages/apps/AppsPage.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { SearchOutlined } from '@ant-design/icons'
|
||||||
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
|
import { Center } from '@renderer/components/Layout'
|
||||||
|
import { getAllMinApps } from '@renderer/config/minapp'
|
||||||
|
import { Empty, Input } from 'antd'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
const list = getAllMinApps()
|
||||||
|
|
||||||
|
const AppsPage: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
const apps = search
|
||||||
|
? list.filter(
|
||||||
|
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
: list
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Navbar>
|
||||||
|
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||||
|
{t('minapp.title')}
|
||||||
|
<Input
|
||||||
|
placeholder={t('common.search')}
|
||||||
|
className="nodrag"
|
||||||
|
style={{ width: '30%', height: 28 }}
|
||||||
|
size="small"
|
||||||
|
variant="filled"
|
||||||
|
suffix={<SearchOutlined />}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div style={{ width: 80 }} />
|
||||||
|
</NavbarCenter>
|
||||||
|
</Navbar>
|
||||||
|
<ContentContainer>
|
||||||
|
<AppsContainer>
|
||||||
|
{apps.map((app) => (
|
||||||
|
<App key={app.name} app={app} />
|
||||||
|
))}
|
||||||
|
{isEmpty(apps) && (
|
||||||
|
<Center style={{ flex: 1 }}>
|
||||||
|
<Empty />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</AppsContainer>
|
||||||
|
</ContentContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContentContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
padding: 50px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AppsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
min-width: 900px;
|
||||||
|
max-width: 900px;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: flex-start;
|
||||||
|
gap: 50px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default AppsPage
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
import { CopyOutlined, DeleteOutlined, EditOutlined, MinusCircleOutlined } from '@ant-design/icons'
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
import DragableList from '@renderer/components/DragableList'
|
||||||
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
|
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { droppableReorder, uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Dropdown } from 'antd'
|
import { Dropdown } from 'antd'
|
||||||
import { ItemType } from 'antd/es/menu/interface'
|
import { ItemType } from 'antd/es/menu/interface'
|
||||||
import { last } from 'lodash'
|
import { last } from 'lodash'
|
||||||
@@ -23,7 +24,8 @@ interface Props {
|
|||||||
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
||||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||||
const generating = useAppSelector((state) => state.runtime.generating)
|
const generating = useAppSelector((state) => state.runtime.generating)
|
||||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id)
|
||||||
|
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
@@ -35,6 +37,15 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
|
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onEditAssistant = useCallback(
|
||||||
|
async (assistant: Assistant) => {
|
||||||
|
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||||
|
updateAssistant(_assistant)
|
||||||
|
syncAsistantToAgent(_assistant)
|
||||||
|
},
|
||||||
|
[updateAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
const getMenuItems = useCallback(
|
const getMenuItems = useCallback(
|
||||||
(assistant: Assistant) =>
|
(assistant: Assistant) =>
|
||||||
[
|
[
|
||||||
@@ -42,11 +53,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
label: t('common.edit'),
|
label: t('common.edit'),
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
async onClick() {
|
onClick: () => onEditAssistant(assistant)
|
||||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
|
||||||
updateAssistant(_assistant)
|
|
||||||
syncAsistantToAgent(_assistant)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('common.duplicate'),
|
label: t('common.duplicate'),
|
||||||
@@ -58,6 +65,19 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
setActiveAssistant(_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'),
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
onOk: removeAllTopics
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
label: t('common.delete'),
|
label: t('common.delete'),
|
||||||
@@ -67,19 +87,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
onClick: () => onDelete(assistant)
|
onClick: () => onDelete(assistant)
|
||||||
}
|
}
|
||||||
] as ItemType[],
|
] as ItemType[],
|
||||||
[addAssistant, onDelete, setActiveAssistant, t, updateAssistant]
|
[addAssistant, onDelete, onEditAssistant, removeAllTopics, setActiveAssistant, t]
|
||||||
)
|
|
||||||
|
|
||||||
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]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSwitchAssistant = useCallback(
|
const onSwitchAssistant = useCallback(
|
||||||
@@ -90,6 +98,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
key: 'switch-assistant'
|
key: 'switch-assistant'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||||
setActiveAssistant(assistant)
|
setActiveAssistant(assistant)
|
||||||
},
|
},
|
||||||
@@ -98,33 +107,24 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragableList list={assistants} onUpdate={updateAssistants}>
|
||||||
<Droppable droppableId="droppable">
|
{(assistant) => {
|
||||||
{(provided) => (
|
const isCurrent = assistant.id === activeAssistant?.id
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
return (
|
||||||
{assistants.map((assistant, index) => (
|
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
||||||
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
|
<AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}>
|
||||||
{(provided) => (
|
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
|
||||||
<div
|
<ArrowRightButton
|
||||||
ref={provided.innerRef}
|
className={`arrow-button ${isCurrent && showTopics ? 'active' : ''}`}
|
||||||
{...provided.draggableProps}
|
onClick={() => isCurrent && toggleShowTopics()}>
|
||||||
{...provided.dragHandleProps}
|
<i className="iconfont icon-gridlines" />
|
||||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
</ArrowRightButton>
|
||||||
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
{false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>}
|
||||||
<AssistantItem
|
</AssistantItem>
|
||||||
onClick={() => onSwitchAssistant(assistant)}
|
</Dropdown>
|
||||||
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
|
)
|
||||||
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
|
}}
|
||||||
</AssistantItem>
|
</DragableList>
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -136,34 +136,38 @@ const Container = styled.div`
|
|||||||
max-width: var(--assistants-width);
|
max-width: var(--assistants-width);
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
padding: 10px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AssistantItem = styled.div`
|
const AssistantItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
|
margin: 0 10px;
|
||||||
|
padding-right: 35px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
.anticon {
|
.iconfont {
|
||||||
display: none;
|
opacity: 0;
|
||||||
}
|
color: var(--color-text-3);
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
.anticon {
|
|
||||||
display: block;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--color-background-mute);
|
background-color: var(--color-background-mute);
|
||||||
cursor: pointer;
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.topics-count {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.iconfont {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -173,6 +177,46 @@ const AssistantName = styled.div`
|
|||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
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
|
export default Assistants
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Assistant } from '@renderer/types'
|
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { Flex } from 'antd'
|
import { Flex } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Inputbar from './Inputbar/Inputbar'
|
import Inputbar from './Inputbar/Inputbar'
|
||||||
import Messages from './Messages'
|
import Messages from './Messages/Messages'
|
||||||
import RightSidebar from './RightSidebar'
|
import Topics from './Topics'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
|
activeTopic: Topic
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<Props> = (props) => {
|
const Chat: FC<Props> = (props) => {
|
||||||
const { assistant } = useAssistant(props.assistant.id)
|
const { assistant } = useAssistant(props.assistant.id)
|
||||||
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
|
const { showTopics } = useShowTopics()
|
||||||
|
const { topicPosition } = useSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="chat">
|
<Container id="chat">
|
||||||
|
{showTopics && topicPosition === 'left' && (
|
||||||
|
<Topics assistant={assistant} activeTopic={props.activeTopic} setActiveTopic={props.setActiveTopic} />
|
||||||
|
)}
|
||||||
<Main vertical flex={1} justify="space-between">
|
<Main vertical flex={1} justify="space-between">
|
||||||
<Messages assistant={assistant} topic={activeTopic} />
|
<Messages assistant={assistant} topic={props.activeTopic} setActiveTopic={props.setActiveTopic} />
|
||||||
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
|
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} />
|
||||||
</Main>
|
</Main>
|
||||||
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
{showTopics && topicPosition === 'right' && (
|
||||||
|
<Topics assistant={assistant} activeTopic={props.activeTopic} setActiveTopic={props.setActiveTopic} />
|
||||||
|
)}
|
||||||
</Container>
|
</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,28 +1,24 @@
|
|||||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
|
||||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Switch } from 'antd'
|
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Assistants from './Assistants'
|
import Assistants from './Assistants'
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
import AddAssistantPopup from './components/AddAssistantPopup'
|
import Navbar from './Navbar'
|
||||||
import Navigation from './Header'
|
|
||||||
|
|
||||||
let _activeAssistant: Assistant
|
let _activeAssistant: Assistant
|
||||||
|
|
||||||
const HomePage: FC = () => {
|
const HomePage: FC = () => {
|
||||||
const { assistants, addAssistant } = useAssistants()
|
const { assistants, addAssistant } = useAssistants()
|
||||||
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
|
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
|
||||||
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
|
const { showAssistants } = useShowAssistants()
|
||||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
|
||||||
const { defaultAssistant } = useDefaultAssistant()
|
const { defaultAssistant } = useDefaultAssistant()
|
||||||
const { theme, toggleTheme } = useTheme()
|
|
||||||
|
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant)
|
||||||
|
|
||||||
_activeAssistant = activeAssistant
|
_activeAssistant = activeAssistant
|
||||||
|
|
||||||
@@ -32,37 +28,18 @@ const HomePage: FC = () => {
|
|||||||
setActiveAssistant(assistant)
|
setActiveAssistant(assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCreateAssistant = async () => {
|
const onSetActiveTopic = (topic: Topic) => {
|
||||||
const assistant = await AddAssistantPopup.show()
|
setActiveTopic(topic)
|
||||||
assistant && setActiveAssistant(assistant)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<Navbar
|
||||||
{showAssistants && (
|
activeAssistant={activeAssistant}
|
||||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
setActiveAssistant={setActiveAssistant}
|
||||||
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
activeTopic={activeTopic}
|
||||||
<i className="iconfont icon-hidesidebarhoriz" />
|
setActiveTopic={onSetActiveTopic}
|
||||||
</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>
|
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
{showAssistants && (
|
{showAssistants && (
|
||||||
<Assistants
|
<Assistants
|
||||||
@@ -71,7 +48,7 @@ const HomePage: FC = () => {
|
|||||||
onCreateAssistant={onCreateDefaultAssistant}
|
onCreateAssistant={onCreateDefaultAssistant}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Chat assistant={activeAssistant} />
|
<Chat assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={onSetActiveTopic} />
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@@ -90,40 +67,4 @@ const ContentContainer = styled.div`
|
|||||||
background-color: var(--color-background);
|
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
|
export default HomePage
|
||||||
|
|||||||
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
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
|
FormOutlined,
|
||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
PlusCircleOutlined,
|
|
||||||
QuestionCircleOutlined
|
QuestionCircleOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { estimateInputTokenCount } from '@renderer/services/messages'
|
import { estimateInputTokenCount } from '@renderer/services/messages'
|
||||||
@@ -18,14 +19,15 @@ import store, { useAppSelector } from '@renderer/store'
|
|||||||
import { setGenerating } from '@renderer/store/runtime'
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
import { Assistant, Message, Topic } from '@renderer/types'
|
import { Assistant, Message, Topic } from '@renderer/types'
|
||||||
import { delay, uuid } from '@renderer/utils'
|
import { delay, uuid } from '@renderer/utils'
|
||||||
import { Button, Divider, Popconfirm, Tag, Tooltip } from 'antd'
|
import { Button, Divider, Popconfirm, Popover, Tag, Tooltip } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { debounce, isEmpty } from 'lodash'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import SettingsTab from '../Settings'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -43,8 +45,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const [expended, setExpend] = useState(false)
|
const [expended, setExpend] = useState(false)
|
||||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||||
const generating = useAppSelector((state) => state.runtime.generating)
|
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 { t } = useTranslation()
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
const { toggleShowTopics } = useShowTopics()
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
|
|
||||||
@@ -67,12 +72,19 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
message.files = files
|
||||||
|
}
|
||||||
|
|
||||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||||
|
|
||||||
setText('')
|
setText('')
|
||||||
|
setFiles([])
|
||||||
|
setTimeout(() => setText(''), 500)
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
|
||||||
setExpend(false)
|
setExpend(false)
|
||||||
}, [assistant.id, assistant.topics, generating, text])
|
}, [assistant.id, assistant.topics, generating, files, text])
|
||||||
|
|
||||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
||||||
|
|
||||||
@@ -116,6 +128,32 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
store.dispatch(setGenerating(false))
|
store.dispatch(setGenerating(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Command or Ctrl + N create new topic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeydown = (e) => {
|
const onKeydown = (e) => {
|
||||||
@@ -123,7 +161,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||||
addNewTopic()
|
addNewTopic()
|
||||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||||
inputRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +174,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
||||||
setText(message.content)
|
setText(message.content)
|
||||||
inputRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
|
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
|
||||||
]
|
]
|
||||||
@@ -144,19 +182,32 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
}, [assistant])
|
}, [assistant])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
|
||||||
id="inputbar"
|
<Textarea
|
||||||
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
|
value={text}
|
||||||
className={inputFocus ? 'focus' : ''}>
|
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>
|
<Toolbar>
|
||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||||
<PlusCircleOutlined />
|
<FormOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||||
@@ -173,36 +224,32 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
|
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
|
||||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
|
<ToolbarButton type="text" onClick={toggleShowTopics}>
|
||||||
<HistoryOutlined />
|
<HistoryOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
<Popover content={<SettingsTab assistant={assistant} />} trigger="click" placement="topRight">
|
||||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
|
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
||||||
<ControlOutlined />
|
<ToolbarButton type="text">
|
||||||
</ToolbarButton>
|
<ControlOutlined />
|
||||||
</Tooltip>
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Popover>
|
||||||
|
{/* <AttachmentButton files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} /> */}
|
||||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
<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 />}
|
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showInputEstimatedTokens && (
|
{showInputEstimatedTokens && (
|
||||||
<TextCount>
|
<TextCount>
|
||||||
<Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
|
<Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
|
||||||
<Tag
|
<StyledTag>
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
borderRadius: '20px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '2px 8px'
|
|
||||||
}}>
|
|
||||||
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
|
|
||||||
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
|
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
|
||||||
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑
|
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑{inputTokenCount}
|
||||||
{`${inputTokenCount} / ${estimateTokenCount}`}
|
<span style={{ margin: '0 2px' }}>/</span>
|
||||||
</Tag>
|
{estimateTokenCount}
|
||||||
|
</StyledTag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TextCount>
|
</TextCount>
|
||||||
)}
|
)}
|
||||||
@@ -218,29 +265,18 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
||||||
</ToolbarMenu>
|
</ToolbarMenu>
|
||||||
</Toolbar>
|
</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>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TextareaStyle: CSSProperties = {
|
||||||
|
paddingLeft: 0,
|
||||||
|
padding: '10px 15px 8px',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
border: 1px solid var(--color-border-soft);
|
||||||
flex-direction: column;
|
|
||||||
height: var(--input-bar-height);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 20px 15px 20px;
|
margin: 0 20px 15px 20px;
|
||||||
@@ -254,11 +290,11 @@ const Textarea = styled(TextArea)`
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0 15px 5px 15px;
|
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: auto;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Toolbar = styled.div`
|
const Toolbar = styled.div`
|
||||||
@@ -266,8 +302,9 @@ const Toolbar = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
padding-top: 3px;
|
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
height: 36px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ToolbarMenu = styled.div`
|
const ToolbarMenu = styled.div`
|
||||||
@@ -278,17 +315,23 @@ const ToolbarMenu = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const ToolbarButton = styled(Button)`
|
const ToolbarButton = styled(Button)`
|
||||||
width: 32px;
|
width: 30px;
|
||||||
height: 32px;
|
height: 30px;
|
||||||
font-size: 18px;
|
font-size: 17px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
&.anticon {
|
&.anticon {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&.active {
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
.anticon {
|
.anticon {
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
@@ -303,7 +346,16 @@ const TextCount = styled.div`
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-top-left-radius: 7px;
|
border-top-left-radius: 7px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin-right: 10px;
|
`
|
||||||
|
|
||||||
|
const StyledTag = styled(Tag)`
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-width: 0.5;
|
||||||
|
margin: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default Inputbar
|
export default Inputbar
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons'
|
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 { initMermaid } from '@renderer/init'
|
||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
|
||||||
import { ThemeMode } from '@renderer/store/settings'
|
import { ThemeMode } from '@renderer/store/settings'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -39,7 +40,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
|
|||||||
<div>
|
<div>
|
||||||
<CodeHeader>
|
<CodeHeader>
|
||||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||||
{!copied && <CopyOutlined className="copy" onClick={onCopy} />}
|
{!copied && <CopyIcon className="copy" onClick={onCopy} />}
|
||||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||||
</CodeHeader>
|
</CodeHeader>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
|
|||||||