Compare commits

...

135 Commits

Author SHA1 Message Date
kangfenmao
27b802d3c2 chore(version): 0.6.9 2024-09-06 18:04:11 +08:00
kangfenmao
37b0a175f7 feat: Add theme switching to Navbar
- Added a new theme switching functionality to the Navbar.
2024-09-06 18:03:06 +08:00
kangfenmao
b2b79f12a2 feat: Enhanced code block styling in Markdown editor
- Added styles for code blocks in markdown to match the application's design.
- Improved the rendering of code blocks in the Markdown editor by adding a border and changing the default display in dark mode.
2024-09-06 17:58:15 +08:00
kangfenmao
885c578582 chore(version): 0.6.8 2024-09-06 15:54:44 +08:00
kangfenmao
e61e4b109a refactor: Remove unused CSS classes and optimize conditional styling
- Removed unused CSS classes and optimized code for conditional styling.
2024-09-06 15:53:58 +08:00
kangfenmao
f3bafbeb52 feat: Update UI components and styling for consistency and readability.
- Updated icon font asset reference URL to reflect a new timestamp.
- Updated icon-fonts file asset.
- Updated markdown styling to adjust margins and padding of pre-formatted text elements.
- Added Windows-specific styling to the Inputbar component.
- Improved the rendering of code blocks with a focus on readability and theming consistency.
- Added new 'plain' attribute to Divider component for 'clear' message type.
- Minor adjustments made to the navigation bar styles and layout.
2024-09-06 15:41:46 +08:00
kangfenmao
e55c0cdcef feat: Update context count logic
- Updated logic for determining context count based on clear messages.
2024-09-06 14:17:22 +08:00
kangfenmao
e73bbf4d6a style: Update toolbar button hover and active states
- Updated styles and icons for hover and active states of toolbar buttons.
2024-09-06 14:12:01 +08:00
kangfenmao
3859289218 style: Update styling and input bar characters.
- Updated styling and characters added to input bar.
2024-09-06 14:07:45 +08:00
kangfenmao
591bb45a4e feat: Improved chat UI with context handling and filtering #43
- Updated default context count from 5 to 6.
- Updated string translations for multiple languages.
- Added functionality to handle new context and update context count in Inputbar component.
- Added support for displaying new chat context divider for 'clear' type messages.
- Added functionality to emit estimated token count with context count when the estimated token count event is triggered.
- Improved filtering and processing of user messages for the AnthropicProvider class.
- Updated message filtering logic with context consideration.
- Improved filtering of user messages to include only context-relevant messages.
- Updated logic to pass messages directly to AI.completions and AI.suggestions API requests instead of filtered messages.
- Added new event names for handling topic sidebar and context switching.
- Improved handling of message filtering and context counting.
- Added new valid value 'clear' to type option in Message type.
2024-09-06 13:54:48 +08:00
kangfenmao
b31f518fca fix: Handle Enter key press event in input field
- Updated handling for Enter key press event in input field to match shortcut settings.
2024-09-06 11:34:55 +08:00
kangfenmao
dfbdb989db feat: Update icon font and navigation buttons
- Updated icon font references and added new icon font glyphs.
- Updated icon font file for improved rendering.
- Updated icon font sizes and hover animations for navigation buttons.
- Removed border styles from styled Container component.
- Removed unused import and updated icon for '/settings/model' menu item.
2024-09-06 10:00:18 +08:00
kangfenmao
f194ebbc20 chore(version): 0.6.7 2024-09-05 23:53:47 +08:00
kangfenmao
ab0e7e1e07 feat: change topics position 2024-09-05 23:53:47 +08:00
kangfenmao
d809f50c0e feat: Update Content-Security-Policy to allow file: frame-src #38
- Updated Content-Security-Policy directive to allow frame-src from file: in the HTML document.
2024-09-05 17:19:17 +08:00
kangfenmao
a48d24de26 refactor: renamed and refactored topic properties and added date-time tracking
- Renamed localforage topic item property from topic object to id.
- Added date-time tracking for assistant topics.
- Incremented the store version to 24.
- Refactored migrate function to add support for local storage and update topics timestamps.
- Added createdAt and updatedAt properties to Topic type.
2024-09-05 16:15:48 +08:00
kangfenmao
0dacc20e74 docs(DragableList): improve types and props documentation for DragDropContext responders 2024-09-05 15:30:26 +08:00
kangfenmao
08df6cb4f8 feat: highlight acitve topic icon 2024-09-05 14:36:19 +08:00
kangfenmao
0676ac8942 feat: quickly edit the asistant on edit title #42 2024-09-05 13:41:47 +08:00
kangfenmao
c257e8f0fe fix: anthropic first message must use the user role #39
{"type":"error","error":{"type":"invalid_request_error","message":"messages: first message must use the "user" role"}}
2024-09-05 13:35:16 +08:00
kangfenmao
521670f683 fix: assistant and topic list style 2024-09-05 00:04:35 +08:00
kangfenmao
87216b5d91 chore(version): 0.6.6 2024-09-04 22:33:15 +08:00
kangfenmao
e6122a3d36 fix: left sidebar icon 2024-09-04 22:31:39 +08:00
kangfenmao
e6e1502308 feat: remove hashtag title 2024-09-04 21:57:23 +08:00
kangfenmao
7f5be3a688 chore(version): 0.6.5 2024-09-04 21:29:56 +08:00
kangfenmao
4dde49a9f0 feat: new chat style 2024-09-04 21:29:16 +08:00
kangfenmao
ce830b692b revert: fold topics 2024-09-04 15:37:39 +08:00
kangfenmao
563472f3a9 wip 2024-09-04 13:26:51 +08:00
kangfenmao
14acd45927 feat: transparent window settings 2024-09-04 11:23:45 +08:00
kangfenmao
9e2c7a08df feat: change assistant sidebar width 2024-09-03 23:37:40 +08:00
kangfenmao
f10c8dc379 chore(version): 0.6.4 2024-09-03 22:14:12 +08:00
kangfenmao
fdd815879a feat: double click to change assistat view 2024-09-03 22:13:25 +08:00
kangfenmao
635f238576 chore(version): 0.6.3 2024-09-03 20:50:46 +08:00
kangfenmao
615e337e3f fix: assistant nav style 2024-09-03 20:50:37 +08:00
kangfenmao
acd5d4b192 feat: change default avatar 2024-09-03 20:39:27 +08:00
kangfenmao
9a41b697c6 fix: inputbar height 2024-09-03 20:11:25 +08:00
kangfenmao
5cb67e00a6 feat: change default provider 2024-09-03 20:11:20 +08:00
kangfenmao
350f13e97c fix: backup and restore i18n 2024-09-03 19:30:21 +08:00
kangfenmao
4d6cbf5073 refactor: provider sdk 2024-09-03 19:00:24 +08:00
kangfenmao
8d7b10d21e refactor: remove modal enabled key 2024-09-03 13:17:55 +08:00
kangfenmao
6753a93c0d fix: use webview replace iframe 2024-09-03 13:17:38 +08:00
kangfenmao
9ee763337d refactor: remove models config enabled 2024-09-03 11:40:46 +08:00
kangfenmao
ace0cb7823 feat: merge assistant and topics 2024-09-03 11:36:57 +08:00
kangfenmao
44e518ef03 refactor: assistant drap and drop 2024-09-02 20:48:31 +08:00
kangfenmao
e28b96b45e feat: expand inputbar height 2024-09-02 15:38:48 +08:00
kangfenmao
11427a980c feat: auto change inputbar height 2024-09-02 14:09:03 +08:00
kangfenmao
cb95562e58 feat: add attachment button 2024-09-01 23:22:21 +08:00
kangfenmao
89bdab58f7 feat: hide entry for local ai 2024-08-28 18:11:35 +08:00
kangfenmao
d42ee59335 fix: https://github.com/electron/notarize/issues/193 2024-08-27 19:42:39 +08:00
kangfenmao
88e7ab211d fix: electron-builder files path 2024-08-27 19:42:32 +08:00
kangfenmao
5347bdfa83 refactor: change env file path 2024-08-27 11:58:19 +08:00
kangfenmao
c8711c5804 feat: add local module 2024-08-27 11:31:05 +08:00
kangfenmao
24cf3bb043 chore(version): 0.6.2 2024-08-26 18:30:05 +08:00
kangfenmao
0531ecf3cf fix: electron builder ignore files 2024-08-26 18:19:01 +08:00
kangfenmao
0cbfd26883 build: remove sentry 2024-08-26 18:06:07 +08:00
kangfenmao
ee398489de build: remove electron-devtools-installer 2024-08-26 18:02:20 +08:00
kangfenmao
71d7c2c738 fix: workspace config 2024-08-26 17:49:19 +08:00
kangfenmao
b98f7298a2 build: add yarn workspace config 2024-08-25 22:12:31 +08:00
kangfenmao
de4f2599be refactor: remove unnecessary logs 2024-08-25 21:37:13 +08:00
kangfenmao
93b32e8e21 feat: update user data path 2024-08-25 18:39:53 +08:00
kangfenmao
e353d0f8ee fix: default assistant name 2024-08-23 21:41:16 +08:00
kangfenmao
dfd42fe9a6 feat: add devv referral code 2024-08-23 20:57:54 +08:00
kangfenmao
a2dc325896 chore(version): 0.6.1 2024-08-22 19:17:35 +08:00
kangfenmao
b131d320ea feat: more ai minapp 2024-08-22 18:45:06 +08:00
kangfenmao
b88f4a869e wip 2024-08-22 16:36:04 +08:00
kangfenmao
461458e5ec refactor: remove minapp.html 2024-08-22 13:04:24 +08:00
kangfenmao
4c2014f1d6 chore(version): 0.6.0 2024-08-21 10:28:31 +08:00
kangfenmao
647dd3e751 feat: add minapps 2024-08-21 10:14:04 +08:00
kangfenmao
4225312d4a chore(version): 0.5.9 2024-08-20 13:42:50 +08:00
kangfenmao
c2a4613e32 fix: windows minapp control button 2024-08-18 23:37:09 +08:00
kangfenmao
5d5c1eee74 feat: change sidebar width 2024-08-18 22:20:09 +08:00
kangfenmao
c1b5e6b183 feat: new input status bar style 2024-08-18 20:44:55 +08:00
kangfenmao
fd37ba18dc chore(version): 0.5.8 2024-08-18 18:06:56 +08:00
kangfenmao
4a26f7ce78 feat: add minimax provider 2024-08-18 18:06:21 +08:00
kangfenmao
8b38ebcac4 fix: hmr recycle 2024-08-18 17:10:59 +08:00
kangfenmao
e8dac28787 fix: graph rag model id 2024-08-17 21:54:34 +08:00
kangfenmao
3ccebb503f fix: input text 2024-08-17 21:30:28 +08:00
kangfenmao
42327836de fix: graphrag node url 2024-08-17 21:30:04 +08:00
kangfenmao
4d7a3bb8c3 feat: add minapp window 2024-08-17 17:11:48 +08:00
kangfenmao
1996e163c9 feat: add minapp window 2024-08-17 13:30:54 +08:00
kangfenmao
e43f7f87ab feat: window.app add app path 2024-08-16 22:44:00 +08:00
kangfenmao
47a83fa67f fix: minapp title null 2024-08-16 22:43:18 +08:00
kangfenmao
5e954566c9 chore(version): 0.5.7 2024-08-16 17:41:30 +08:00
kangfenmao
b8960ef02c fix: windows frame background color 2024-08-16 17:41:14 +08:00
kangfenmao
1866b00265 feat: add user edit modal & add prompt block 2024-08-16 17:19:18 +08:00
kangfenmao
be0799a4c6 chore(version): 0.5.6 2024-08-14 21:32:14 +08:00
kangfenmao
d0f5547419 feat: new windows and linux sidebar style 2024-08-14 21:28:44 +08:00
kangfenmao
076011b02b fix: anthropic message generating error 2024-08-14 20:35:57 +08:00
kangfenmao
ba5c70c45a feat: add minapp popup 2024-08-14 19:47:58 +08:00
kangfenmao
ebe74ffd05 chore(version): 0.5.5 2024-08-13 21:10:04 +08:00
kangfenmao
d0bea0491f fix(settings): provider list scroll 2024-08-13 21:04:04 +08:00
kangfenmao
514e1a4796 chore: remove ahooks 2024-08-13 20:50:54 +08:00
kangfenmao
2ffedadee4 Revert "feat(translate): use full screen input"
This reverts commit b0c479190c.
2024-08-13 20:48:51 +08:00
kangfenmao
7b72783ae7 feat: add graphrag provider 2024-08-13 20:48:38 +08:00
kangfenmao
4485a00395 feat: add doubao provider 2024-08-13 19:41:01 +08:00
kangfenmao
77c0952635 feat: add stepfun provider 2024-08-13 18:02:00 +08:00
kangfenmao
e1c7a25b87 feat: add gemini provider 2024-08-13 16:51:52 +08:00
kangfenmao
b0c479190c feat(translate): use full screen input 2024-08-13 14:57:46 +08:00
kangfenmao
c7c3d28893 chore(version): 0.5.4 2024-08-12 22:37:09 +08:00
kangfenmao
994ee8d7df feat: change sidebar opacity 2024-08-12 22:35:35 +08:00
kangfenmao
57f9550891 feat: add font size options to assistant settings pannel 2024-08-12 22:21:47 +08:00
kangfenmao
0c0d1560db feat: about page add icons 2024-08-12 22:03:16 +08:00
kangfenmao
145d7ee748 refactor: slider onChange event 2024-08-12 21:48:59 +08:00
kangfenmao
52af23b931 feat: enable anthropic api host edit 2024-08-12 21:31:32 +08:00
kangfenmao
f7151bd066 feat: add change message font size feature #22
支持消息字体大小调节
2024-08-12 21:28:18 +08:00
kangfenmao
744a1fedba style(Inputbar): add width: auto to Textarea 2024-08-11 16:18:06 +08:00
kangfenmao
978432d910 fix: clear topic white generating 2024-08-11 16:11:31 +08:00
kangfenmao
b6cb1e4d84 refactor: code format 2024-08-11 15:49:08 +08:00
kangfenmao
0096783f26 chore(version): 0.5.3 2024-08-09 18:58:58 +08:00
kangfenmao
4fc53d7c19 feat: new inputbar style 2024-08-09 18:56:45 +08:00
亢奋猫
34d99b711c Update README.md 2024-08-09 11:30:56 +08:00
kangfenmao
5dd74a1018 chore(version): 0.5.2 2024-08-08 23:53:18 +08:00
kangfenmao
e028d0600f fix: windows style 2024-08-08 23:30:55 +08:00
kangfenmao
64ee3f2108 chore(version): 0.5.1 2024-08-08 18:13:47 +08:00
kangfenmao
30a082b979 fix: filter empty user messages 2024-08-08 18:13:15 +08:00
kangfenmao
5a0927393d feat(message): add error tips 2024-08-08 17:57:57 +08:00
kangfenmao
16c68dcdcb fix: inputbar height 2024-08-08 17:21:00 +08:00
kangfenmao
b6500977b0 fix: model settings crash 2024-08-08 17:16:45 +08:00
kangfenmao
78cf33e8bc feat(AssistantSettings.tsx): fix reset functionality 2024-08-08 16:49:18 +08:00
kangfenmao
2f62f04adf feat(ModelSettings.tsx): sorting model names and capitalizing first letter 2024-08-08 16:36:36 +08:00
kangfenmao
84915b1ede feat(AboutSettings.tsx): add GithubOutlined icon linking to project repository for better user navigation and project visibility 2024-08-08 16:12:42 +08:00
亢奋猫
248c7ea20e Update README.md 2024-08-08 15:43:01 +08:00
kangfenmao
1031d40ddb docs(README.md): add star history chart for project visibility 2024-08-08 15:38:31 +08:00
kangfenmao
3d44fc2208 fix: navbar style on linux 2024-08-08 14:50:36 +08:00
kangfenmao
22e3c0e270 build: add linux target 2024-08-08 11:31:15 +08:00
kangfenmao
5d81874166 fix(i18n): update default assistant emoji from 😀 to 🔆 2024-08-08 09:18:33 +08:00
kangfenmao
f7ef895ce6 chore(version): 0.5.0 2024-08-07 21:55:51 +08:00
kangfenmao
beb40f5baf feat: fix add assistant search keywords format 2024-08-07 20:57:31 +08:00
kangfenmao
07613e65f5 feat: add max token limit #18 2024-08-07 20:49:21 +08:00
kangfenmao
6185068353 feat: use ubuntu font as default 2024-08-07 14:28:29 +08:00
kangfenmao
61934cd65c feat add agent popup #14 2024-08-07 13:23:29 +08:00
kangfenmao
41f65b66ba chore(version): 0.4.9 2024-08-06 20:41:34 +08:00
kangfenmao
5edb53ef7d feat: add ollama settings 2024-08-06 20:38:01 +08:00
kangfenmao
167988927b feat: add custom agent #14 2024-08-06 19:18:17 +08:00
kangfenmao
a39beb3841 fix(AboutSettings.tsx): handle errors in update check by setting loading state 2024-08-05 16:15:58 +08:00
197 changed files with 7128 additions and 4343 deletions

