Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a654ccc25e | ||
|
|
71a35ccd44 | ||
|
|
29826ff091 | ||
|
|
8566476d91 | ||
|
|
a173a87f29 | ||
|
|
cb068d71ca | ||
|
|
66210d1d2e | ||
|
|
aa427c9911 | ||
|
|
9ae9fdf392 | ||
|
|
0ddef31ed8 | ||
|
|
617af8b12a | ||
|
|
71876e6a70 | ||
|
|
4f250cdcb1 | ||
|
|
9268ab845e | ||
|
|
0337c6649b | ||
|
|
8781388760 | ||
|
|
2016ba7062 | ||
|
|
a03d619e2f | ||
|
|
76d1f0bb1e | ||
|
|
2bad5a1184 | ||
|
|
94ba3aee05 | ||
|
|
563758f69f | ||
|
|
56af85cc3e | ||
|
|
6a1a861ecc | ||
|
|
ceab574a22 | ||
|
|
98704fdb28 | ||
|
|
fd5cba5219 | ||
|
|
be5aaa2b66 | ||
|
|
7e8687decd | ||
|
|
4c96324ef7 | ||
|
|
dd3c81ec5f | ||
|
|
42f0b5f8fc | ||
|
|
11b2cd88b7 | ||
|
|
6bf98f6db3 | ||
|
|
10b4e3c634 | ||
|
|
a3f5223b4c | ||
|
|
2855575b36 | ||
|
|
1f0ba20523 | ||
|
|
2f53416e09 | ||
|
|
ddbf266a3f | ||
|
|
d815415f36 | ||
|
|
cdacc56fd7 | ||
|
|
455d909c74 | ||
|
|
52d84afed6 | ||
|
|
f06d1d4d9a | ||
|
|
805a65bbaa | ||
|
|
f217950b13 | ||
|
|
9ff65441ef | ||
|
|
2b20282a41 | ||
|
|
96ad2de896 | ||
|
|
e1ea875c21 | ||
|
|
500e91977c | ||
|
|
bd194ff955 | ||
|
|
828bd71f22 | ||
|
|
5991f692b2 | ||
|
|
200d78a140 | ||
|
|
9a502b5e47 | ||
|
|
97ef3772ea | ||
|
|
eb18be200e | ||
|
|
467e97ff4b | ||
|
|
27b802d3c2 | ||
|
|
37b0a175f7 | ||
|
|
b2b79f12a2 | ||
|
|
885c578582 | ||
|
|
e61e4b109a | ||
|
|
f3bafbeb52 | ||
|
|
e55c0cdcef | ||
|
|
e73bbf4d6a | ||
|
|
3859289218 | ||
|
|
591bb45a4e | ||
|
|
b31f518fca | ||
|
|
dfbdb989db | ||
|
|
f194ebbc20 | ||
|
|
ab0e7e1e07 | ||
|
|
d809f50c0e | ||
|
|
a48d24de26 | ||
|
|
0dacc20e74 | ||
|
|
08df6cb4f8 | ||
|
|
0676ac8942 | ||
|
|
c257e8f0fe | ||
|
|
521670f683 | ||
|
|
87216b5d91 | ||
|
|
e6122a3d36 | ||
|
|
e6e1502308 | ||
|
|
7f5be3a688 | ||
|
|
4dde49a9f0 | ||
|
|
ce830b692b | ||
|
|
563472f3a9 | ||
|
|
14acd45927 | ||
|
|
9e2c7a08df | ||
|
|
f10c8dc379 | ||
|
|
fdd815879a | ||
|
|
635f238576 | ||
|
|
615e337e3f | ||
|
|
acd5d4b192 | ||
|
|
9a41b697c6 | ||
|
|
5cb67e00a6 | ||
|
|
350f13e97c | ||
|
|
4d6cbf5073 | ||
|
|
8d7b10d21e | ||
|
|
6753a93c0d | ||
|
|
9ee763337d | ||
|
|
ace0cb7823 | ||
|
|
44e518ef03 | ||
|
|
e28b96b45e | ||
|
|
11427a980c | ||
|
|
cb95562e58 | ||
|
|
89bdab58f7 | ||
|
|
d42ee59335 | ||
|
|
88e7ab211d | ||
|
|
5347bdfa83 | ||
|
|
c8711c5804 | ||
|
|
24cf3bb043 | ||
|
|
0531ecf3cf | ||
|
|
0cbfd26883 | ||
|
|
ee398489de | ||
|
|
71d7c2c738 | ||
|
|
b98f7298a2 | ||
|
|
de4f2599be | ||
|
|
93b32e8e21 | ||
|
|
e353d0f8ee | ||
|
|
dfd42fe9a6 | ||
|
|
a2dc325896 | ||
|
|
b131d320ea | ||
|
|
b88f4a869e | ||
|
|
461458e5ec | ||
|
|
4c2014f1d6 | ||
|
|
647dd3e751 | ||
|
|
4225312d4a | ||
|
|
c2a4613e32 | ||
|
|
5d5c1eee74 | ||
|
|
c1b5e6b183 | ||
|
|
fd37ba18dc | ||
|
|
4a26f7ce78 | ||
|
|
8b38ebcac4 | ||
|
|
e8dac28787 | ||
|
|
3ccebb503f | ||
|
|
42327836de | ||
|
|
4d7a3bb8c3 | ||
|
|
1996e163c9 | ||
|
|
e43f7f87ab | ||
|
|
47a83fa67f | ||
|
|
5e954566c9 | ||
|
|
b8960ef02c | ||
|
|
1866b00265 | ||
|
|
be0799a4c6 | ||
|
|
d0f5547419 | ||
|
|
076011b02b | ||
|
|
ba5c70c45a | ||
|
|
ebe74ffd05 | ||
|
|
d0bea0491f | ||
|
|
514e1a4796 | ||
|
|
2ffedadee4 | ||
|
|
7b72783ae7 | ||
|
|
4485a00395 | ||
|
|
77c0952635 | ||
|
|
e1c7a25b87 | ||
|
|
b0c479190c | ||
|
|
c7c3d28893 | ||
|
|
994ee8d7df | ||
|
|
57f9550891 | ||
|
|
0c0d1560db | ||
|
|
145d7ee748 | ||
|
|
52af23b931 | ||
|
|
f7151bd066 | ||
|
|
744a1fedba | ||
|
|
978432d910 | ||
|
|
b6cb1e4d84 | ||
|
|
0096783f26 | ||
|
|
4fc53d7c19 | ||
|
|
34d99b711c | ||
|
|
5dd74a1018 | ||
|
|
e028d0600f | ||
|
|
64ee3f2108 | ||
|
|
30a082b979 | ||
|
|
5a0927393d | ||
|
|
16c68dcdcb | ||
|
|
b6500977b0 | ||
|
|
78cf33e8bc | ||
|
|
2f62f04adf | ||
|
|
84915b1ede | ||
|
|
248c7ea20e | ||
|
|
1031d40ddb | ||
|
|
3d44fc2208 | ||
|
|
22e3c0e270 | ||
|
|
5d81874166 | ||
|
|
f7ef895ce6 | ||
|
|
beb40f5baf | ||
|
|
07613e65f5 | ||
|
|
6185068353 | ||
|
|
61934cd65c | ||
|
|
41f65b66ba | ||
|
|
5edb53ef7d | ||
|
|
167988927b | ||
|
|
a39beb3841 | ||
|
|
8719d5c330 | ||
|
|
a7427d6cb6 | ||
|
|
8759a50727 | ||
|
|
7ffa42caa0 | ||
|
|
b0a3d705ff | ||
|
|
de41199f7e | ||
|
|
cbd9f60cfc | ||
|
|
8a0e2890dd | ||
|
|
a8f3e2be6b |
@@ -6,4 +6,4 @@ indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error'
|
||||
'simple-import-sort/exports': 'error',
|
||||
'react/no-is-mounted': 'off'
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest]
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
|
||||
3
.gitignore
vendored
@@ -45,3 +45,6 @@ out
|
||||
# ENV
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Local
|
||||
local
|
||||
|
||||
3
.vscode/settings.json
vendored
@@ -29,5 +29,6 @@
|
||||
},
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false
|
||||
}
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/renderer/src/i18n"]
|
||||
}
|
||||
|
||||
53
.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch
Normal file
@@ -0,0 +1,53 @@
|
||||
diff --git a/lib/check-signature.js b/lib/check-signature.js
|
||||
index 324568af71bcc4372c9f959131ecd24122848c86..677348e0a138ff608b2ac41f592d813b15ee4956 100644
|
||||
--- a/lib/check-signature.js
|
||||
+++ b/lib/check-signature.js
|
||||
@@ -41,16 +41,12 @@ const spawn_1 = require("./spawn");
|
||||
const debug_1 = __importDefault(require("debug"));
|
||||
const d = (0, debug_1.default)('electron-notarize');
|
||||
const codesignDisplay = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
|
||||
return result;
|
||||
});
|
||||
const codesign = (opts) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
d('attempting to check codesign of app:', opts.appPath);
|
||||
- const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
|
||||
return result;
|
||||
});
|
||||
function checkSignatures(opts) {
|
||||
diff --git a/lib/notarytool.js b/lib/notarytool.js
|
||||
index 1ab090efb2101fc8bee5553445e0349c54474421..a5ddfd922197449fc56078e4a7e9a2ee5d8d207d 100644
|
||||
--- a/lib/notarytool.js
|
||||
+++ b/lib/notarytool.js
|
||||
@@ -92,9 +92,7 @@ function notarizeAndWaitForNotaryTool(opts) {
|
||||
else {
|
||||
filePath = path.resolve(dir, `${path.parse(opts.appPath).name}.zip`);
|
||||
d('zipping application to:', filePath);
|
||||
- const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), filePath], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', opts.appPath, filePath]);
|
||||
if (zipResult.code !== 0) {
|
||||
throw new Error(`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`);
|
||||
}
|
||||
diff --git a/lib/staple.js b/lib/staple.js
|
||||
index 47dbd85b2fc279d999b57f47fb8171e1cc674436..f8829e6ac54fcd630a730d12d75acc1591b953b6 100644
|
||||
--- a/lib/staple.js
|
||||
+++ b/lib/staple.js
|
||||
@@ -43,9 +43,7 @@ const d = (0, debug_1.default)('electron-notarize:staple');
|
||||
function stapleApp(opts) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
d('attempting to staple app:', opts.appPath);
|
||||
- const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', path.basename(opts.appPath)], {
|
||||
- cwd: path.dirname(opts.appPath),
|
||||
- });
|
||||
+ const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', opts.appPath]);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`Failed to staple your application with code: ${result.code}\n\n${result.output}`);
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
nodeLinker: node-modules
|
||||
enableImmutableInstalls: false
|
||||
|
||||
httpTimeout: 300000
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
27
README.md
@@ -1,18 +1,18 @@
|
||||
# Cherry Studio
|
||||
# 🍒 Cherry Studio
|
||||
|
||||
🍒 Cherry Studio is a desktop client that supports multiple Large Language Model (LLM) providers, available on Windows, Mac and Linux.
|
||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||
|
||||
# Screenshot
|
||||
# 🌠 Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
# Feature
|
||||
# 🌟 Features
|
||||
|
||||
1. Supports multiple large language model service providers.
|
||||
1. Support for Multiple LLM Providers.
|
||||
2. Allows creation of multiple Assistants.
|
||||
3. Enables creation of multiple topics.
|
||||
4. Allows using multiple models to answer questions in the same conversation.
|
||||
@@ -20,7 +20,8 @@
|
||||
6. Code highlighting.
|
||||
7. Mermaid chart
|
||||
|
||||
# Develop
|
||||
# 🖥️ Develop
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
@@ -51,3 +52,11 @@ $ yarn build:mac
|
||||
# For Linux
|
||||
$ yarn build:linux
|
||||
```
|
||||
|
||||
# ⭐️ Star History
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
# 📃 License
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
|
||||
@@ -3,12 +3,15 @@ productName: Cherry Studio
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!{.vscode,.yarn,.github}'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
- '!src'
|
||||
- '!local'
|
||||
- '!scripts'
|
||||
- '!resources'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
@@ -40,8 +43,8 @@ dmg:
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
# - snap
|
||||
# - deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
@@ -56,5 +59,14 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
新增发送按钮
|
||||
输入区域展开可以全屏显示
|
||||
本次更新:
|
||||
支持行内公式
|
||||
支持编辑所有集成的服务商API地址
|
||||
新增智能体搜索功能(>10个)
|
||||
修复正则表达式显示错误
|
||||
修复默认模型参数不生效
|
||||
修复暗黑模式下分界线不明显问题
|
||||
近期更新:
|
||||
智能助理和消息列表合并
|
||||
优化输入框样式
|
||||
提升小程序稳定性
|
||||
|
||||
@@ -4,7 +4,13 @@ import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@types': resolve('src/renderer/src/types'),
|
||||
'@main': resolve('src/main')
|
||||
}
|
||||
}
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
@@ -15,7 +21,6 @@ export default defineConfig({
|
||||
'@renderer': resolve('src/renderer/src')
|
||||
}
|
||||
},
|
||||
plugins: [react()],
|
||||
assetsInclude: ['**/*.md']
|
||||
plugins: [react()]
|
||||
}
|
||||
})
|
||||
|
||||
31
package.json
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.4.7",
|
||||
"name": "CherryStudio",
|
||||
"version": "0.7.0",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
"homepage": "https://github.com/kangfenmao/cherry-studio",
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"local"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
@@ -25,18 +31,17 @@
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@sentry/electron": "^5.2.0",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1"
|
||||
"electron-window-state": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.3",
|
||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@google/generative-ai": "^0.16.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
@@ -45,24 +50,28 @@
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"ahooks": "^3.8.0",
|
||||
"antd": "^5.18.3",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"electron": "^28.2.0",
|
||||
"electron": "^28.3.3",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-vite": "^2.0.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"gpt-tokens": "^1.3.6",
|
||||
"i18next": "^23.11.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.4",
|
||||
"openai": "^4.52.1",
|
||||
"prettier": "^3.2.4",
|
||||
"react": "^18.2.0",
|
||||
@@ -74,13 +83,14 @@
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sass": "^1.77.2",
|
||||
"styled-components": "^6.1.11",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
@@ -89,7 +99,8 @@
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@electron/notarize": "2.3.2"
|
||||
"@electron/notarize": "2.3.2",
|
||||
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
|
||||
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>
|
||||
|
Before Width: | Height: | Size: 197 KiB |
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,4 +1,18 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { app } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import path from 'path'
|
||||
|
||||
const getDataPath = () => {
|
||||
const dataPath = path.join(app.getPath('userData'), 'Data')
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.mkdirSync(dataPath, { recursive: true })
|
||||
}
|
||||
return dataPath
|
||||
}
|
||||
|
||||
export const DATA_PATH = getDataPath()
|
||||
|
||||
export const appConfig = new Store()
|
||||
|
||||
|
||||
@@ -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 * as Sentry from '@sentry/electron/main'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { saveFile } from './event'
|
||||
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: 500,
|
||||
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
|
||||
}
|
||||
import { registerIpc } from './ipc'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
import { createMainWindow } from './window'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
await updateUserDataPath()
|
||||
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
|
||||
|
||||
@@ -97,45 +24,12 @@ app.whenReady().then(() => {
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
|
||||
})
|
||||
|
||||
const mainWindow = createWindow()
|
||||
const mainWindow = createMainWindow()
|
||||
|
||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||
|
||||
// IPC
|
||||
ipcMain.handle('get-app-info', () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged
|
||||
}))
|
||||
|
||||
ipcMain.handle('open-website', (_, url: string) => {
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
||||
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
||||
})
|
||||
|
||||
ipcMain.handle('save-file', saveFile)
|
||||
|
||||
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
|
||||
appConfig.set('theme', theme)
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
})
|
||||
|
||||
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
|
||||
ipcMain.handle('check-for-update', async () => {
|
||||
autoUpdater.logger?.info('触发检查更新')
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
}
|
||||
})
|
||||
|
||||
installExtension(REDUX_DEVTOOLS)
|
||||
registerIpc(mainWindow, app)
|
||||
})
|
||||
|
||||
// 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
|
||||
// code. You can also put them in separate files and require them here.
|
||||
Sentry.init({
|
||||
dsn: 'https://f0e972deff79c2df3e887e232d8a46a3@o4507610668007424.ingest.us.sentry.io/4507610670563328'
|
||||
})
|
||||
|
||||
87
src/main/ipc.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { FileType } from '@types'
|
||||
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import File from './services/File'
|
||||
import { openFile, saveFile } from './utils/file'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { createMinappWindow } from './window'
|
||||
|
||||
const fileManager = new File()
|
||||
|
||||
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('image:base64', async (_, filePath) => {
|
||||
try {
|
||||
const data = await fs.promises.readFile(filePath)
|
||||
const base64 = data.toString('base64')
|
||||
const mime = `image/${path.extname(filePath).slice(1)}`
|
||||
return {
|
||||
mime,
|
||||
base64,
|
||||
data: `data:${mime};base64,${base64}`
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('Error reading file:', error)
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
||||
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
|
||||
ipcMain.handle('file:delete', async (_, fileId: string) => {
|
||||
await fileManager.deleteFile(fileId)
|
||||
return { success: true }
|
||||
})
|
||||
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)
|
||||
})
|
||||
|
||||
// 检测是否需要更新
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
logger.info('正在检查更新……')
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
|
||||
autoUpdater.logger?.info('检测到新版本,确认是否下载')
|
||||
mainWindow.webContents.send('update-available', releaseInfo)
|
||||
@@ -59,7 +54,6 @@ export default class AppUpdater {
|
||||
|
||||
// 检测到不需要更新时
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
logger.info('现在使用的就是最新版本,不用更新')
|
||||
mainWindow.webContents.send('update-not-available')
|
||||
})
|
||||
|
||||
139
src/main/services/File.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { getFileType } from '@main/utils/file'
|
||||
import { FileType } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import { app, dialog, OpenDialogOptions } from 'electron'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
class File {
|
||||
private storageDir: string
|
||||
|
||||
constructor() {
|
||||
this.storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
this.initStorageDir()
|
||||
}
|
||||
|
||||
private initStorageDir(): void {
|
||||
if (!fs.existsSync(this.storageDir)) {
|
||||
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
private async getFileHash(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('md5')
|
||||
const stream = fs.createReadStream(filePath)
|
||||
stream.on('data', (data) => hash.update(data))
|
||||
stream.on('end', () => resolve(hash.digest('hex')))
|
||||
stream.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async findDuplicateFile(filePath: string): Promise<FileType | null> {
|
||||
const stats = fs.statSync(filePath)
|
||||
const fileSize = stats.size
|
||||
|
||||
const files = await fs.promises.readdir(this.storageDir)
|
||||
for (const file of files) {
|
||||
const storedFilePath = path.join(this.storageDir, file)
|
||||
const storedStats = fs.statSync(storedFilePath)
|
||||
|
||||
if (storedStats.size === fileSize) {
|
||||
const [originalHash, storedHash] = await Promise.all([
|
||||
this.getFileHash(filePath),
|
||||
this.getFileHash(storedFilePath)
|
||||
])
|
||||
|
||||
if (originalHash === storedHash) {
|
||||
const ext = path.extname(file)
|
||||
const id = path.basename(file, ext)
|
||||
return {
|
||||
id,
|
||||
origin_name: file,
|
||||
name: file + ext,
|
||||
path: storedFilePath,
|
||||
created_at: storedStats.birthtime,
|
||||
size: storedStats.size,
|
||||
ext,
|
||||
type: getFileType(ext),
|
||||
count: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async selectFile(options?: OpenDialogOptions): Promise<FileType[] | null> {
|
||||
const defaultOptions: OpenDialogOptions = {
|
||||
properties: ['openFile']
|
||||
}
|
||||
|
||||
const dialogOptions = { ...defaultOptions, ...options }
|
||||
|
||||
const result = await dialog.showOpenDialog(dialogOptions)
|
||||
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fileMetadataPromises = result.filePaths.map(async (filePath) => {
|
||||
const stats = fs.statSync(filePath)
|
||||
const ext = path.extname(filePath)
|
||||
const fileType = getFileType(ext)
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
origin_name: path.basename(filePath),
|
||||
name: path.basename(filePath),
|
||||
path: filePath,
|
||||
created_at: stats.birthtime,
|
||||
size: stats.size,
|
||||
ext: ext,
|
||||
type: fileType,
|
||||
count: 1
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.all(fileMetadataPromises)
|
||||
}
|
||||
|
||||
async uploadFile(file: FileType): Promise<FileType> {
|
||||
const duplicateFile = await this.findDuplicateFile(file.path)
|
||||
|
||||
if (duplicateFile) {
|
||||
return duplicateFile
|
||||
}
|
||||
|
||||
const uuid = uuidv4()
|
||||
const origin_name = path.basename(file.path)
|
||||
const ext = path.extname(origin_name)
|
||||
const destPath = path.join(this.storageDir, uuid + ext)
|
||||
|
||||
await fs.promises.copyFile(file.path, destPath)
|
||||
const stats = await fs.promises.stat(destPath)
|
||||
const fileType = getFileType(ext)
|
||||
|
||||
const fileMetadata: FileType = {
|
||||
id: uuid,
|
||||
origin_name,
|
||||
name: uuid + ext,
|
||||
path: destPath,
|
||||
created_at: stats.birthtime,
|
||||
size: stats.size,
|
||||
ext: ext,
|
||||
type: fileType,
|
||||
count: 1
|
||||
}
|
||||
|
||||
return fileMetadata
|
||||
}
|
||||
|
||||
async deleteFile(id: string): Promise<void> {
|
||||
await fs.promises.unlink(path.join(this.storageDir, id))
|
||||
}
|
||||
}
|
||||
|
||||
export default File
|
||||
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
|
||||
}
|
||||
71
src/main/utils/file.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import { writeFile } from 'fs'
|
||||
import { readFile } from 'fs/promises'
|
||||
|
||||
import { FileTypes } from '../../renderer/src/types'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export function getFileType(ext: string): FileTypes {
|
||||
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt']
|
||||
|
||||
ext = ext.toLowerCase()
|
||||
if (imageExts.includes(ext)) return FileTypes.IMAGE
|
||||
if (videoExts.includes(ext)) return FileTypes.VIDEO
|
||||
if (audioExts.includes(ext)) return FileTypes.AUDIO
|
||||
if (documentExts.includes(ext)) return FileTypes.DOCUMENT
|
||||
return FileTypes.OTHER
|
||||
}
|
||||
7
src/main/utils/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { app } from 'electron'
|
||||
|
||||
export function getResourcePath() {
|
||||
return path.join(app.getAppPath(), 'resources')
|
||||
}
|
||||
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
|
||||
}
|
||||
18
src/preload/index.d.ts
vendored
@@ -1,4 +1,6 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import { FileType } from '@renderer/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -7,12 +9,26 @@ declare global {
|
||||
getAppInfo: () => Promise<{
|
||||
version: string
|
||||
isPackaged: boolean
|
||||
appPath: string
|
||||
}>
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => 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
|
||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||
reload: () => void
|
||||
compress: (text: string) => Promise<Buffer>
|
||||
decompress: (text: Buffer) => Promise<string>
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||
upload: (file: FileType) => Promise<FileType>
|
||||
delete: (fileId: string) => Promise<{ success: boolean }>
|
||||
}
|
||||
image: {
|
||||
base64: (filePath: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
@@ -7,8 +7,23 @@ const api = {
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
|
||||
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),
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId)
|
||||
},
|
||||
image: {
|
||||
base64: (filePath: string) => ipcRenderer.invoke('image:base64', filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:" />
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import '@renderer/databases'
|
||||
|
||||
import store, { persistor } from '@renderer/store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
@@ -5,12 +7,14 @@ import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
import AntdProvider from './providers/AntdProvider'
|
||||
import { ThemeProvider } from './providers/ThemeProvider'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
@@ -23,8 +27,10 @@ function App(): JSX.Element {
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Thin.ttf) format('truetype');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-ExtraLight.ttf) format('truetype');
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Light.ttf) format('truetype');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Regular.ttf) format('truetype');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Medium.ttf) format('truetype');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-SemiBold.ttf) format('truetype');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Bold.ttf) format('truetype');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-ExtraBold.ttf) format('truetype');
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1,55 +1,88 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
|
||||
url('iconfont.woff?t=1722242729348') format('woff'),
|
||||
url('iconfont.ttf?t=1722242729348') format('truetype');
|
||||
font-family: 'iconfont'; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: '\e6cd';
|
||||
}
|
||||
|
||||
.icon-ai-model:before {
|
||||
content: '\e827';
|
||||
}
|
||||
|
||||
.icon-ai-model1:before {
|
||||
content: '\ec09';
|
||||
}
|
||||
|
||||
.icon-gridlines:before {
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-grid-row-2copy:before {
|
||||
content: '\e681';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: '\e869';
|
||||
}
|
||||
|
||||
.icon-business-smart-assistant:before {
|
||||
content: '\e601';
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: '\e6ae';
|
||||
}
|
||||
|
||||
.icon-ic_send:before {
|
||||
content: '\e795';
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
content: '\e72f';
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
content: '\e6b7';
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
content: '\e7de';
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
content: '\e758';
|
||||
}
|
||||
|
||||
.icon-hidesidebarhoriz:before {
|
||||
content: "\e8eb";
|
||||
.icon-hide-sidebar:before {
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-showsidebarhoriz:before {
|
||||
content: "\e944";
|
||||
.icon-show-sidebar:before {
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: "\e658";
|
||||
content: '\e658';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
content: '\e615';
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
content: '\e78e';
|
||||
}
|
||||
|
||||
|
||||
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Bold.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-BoldItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Italic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Light.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-LightItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Medium.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-MediumItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/ubuntu/Ubuntu-Regular.ttf
Normal file
55
src/renderer/src/assets/fonts/ubuntu/ubuntu.css
Normal file
@@ -0,0 +1,55 @@
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Italic.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-BoldItalic.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-LightItalic.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
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/poe.webp
Normal file
|
After Width: | Height: | Size: 3.1 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/apps/zhihu.png
Normal file
|
After Width: | Height: | Size: 14 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/cohere.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/renderer/src/assets/images/models/doubao.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/models/embedding.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/renderer/src/assets/images/models/gemini.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/renderer/src/assets/images/models/hailuo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
67
src/renderer/src/assets/images/models/palm.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Standard_product_icon__x28_1:1_x29_"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="192px" height="192px"
|
||||
viewBox="0 0 192 192" enable-background="new 0 0 192 192" xml:space="preserve">
|
||||
<symbol id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" viewBox="-96 -96 192 192">
|
||||
<g opacity="0.4">
|
||||
<defs>
|
||||
<path id="SVGID_1_" opacity="0.4" d="M-96,96V-96H96V96H-96z"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_00000071517564283228984050000017848131202901217410_">
|
||||
<use xlink:href="#SVGID_1_" overflow="visible"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)">
|
||||
<g>
|
||||
<path d="M95.75,95.75v-191.5h-191.5v191.5H95.75 M96,96H-96V-96H96V96L96,96z"/>
|
||||
</g>
|
||||
<circle fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="64"/>
|
||||
</g>
|
||||
|
||||
<circle clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" cx="0" cy="0" r="88"/>
|
||||
|
||||
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
|
||||
M64,76H-64c-6.6,0-12-5.4-12-12V-64c0-6.6,5.4-12,12-12H64c6.6,0,12,5.4,12,12V64C76,70.6,70.6,76,64,76z"/>
|
||||
|
||||
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
|
||||
M52,88H-52c-6.6,0-12-5.4-12-12V-76c0-6.6,5.4-12,12-12H52c6.6,0,12,5.4,12,12V76C64,82.6,58.6,88,52,88z"/>
|
||||
|
||||
<path clip-path="url(#SVGID_00000071517564283228984050000017848131202901217410_)" fill="none" stroke="#000000" stroke-width="0.25" stroke-miterlimit="10" d="
|
||||
M76,64H-76c-6.6,0-12-5.4-12-12V-52c0-6.6,5.4-12,12-12H76c6.6,0,12,5.4,12,12V52C88,58.6,82.6,64,76,64z"/>
|
||||
</g>
|
||||
</symbol>
|
||||
<rect id="bounding_box_1_" display="none" fill="none" width="192" height="192"/>
|
||||
<g id="art_layer">
|
||||
<g>
|
||||
<path fill="#F9AB00" d="M96,181.92L96,181.92c6.63,0,12-5.37,12-12v-104H84v104C84,176.55,89.37,181.92,96,181.92z"/>
|
||||
<g>
|
||||
<path fill="#5BB974" d="M143.81,103.87C130.87,90.94,111.54,88.32,96,96l51.37,51.37c2.12,2.12,5.77,1.28,6.67-1.57
|
||||
C158.56,131.49,155.15,115.22,143.81,103.87z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#129EAF" d="M48.19,103.87C61.13,90.94,80.46,88.32,96,96l-51.37,51.37c-2.12,2.12-5.77,1.28-6.67-1.57
|
||||
C33.44,131.49,36.85,115.22,48.19,103.87z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#AF5CF7" d="M140,64c-20.44,0-37.79,13.4-44,32h81.24c3.33,0,5.55-3.52,4.04-6.49C173.56,74.36,157.98,64,140,64z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#FF8BCB" d="M104.49,42.26C90.03,56.72,87.24,78.45,96,96l57.45-57.45c2.36-2.36,1.44-6.42-1.73-7.45
|
||||
C135.54,25.85,117.2,29.55,104.49,42.26z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#FA7B17" d="M87.51,42.26C101.97,56.72,104.76,78.45,96,96L38.55,38.55c-2.36-2.36-1.44-6.42,1.73-7.45
|
||||
C56.46,25.85,74.8,29.55,87.51,42.26z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#4285F4" d="M52,64c20.44,0,37.79,13.4,44,32H14.76c-3.33,0-5.55-3.52-4.04-6.49C18.44,74.36,34.02,64,52,64z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="keylines" display="none">
|
||||
|
||||
<use xlink:href="#material_x5F_product_x5F_standard_x5F_icon_x5F_keylines_00000077318920148093339210000006245950728745084294_" width="192" height="192" id="material_x5F_product_x5F_standard_x5F_icon_x5F_keylines" x="-96" y="-96" transform="matrix(1 0 0 -1 96 96)" display="inline" overflow="visible"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/renderer/src/assets/images/models/step.jpg
Normal file
|
After Width: | Height: | Size: 6.2 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 |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/providers/doubao.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/providers/gemini.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
3
src/renderer/src/assets/images/providers/github.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16 0 0 7.16 0 16C0 23.08 4.58 29.06 10.94 31.18C11.74 31.32 12.04 30.84 12.04 30.42C12.04 30.04 12.02 28.78 12.02 27.44C8 28.18 6.96 26.46 6.64 25.56C6.46 25.1 5.68 23.68 5 23.3C4.44 23 3.64 22.26 4.98 22.24C6.24 22.22 7.14 23.4 7.44 23.88C8.88 26.3 11.18 25.62 12.1 25.2C12.24 24.16 12.66 23.46 13.12 23.06C9.56 22.66 5.84 21.28 5.84 15.16C5.84 13.42 6.46 11.98 7.48 10.86C7.32 10.46 6.76 8.82 7.64 6.62C7.64 6.62 8.98 6.2 12.04 8.26C13.32 7.9 14.68 7.72 16.04 7.72C17.4 7.72 18.76 7.9 20.04 8.26C23.1 6.18 24.44 6.62 24.44 6.62C25.32 8.82 24.76 10.46 24.6 10.86C25.62 11.98 26.24 13.4 26.24 15.16C26.24 21.3 22.5 22.66 18.94 23.06C19.52 23.56 20.02 24.52 20.02 26.02C20.02 28.16 20 29.88 20 30.42C20 30.84 20.3 31.34 21.1 31.18C27.42 29.06 32 23.06 32 16C32 7.16 24.84 0 16 0V0Z" fill="#24292E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 959 B |
BIN
src/renderer/src/assets/images/providers/graph-rag.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/providers/graph-rag.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
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/openai.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/renderer/src/assets/images/providers/stepfun.png
Normal file
|
After Width: | Height: | Size: 2.1 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 './scrollbar.scss';
|
||||
@import '../fonts/icon-fonts/iconfont.css';
|
||||
@import '../fonts/Poppins/Poppins.css';
|
||||
@import '../fonts/ubuntu/ubuntu.css';
|
||||
|
||||
:root {
|
||||
--color-white: #ffffff;
|
||||
@@ -24,29 +24,33 @@
|
||||
--color-background-soft: var(--color-black-soft);
|
||||
--color-background-mute: var(--color-black-mute);
|
||||
|
||||
--color-primary: #135200;
|
||||
--color-primary-soft: #13520099;
|
||||
--color-primary-mute: #13520033;
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
--color-primary-mute: #00b96b33;
|
||||
|
||||
--color-text: var(--color-text-1);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff20;
|
||||
--color-border-soft: #ffffff20;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #323232;
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
|
||||
|
||||
--navbar-background: rgba(0, 0, 0, 0.8);
|
||||
--sidebar-background: rgba(0, 0, 0, 0.8);
|
||||
--navbar-background-mac: rgba(30, 30, 30, 0.8);
|
||||
--navbar-background: rgba(30, 30, 30);
|
||||
--input-bar-background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
--navbar-height: 42px;
|
||||
--sidebar-width: 55px;
|
||||
--assistants-width: 245px;
|
||||
--topic-list-width: 260px;
|
||||
--settings-width: var(--assistants-width);
|
||||
--sidebar-width: 52px;
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 115px;
|
||||
--input-bar-height: 85px;
|
||||
|
||||
--assistants-width: 280px;
|
||||
--topic-list-width: 280px;
|
||||
--settings-width: 260px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -78,13 +82,16 @@ body[theme-mode='light'] {
|
||||
--color-icon: #00000099;
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000028;
|
||||
--color-border-soft: #00000028;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
|
||||
|
||||
--navbar-background: rgba(255, 255, 255, 0.8);
|
||||
--sidebar-background: rgba(255, 255, 255, 0.8);
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.75);
|
||||
--navbar-background: rgba(255, 255, 255);
|
||||
--input-bar-background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -95,6 +102,14 @@ body[theme-mode='light'] {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -103,21 +118,12 @@ body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
background: transparent !important;
|
||||
font-family:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Microsoft YaHei',
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue' sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -163,3 +169,65 @@ body,
|
||||
position: relative;
|
||||
animation: flash 0.5s ease-out infinite alternate;
|
||||
}
|
||||
|
||||
.ant-segmented-group {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.drag {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.nodrag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.minapp-drawer {
|
||||
.ant-drawer-content-wrapper {
|
||||
box-shadow: none;
|
||||
}
|
||||
.ant-drawer-header {
|
||||
position: absolute;
|
||||
-webkit-app-region: drag;
|
||||
min-height: calc(var(--navbar-height) + 0.5px);
|
||||
background: var(--navbar-background);
|
||||
width: calc(100vw - var(--sidebar-width));
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
margin-top: -0.5px;
|
||||
}
|
||||
.ant-drawer-body {
|
||||
padding: 0;
|
||||
margin-top: var(--navbar-height);
|
||||
overflow: hidden;
|
||||
}
|
||||
.minapp-mask {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-header {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.segmented-tab {
|
||||
.ant-segmented-item-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
.iconfont {
|
||||
font-size: 13px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.anticon-setting {
|
||||
font-size: 12px;
|
||||
}
|
||||
.icon-business-smart-assistant {
|
||||
margin-right: -2px;
|
||||
}
|
||||
.ant-segmented-item-icon + * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
.markdown {
|
||||
color: var(--color-text);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
user-select: text;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
word-break: break-word;
|
||||
|
||||
h1:first-child,
|
||||
h2:first-child,
|
||||
@@ -29,6 +21,8 @@
|
||||
h6 {
|
||||
margin: 1em 0 1em 0;
|
||||
font-weight: 800;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -61,6 +55,13 @@
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
&:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
@@ -71,6 +72,12 @@
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
pre {
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
&::marker {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
li > ul,
|
||||
@@ -90,28 +97,36 @@
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
white-space: pre-wrap !important;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
p code {
|
||||
p code,
|
||||
li code {
|
||||
background: var(--color-background-mute);
|
||||
padding: 3px 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
padding: 1em;
|
||||
white-space: pre-wrap !important;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||
background-color: var(--color-background-mute);
|
||||
&:not(pre pre) {
|
||||
> code:not(pre pre > code) {
|
||||
padding: 15px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
margin: 0 !important;
|
||||
}
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +135,7 @@
|
||||
padding-left: 1em;
|
||||
color: var(--color-text-light);
|
||||
border-left: 4px solid var(--color-border);
|
||||
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -137,6 +153,8 @@
|
||||
th {
|
||||
background-color: var(--color-background-mute);
|
||||
font-weight: bold;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -144,9 +162,11 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
a,
|
||||
.link {
|
||||
color: var(--color-link);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@@ -228,3 +248,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emoji-picker {
|
||||
--border-size: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* 全局初始化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
||||
24
src/renderer/src/components/Avatar/ModelAvatar.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, AvatarProps } from 'antd'
|
||||
import { first } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
model: Model
|
||||
size: number
|
||||
props?: AvatarProps
|
||||
}
|
||||
|
||||
const ModelAvatar: FC<Props> = ({ model, size, props }) => {
|
||||
return (
|
||||
<Avatar
|
||||
src={getModelLogo(model?.id || '')}
|
||||
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
{...props}>
|
||||
{first(model?.name)}
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelAvatar
|
||||
70
src/renderer/src/components/DragableList/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DroppableProps,
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
OnDragStartResponder,
|
||||
ResponderProvided
|
||||
} from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props<T> {
|
||||
list: T[]
|
||||
style?: React.CSSProperties
|
||||
listStyle?: React.CSSProperties
|
||||
children: (item: T, index: number) => React.ReactNode
|
||||
onUpdate: (list: T[]) => void
|
||||
onDragStart?: OnDragStartResponder
|
||||
onDragEnd?: OnDragEndResponder
|
||||
droppableProps?: Partial<DroppableProps>
|
||||
}
|
||||
|
||||
const DragableList: FC<Props<any>> = ({
|
||||
children,
|
||||
list,
|
||||
style,
|
||||
listStyle,
|
||||
droppableProps,
|
||||
onDragStart,
|
||||
onUpdate,
|
||||
onDragEnd
|
||||
}) => {
|
||||
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided)
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
|
||||
onUpdate(reorderAgents)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||
<Droppable droppableId="droppable" {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
||||
{list.map((item, index) => (
|
||||
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)
|
||||
}
|
||||
|
||||
export default DragableList
|
||||