View File

@@ -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
@@ -52,8 +52,6 @@ jobs:
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Replace spaces in filenames

3
.gitignore vendored
View File

@@ -45,3 +45,6 @@ out
# ENV
.env
.env.*
# Local
local

View 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}`);
}

View File

@@ -1,2 +1,5 @@
nodeLinker: node-modules
enableImmutableInstalls: false
httpTimeout: 300000
nodeLinker: node-modules

View File

@@ -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
![](https://github.com/user-attachments/assets/e32b244f-3a84-473a-89ef-0b12ef4127b2)
![](https://github.com/user-attachments/assets/e24d1e7d-126a-4647-bd98-f470bfe26fde)
![](https://github.com/user-attachments/assets/18c10eed-4711-4975-bf9c-b274c61924f3)
![](https://github.com/user-attachments/assets/3f3f0bfa-cb88-4abf-923a-a0859fa3c912)
![](https://github.com/user-attachments/assets/7395ebf2-64f8-46fa-aa48-63293516c320)
![](https://github.com/user-attachments/assets/288560c1-d218-437c-87c2-2a5e87b43b93)
# 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
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
# 📃 License
[LICENSE](./LICENSE)

View File

@@ -3,12 +3,13 @@ productName: Cherry Studio
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!{.vscode,.yarn,.github}'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- '!src/*'
- '!local'
asarUnpack:
- resources/**
win:
@@ -40,8 +41,8 @@ dmg:
linux:
target:
- AppImage
- snap
- deb
# - snap
# - deb
maintainer: electronjs.org
category: Utility
appImage:
@@ -56,6 +57,6 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
支持切换模型重新生成答案
修复数学公式渲染问题
输入状态提示样式更新
智能助理和消息列表合并
优化输入框样式
提升小程序稳定性

View File

@@ -4,7 +4,12 @@ import { resolve } from 'path'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
plugins: [externalizeDepsPlugin()],
resolve: {
alias: {
ollama: resolve('ollama/src')
}
}
},
preload: {
plugins: [externalizeDepsPlugin()]
@@ -15,7 +20,6 @@ export default defineConfig({
'@renderer': resolve('src/renderer/src')
}
},
plugins: [react()],
assetsInclude: ['**/*.md']
plugins: [react()]
}
})

View File

@@ -1,10 +1,16 @@
{
"name": "cherry-studio",
"version": "0.4.8",
"name": "CherryStudio",
"version": "0.6.9",
"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,19 +50,20 @@
"@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",
"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",
@@ -74,6 +80,7 @@
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
@@ -89,7 +96,8 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"resolutions": {
"@electron/notarize": "2.3.2"
"@electron/notarize": "2.3.2",
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
},
"packageManager": "yarn@4.3.1"
}

68
resources/graphrag.html Normal file
View 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

36
resources/js/bridge.js Normal file
View 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
View 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)
}

View File

@@ -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)
}
}

View File

@@ -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'
})

58
src/main/ipc.ts Normal file
View 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()
}
})
}

View File

@@ -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')
})

24
src/main/utils/aes.ts Normal file
View 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
View 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
View 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
View 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
View 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
}

View File

@@ -1,4 +1,5 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import type { OpenDialogOptions } from 'electron'
declare global {
interface Window {
@@ -7,12 +8,18 @@ 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>
}
}
}

View File

@@ -7,8 +7,15 @@ 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)
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -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: *; frame-src * file:" />
</head>
<body>
<div id="root"></div>

View File

@@ -5,12 +5,13 @@ import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { ThemeProvider } from './context/ThemeProvider'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import HomePage from './pages/home/HomePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
import AntdProvider from './providers/AntdProvider'
import { ThemeProvider } from './providers/ThemeProvider'
function App(): JSX.Element {
return (
@@ -23,8 +24,9 @@ function App(): JSX.Element {
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>

View File

@@ -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;
}

View File

@@ -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';
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -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

View File

@@ -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;
@@ -31,23 +31,26 @@
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff20;
--color-border: #000;
--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: 240px;
--topic-list-width: 270px;
--settings-width: var(--assistants-width);
}
body[theme-mode='light'] {
@@ -79,14 +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);
}
*,
@@ -97,6 +102,14 @@ body[theme-mode='light'] {
font-weight: normal;
}
*:focus {
outline: none;
}
* {
-webkit-tap-highlight-color: transparent;
}
ul {
list-style: none;
}
@@ -105,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;
@@ -165,3 +169,42 @@ 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;
}

View File

@@ -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,36 +97,45 @@
}
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;
}
}
}
blockquote {
margin: 1em 0;
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 {
@@ -230,3 +248,7 @@
}
}
}
emoji-picker {
--border-size: 0;
}

View File

@@ -1,7 +1,7 @@
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 3px;
height: 3px;
width: 2px;
height: 2px;
}
::-webkit-scrollbar-track {

View 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

View File

@@ -0,0 +1,58 @@
import {
DragDropContext,
Draggable,
Droppable,
DropResult,
OnDragEndResponder,
OnDragStartResponder,
ResponderProvided
} from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { FC } from 'react'
interface Props<T> {
list: T[]
style?: React.CSSProperties
children: (item: T, index: number) => React.ReactNode
onUpdate: (list: T[]) => void
onDragStart?: OnDragStartResponder
onDragEnd?: OnDragEndResponder
}
const DragableList: FC<Props<any>> = ({ children, list, style, 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">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{list.map((item, index) => (
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...style }}>
{children(item, index)}
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
)
}
export default DragableList

View File

@@ -0,0 +1,25 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { FC, useEffect, useRef } from 'react'
interface Props {
onEmojiClick: (emoji: string) => void
}
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
const { theme } = useTheme()
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
ref.current.addEventListener('emoji-click', (event: any) => {
event.stopPropagation()
onEmojiClick(event.detail.emoji.unicode)
})
}
}, [onEmojiClick])
// @ts-ignore next-line
return <emoji-picker ref={ref} class={theme === 'dark' ? 'dark' : 'light'} style={{ border: 'none' }} />
}
export default EmojiPicker

View 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

View File

@@ -150,11 +150,12 @@ export const BaseTypography = styled(Box)<{
`
export const TypographyNormal = styled(BaseTypography)`
font-family: 'Poppins';
font-family: 'Ubuntu';
`
export const TypographyBold = styled(BaseTypography)`
font-family: 'Poppins Bold';
font-family: 'Ubuntu';
font-weight: bold;
`
export const Container = styled.main<ContainerProps>`

View File

@@ -0,0 +1,185 @@
/* eslint-disable react/no-unknown-property */
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant'
import { useBridge } from '@renderer/hooks/useBridge'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
import { Drawer } from 'antd'
import { WebviewTag } from 'electron'
import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import { TopView } from '../TopView'
interface Props {
app: MinAppType
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const [open, setOpen] = useState(true)
const webviewRef = useRef<WebviewTag | null>(null)
useBridge()
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const onClose = () => {
setOpen(false)
setTimeout(() => resolve({}), 300)
}
const onReload = () => {
if (webviewRef.current) {
webviewRef.current.src = app.url
}
}
const onOpenLink = () => {
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 (
<Drawer
title={<Title />}
placement="bottom"
onClose={onClose}
open={open}
mask={true}
rootClassName="minapp-drawer"
maskClassName="minapp-mask"
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}>
<webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />
</Drawer>
)
}
const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))',
height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'white',
display: 'inline-flex'
}
const TitleContainer = styled.div`
display: flex;
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`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
-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`
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
}
`
export default class MinApp {
static topviewId = 0
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
}
static start(app: MinAppType) {
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
app={app}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
}
}

View File

@@ -0,0 +1,135 @@
import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import { Agent, Assistant } from '@renderer/types'
import { Input, Modal, Tag } from 'antd'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
resolve: (value: Assistant | undefined) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { agents: userAgents } = useAgents()
const [searchText, setSearchText] = useState('')
const { defaultAssistant } = useDefaultAssistant()
const { assistants, addAssistant } = useAssistants()
const defaultAgent: Agent = useMemo(
() => ({
id: defaultAssistant.id,
name: defaultAssistant.name,
emoji: defaultAssistant.emoji || '',
prompt: defaultAssistant.prompt,
group: 'system'
}),
[defaultAssistant.emoji, defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
)
const agents = useMemo(() => {
const allAgents = [...userAgents, ...systemAgents] as Agent[]
const list = [defaultAgent, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
return searchText
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
: list
}, [assistants, defaultAgent, searchText, userAgents])
const onCreateAssistant = (agent: Agent) => {
if (agent.id !== 'default') {
if (assistants.map((a) => a.id).includes(String(agent.id))) {
return
}
}
const assistant = covertAgentToAssistant(agent)
addAssistant(assistant)
resolve(assistant)
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
resolve(undefined)
AddAssistantPopup.hide()
}
return (
<Modal
style={{ marginTop: '5vh' }}
title={t('chat.add.assistant.title')}
open={open}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade"
footer={null}>
<Input
placeholder={t('common.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
autoFocus
style={{ marginBottom: 16 }}
/>
<Container>
{agents.map((agent) => (
<AgentItem key={agent.id} onClick={() => onCreateAssistant(agent)}>
{agent.emoji} {agent.name}
{agent.group === 'system' && <Tag color="orange">{t('agents.tag.system')}</Tag>}
{agent.group === 'user' && <Tag color="green">{t('agents.tag.user')}</Tag>}
</AgentItem>
))}
</Container>
</Modal>
)
}
const Container = styled.div`
height: 50vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 8px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
cursor: pointer;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default class AddAssistantPopup {
static topviewId = 0
static hide() {
TopView.hide('AddAssistantPopup')
}
static show() {
return new Promise<Assistant | undefined>((resolve) => {
TopView.show(<PopupContainer resolve={resolve} />, 'AddAssistantPopup')
})
}
}

View File

@@ -34,7 +34,14 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
}
return (
<Modal title={assistant.name} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}>
<Modal
title={assistant.name}
open={open}
onOk={onOk}
onCancel={handleCancel}
afterClose={onClose}
transitionName="ant-move-down"
maskTransitionName="ant-fade">
<Box mb={8}>{t('common.name')}</Box>
<Input
placeholder={t('common.assistant') + t('common.name')}
@@ -45,7 +52,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
{t('common.prompt')}
</Box>
<TextArea
rows={4}
rows={10}
placeholder={t('common.assistant') + t('common.prompt')}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
@@ -57,18 +64,19 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'AssistantSettingPopup'
)
})
}

View File

@@ -58,18 +58,19 @@ const PromptPopupContainer: React.FC<Props> = ({
export default class PromptPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('PromptPopup')
}
static show(props: PromptPopupShowParams) {
return new Promise<string>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PromptPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'PromptPopup'
)
})
}

View File

@@ -37,18 +37,19 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
export default class TemplatePopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('TemplatePopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'TemplatePopup'
)
})
}

View 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'
)
})
}
}

View File

@@ -1,87 +1,94 @@
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd'
import { findIndex, pullAt } from 'lodash'
import React, { useEffect, useState } from 'react'
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
import { Box } from '../Layout'
let id = 0
let onPop = () => {}
let onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
element
key
id
}
let onHide = ({ key }: { key: number }) => {
key
let onHide = (id: string) => {
id
}
let onHideAll = () => {}
interface Props {
children?: React.ReactNode
}
type ElementItem = {
key: number
id: string
element: React.FC | React.ReactNode
}
const TopViewContainer: React.FC<Props> = ({ children }) => {
const [elements, setElements] = useState<ElementItem[]>([])
const elementsRef = useRef<ElementItem[]>([])
elementsRef.current = elements
const [messageApi, messageContextHolder] = message.useMessage()
const [modal, modalContextHolder] = Modal.useModal()
useAppInit()
onPop = () => {
const views = [...elements]
views.pop()
setElements(views)
}
onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
setElements(elements.concat([{ element, key }]))
}
onHide = ({ key }: { key: number }) => {
const views = [...elements]
pullAt(views, findIndex(views, { key }))
setElements(views)
}
useEffect(() => {
window.message = messageApi
window.modal = modal
}, [messageApi, modal])
onPop = () => {
const views = [...elementsRef.current]
views.pop()
elementsRef.current = views
setElements(elementsRef.current)
}
onShow = ({ element, id }: ElementItem) => {
if (!elementsRef.current.find((el) => el.id === id)) {
elementsRef.current = elementsRef.current.concat([{ element, id }])
setElements(elementsRef.current)
}
}
onHide = (id: string) => {
elementsRef.current = elementsRef.current.filter((el) => el.id !== id)
setElements(elementsRef.current)
}
onHideAll = () => {
setElements([])
elementsRef.current = []
}
const FullScreenContainer: React.FC<PropsWithChildren> = useCallback(({ children }) => {
return (
<Box flex={1} position="absolute" w="100%" h="100%">
<Box position="absolute" w="100%" h="100%" onClick={onPop} />
{children}
</Box>
)
}, [])
return (
<>
{children}
{messageContextHolder}
{modalContextHolder}
{elements.length > 0 && (
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
{elements.map(({ element: Element, key }) =>
typeof Element === 'function' ? (
<Element key={`TOPVIEW_${key}`} />
) : (
<div key={`TOPVIEW_${key}`}>{Element}</div>
)
)}
</div>
)}
{elements.map(({ element: Element, id }) => (
<FullScreenContainer key={`TOPVIEW_${id}`}>
{typeof Element === 'function' ? <Element /> : Element}
</FullScreenContainer>
))}
</>
)
}
export const TopView = {
show: (element: React.FC | React.ReactNode) => {
id = id + 1
onShow({ element, key: id })
return id
},
hide: (key: number) => {
onHide({ key })
},
show: (element: React.FC | React.ReactNode, id: string) => onShow({ element, id }),
hide: (id: string) => onHide(id),
clear: () => onHideAll(),
pop: onPop
}

Some files were not shown because too many files have changed in this diff Show More