Compare commits

..

101 Commits

Author SHA1 Message Date
kangfenmao
ebf61b1ce9 feat: plugins 2024-12-30 23:45:47 +08:00
kangfenmao
1a68587684 fix: Microsoft Visual C++ Redistributable #577 2024-12-30 15:07:31 +08:00
kangfenmao
47c455b125 feat: 增加保持并发送的功能 #527 2024-12-30 14:09:59 +08:00
kangfenmao
96124cf58e feat: 增加genspark小程序 #578 2024-12-30 13:10:27 +08:00
juzeon
ef975add01 fix: 修复zh-tw语言文件中的乱码 (#579) 2024-12-30 11:49:40 +08:00
n2yt584v2t4nh7y
ed49066bab feat: 添加自定义API参数功能 (#564)
* add custom api parameters

* allow more data types for custom api parameters

* pass parameter to api payload

* add custom parameter settings to sidebar

* remove unnecessary object and array types

* extract API custom parameter method to BaseProvider

* add i18n for custom parameter settings

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2024-12-29 20:19:07 +08:00
kangfenmao
e7545c5a94 feat: 用户自定义话题总结Prompt #562
close #562
2024-12-29 10:20:45 +08:00
kangfenmao
fc35df65b8 feat: add release notes pages 2024-12-29 09:49:22 +08:00
littel_penguin66
56ca81d245 Fix incorrect synchronization behavior of webdav auto sync (#568) 2024-12-29 08:44:21 +08:00
kangfenmao
6bc1f4b640 chore(version): 0.9.2 2024-12-27 23:03:17 +08:00
kangfenmao
ccb216e76a fix: 模型名后面标注一下服务商 #557 2024-12-27 18:09:22 +08:00
kangfenmao
60931b85ff fix: model settings params step size 2024-12-27 16:47:44 +08:00
kangfenmao
dc1dbc7bb6 feat: add jina provider 2024-12-27 16:29:17 +08:00
kangfenmao
5d2efbd62b fix: 需要只发送图片功能 #538 2024-12-27 14:40:44 +08:00
sommermorgentraum
5337017648 feat: Add capabilities for user to load custom CSS #548 2024-12-27 14:11:12 +08:00
kangfenmao
c409256ae9 fix: azure openai embedding 2024-12-27 14:02:53 +08:00
kangfenmao
4ac608052c chore: update dependencies and improve project structure 2024-12-27 12:42:17 +08:00
kangfenmao
5e6aaabb23 fix: 小程序中增加 github copilot #547 2024-12-27 12:10:41 +08:00
kangfenmao
8812daeeee fix: 某些输出包含 sub 无法正常显示 #545 2024-12-27 11:54:11 +08:00
kangfenmao
13e3a8478c feat: added topic message update and search state management 2024-12-27 11:48:12 +08:00
kangfenmao
8687985ccb feat: add windows platform support for node file detection and npm package download 2024-12-26 12:38:51 +08:00
kangfenmao
7d54f9b4fa chore(version): 0.9.1 2024-12-26 12:25:58 +08:00
kangfenmao
6b7ba35183 fix: build native module script 2024-12-26 12:25:58 +08:00
kangfenmao
5b42a6d054 feat: add embeding tag to settings 2024-12-26 12:25:58 +08:00
kangfenmao
153e7a9299 refactor: knowledge base engine change to libsql 2024-12-26 10:00:37 +08:00
littel_penguin66
77e0c5172e Add Japanese localization for i18n (#533) 2024-12-25 22:04:29 +08:00
kangfenmao
c50ac440c8 fix: knowledge base bugs 2024-12-25 21:54:46 +08:00
kangfenmao
34ebab0af8 refactor: knowledge base database engine 2024-12-25 17:42:03 +08:00
Tan Xiang
b85765915e fix: shortcut tips (#525) 2024-12-24 23:09:54 +08:00
kangfenmao
960f50e4e4 fix: gemini web serach modal 400 request error 2024-12-24 18:00:25 +08:00
kangfenmao
65e19d187c revert: cloudflare-worker.js 2024-12-24 17:38:30 +08:00
kangfenmao
aa4f94f8a4 build: download npm node native modules 2024-12-24 17:24:38 +08:00
kangfenmao
aa3812eddc fix: linux window title style 2024-12-24 14:43:32 +08:00
kangfenmao
6b9e58171b feat: update models inside 2024-12-24 13:27:40 +08:00
kangfenmao
2f64653b1e fix: knowledge base bugs 2024-12-24 12:41:58 +08:00
kangfenmao
03dd3038e0 patch: @llm-tools 2024-12-24 12:11:07 +08:00
kangfenmao
f1f7e8e11b feat: added webdav auto-sync settings synchronization and custom show message option 2024-12-24 10:25:19 +08:00
kangfenmao
fbd189c5e1 Merge branch 'knowledge' 2024-12-24 09:38:38 +08:00
little_penguin66
87c3716f75 add autoSync in WebDav 2024-12-24 09:34:16 +08:00
kangfenmao
37477587b6 fix: check provider connection use the last model 2024-12-24 09:33:43 +08:00
kangfenmao
d558572d97 chore(version): 0.9.0 2024-12-23 17:07:26 +08:00
kangfenmao
7506d04c55 build: reduce package size 2024-12-23 14:22:37 +08:00
kangfenmao
35fd5aef22 fix: knowledge bugs 2024-12-23 10:48:40 +08:00
kangfenmao
8f11d2b1c9 chore: remove release and build workflow, update release workflow for macos-latest 2024-12-19 17:24:39 +08:00
kangfenmao
9aa2a4727d build: add matrix 2024-12-19 17:20:52 +08:00
kangfenmao
ca6027dd83 feat: remove knowledge queue 2024-12-19 13:45:11 +08:00
kangfenmao
c2462fd51c feat: knowledge base 2024-12-19 09:24:20 +08:00
tanxiang
0739758469 feat(i18n): add "Switch Model" message to multiple locales and update tooltip in MessageMenubar 2024-12-18 13:35:39 +08:00
adfnekc
b2554333a9 feat: message 增加 metrics 字段 用以统计token生成速度和首字时延 (#479) 2024-12-16 17:10:36 +08:00
kangfenmao
6ced973b35 chore(version): 0.8.27 2024-12-16 15:47:07 +08:00
kangfenmao
ccbeefc546 feat: added long text paste control and threshold settings 2024-12-16 15:08:40 +08:00
kangfenmao
7fdc2db522 fix: o1模型支持流式输出 #439 2024-12-16 14:48:51 +08:00
kangfenmao
978f1342e4 feat: disable select menu text 2024-12-16 14:39:54 +08:00
kangfenmao
ff935a656e feat: add copy last message shortcuts 2024-12-16 14:13:59 +08:00
kangfenmao
15539a5609 feat: add thinkany minapp 2024-12-16 13:42:08 +08:00
kangfenmao
88cd4f2144 fix: mermaid图表代码一键复制功能 #460 2024-12-16 13:20:24 +08:00
kangfenmao
daf2e035b2 fix: 输出不显示 markdown 的小圆点 #446 2024-12-16 12:58:31 +08:00
kangfenmao
7ceb4920ec feat: added hotkey functionality and improved appstorepopover layout 2024-12-16 12:55:14 +08:00
kangfenmao
0074d5c8b4 feat: add svg preview 2024-12-16 12:35:39 +08:00
kangfenmao
96737ed695 feat: add display settings 2024-12-16 12:04:12 +08:00
kangfenmao
356da1ea67 feat: add miniapp icon to navbar right 2024-12-16 11:32:50 +08:00
kangfenmao
debf996146 feat: add n.cn to minapp list 2024-12-16 10:48:55 +08:00
kangfenmao
8d73d1e844 fix: input bar default rows #431 2024-12-16 10:26:04 +08:00
duanyongcheng77
b0d777293b feat: 🎸 可以多次点击上传文件按钮上传文件 2024-12-16 09:54:12 +08:00
kangfenmao
1a9fbbc0a2 fix: KaTeX引擎公式渲染错位 #473 2024-12-16 09:42:10 +08:00
kangfenmao
ab99a7b96d chore(version): 0.8.26 2024-12-15 18:03:36 +08:00
kangfenmao
7d561dbfb7 feat: added setshowassistants function to useshowassistants hook and updated error handling logic 2024-12-13 16:37:48 +08:00
kangfenmao
6af07c278d fix: handle unknown models in iswebsearchmodel function 2024-12-13 10:28:09 +08:00
kangfenmao
9c18b851cc build: update electron version 2024-12-13 09:52:18 +08:00
Shelly
b1ebe13b5f feat: 🎸 allowMarkdownLongTextToAutomaticallyWrap (#454)
Co-authored-by: duanyongcheng77 <duanyongcheng77@gmail.com>
2024-12-13 09:51:42 +08:00
kangfenmao
9b258734c4 chore: update dependencies and remove unused code 2024-12-13 09:35:40 +08:00
kangfenmao
25eb97902b chore(version): 0.8.25 2024-12-12 18:24:06 +08:00
kangfenmao
2fae6e4a3e feat: add web search for google gemini modal gemini-2.0-flash-exp 2024-12-12 14:26:52 +08:00
Shelly
f312c5fc40 feat: 🎸 add shortcut for command + enter (#443)
* feat: 🎸 add shortcut for command

* feat: 🎸 only command

---------

Co-authored-by: duanyongcheng77 <duanyongcheng77@gmail.com>
2024-12-12 14:22:41 +08:00
kangfenmao
afa96549a3 styles: use mac style 2024-12-11 20:02:15 +08:00
kangfenmao
6beee78ce8 fix: can not delete last message 2024-12-11 20:01:52 +08:00
kangfenmao
a230ee2c69 chore(version): 0.8.24 2024-12-11 11:41:16 +08:00
kangfenmao
28a27447a5 feat: add new social media translations and links 2024-12-10 20:36:37 +08:00
kangfenmao
408976e5dc feat: add shortcut for assistant and topic show 2024-12-10 20:28:05 +08:00
kangfenmao
7153996c35 fix: reduced message counts for messages component 2024-12-10 19:53:14 +08:00
kangfenmao
73f6a743cd feat: add enter key trigger for translate model prompt 2024-12-10 19:41:50 +08:00
kangfenmao
3b250d7d78 style: improved layout and styling 2024-12-10 19:39:00 +08:00
kangfenmao
272efaf76e feat: add top-p settings #224 2024-12-10 19:24:30 +08:00
kangfenmao
44c64a571a feat: sidebar shadow 2024-12-10 18:07:37 +08:00
kangfenmao
f817d9136b fix: 清除上下文按钮容易误点 #426 2024-12-10 17:23:00 +08:00
kangfenmao
c0f192c6f2 fix: support "ctrl+enter" as send shortcuts #244 2024-12-10 17:09:57 +08:00
kangfenmao
b5a109401c feat: add update info ui 2024-12-10 17:06:29 +08:00
牡丹凤凰
aeff59946c Update models.ts 2024-12-10 05:23:13 +08:00
kangfenmao
21ad4cfecc refactor: improve llmodel group assignment logic and sorting 2024-12-09 11:26:02 +08:00
kangfenmao
4df39179bb fix: escaped special characters in code snippets #419 2024-12-09 09:50:15 +08:00
牡丹凤凰
423fdb6992 Update cloudflare-worker.js 2024-12-07 15:28:57 +08:00
亢奋猫
f66adcd217 Merge pull request #418 from 1355873789/develop
历史消息懒加载
2024-12-07 14:41:48 +08:00
Amatsuka
465bf4006c elfix: Add the grok vision model and fix the incorrect marking of the grok beta model as a visual model. 2024-12-07 14:39:48 +08:00
首都爱护动物协会
14c9cb6001 历史消息懒加载
性能优化
2024-12-07 12:27:16 +08:00
牡丹凤凰
e35d928bcd Merge branch 'kangfenmao:main' into develop 2024-12-07 12:21:15 +08:00
duanyongcheng77
1981f2e648 style: 💄 change chinese-traditional icon to hk 2024-12-06 17:03:28 +08:00
亢奋猫
a4d8e71916 Merge branch 'main' into develop 2024-11-05 20:50:25 +08:00
首都爱护动物协会
39cf227e42 Match the avatar for the Pixtral model. 2024-11-05 17:26:04 +08:00
首都爱护动物协会
d2cad31db4 add pixtral avatar 2024-11-05 17:22:24 +08:00
首都爱护动物协会
aa864f3876 Add new model and provider avatars. 2024-11-05 16:04:30 +08:00
首都爱护动物协会
77a8b23d76 Add Providers
1. Fix the naming error of the Grok model
2. Add new providers: Grok, Mistral, Jina, Hyperbolic
2024-11-05 15:56:36 +08:00
152 changed files with 9234 additions and 2030 deletions

View File

@@ -2,11 +2,6 @@ name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version (e.g. v1.2.3)'
required: true
type: string
push:
tags:
- v*.*.*
@@ -34,18 +29,37 @@ jobs:
- name: Install corepack
run: corepack enable && corepack prepare yarn@4.3.1 --activate
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: Cache yarn dependencies
uses: actions/cache@v3
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: yarn install
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
run: |
yarn build:npm linux
yarn build:linux
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Build Mac
if: matrix.os == 'macos-latest'
run: yarn build:mac
run: |
yarn build:npm mac
yarn build:mac
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
@@ -61,7 +75,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Replace spaces in filenames
run: node scripts/replaceSpaces.js
run: node scripts/replace-spaces.js
- name: Release
uses: softprops/action-gh-release@v2

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@ node_modules
dist
out
build/icons
stats.html
# ENV
.env

View File

@@ -1,53 +0,0 @@
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

@@ -0,0 +1,25 @@
diff --git a/src/libsql-db.js b/src/libsql-db.js
index 58c42e4910bd0e53bc497ff9b9702b1f7a961266..250bc97c50a9b790e8798441d904d040f2d2af43 100644
--- a/src/libsql-db.js
+++ b/src/libsql-db.js
@@ -41,9 +41,9 @@ export class LibSqlDb {
}
async similaritySearch(query, k) {
const statement = `SELECT id, pageContent, uniqueLoaderId, source, metadata,
- vector_distance_cos(vector, vector32('[${query.join(',')}]'))
+ vector_distance_cos(vector, vector32('[${query.join(',')}]')) as distance
FROM ${this.tableName}
- ORDER BY vector_distance_cos(vector, vector32('[${query.join(',')}]')) ASC
+ ORDER BY distance ASC
LIMIT ${k};`;
this.debug(`Executing statement - ${truncateCenterString(statement, 700)}`);
const results = await this.client.execute(statement);
@@ -52,7 +52,7 @@ export class LibSqlDb {
return {
metadata,
pageContent: result.pageContent.toString(),
- score: 1,
+ score: 1 - result.distance,
};
});
}

View File

@@ -0,0 +1,17 @@
diff --git a/src/core/rag-embedding.js b/src/core/rag-embedding.js
index 50c3c4064af17bc4c7c46554d8f2419b3afceb0e..632c9b2e04d2e0e3bb09ef1cd8f29d2560e6afc1 100644
--- a/src/core/rag-embedding.js
+++ b/src/core/rag-embedding.js
@@ -1,10 +1,8 @@
export class RAGEmbedding {
static singleton;
static async init(embeddingModel) {
- if (!this.singleton) {
- await embeddingModel.init();
- this.singleton = new RAGEmbedding(embeddingModel);
- }
+ await embeddingModel.init();
+ this.singleton = new RAGEmbedding(embeddingModel);
}
static getInstance() {
return RAGEmbedding.singleton;

View File

@@ -0,0 +1,54 @@
diff --git a/src/util/strings.cjs b/src/util/strings.cjs
index 9933cc6e3866c476b47342a29ddb206eb90fa4a5..2965c4f2808bf94af9ef3e2ec889e5552e30e6ae 100644
--- a/src/util/strings.cjs
+++ b/src/util/strings.cjs
@@ -38,13 +38,16 @@ function toTitleCase(str) {
});
}
function isValidURL(url) {
- try {
- new URL(url);
- return true;
- }
- catch {
- return false;
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) {
+ try {
+ new URL(url);
+ return true;
+ }
+ catch {
+ return false;
+ }
}
+ return false;
}
function isValidJson(str) {
try {
diff --git a/src/util/strings.js b/src/util/strings.js
index f5c1655512099b880fc5022e95d5e0c4d1d073f2..1a64bd662a22efd2effd9d2846ffcf0b93391963 100644
--- a/src/util/strings.js
+++ b/src/util/strings.js
@@ -29,13 +29,16 @@ export function toTitleCase(str) {
});
}
export function isValidURL(url) {
- try {
- new URL(url);
- return true;
- }
- catch {
- return false;
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) {
+ try {
+ new URL(url);
+ return true;
+ }
+ catch {
+ return false;
+ }
}
+ return false;
}
export function isValidJson(str) {
try {

View File

@@ -1,5 +1,5 @@
diff --git a/core.js b/core.js
index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d82573ea5 100644
index 30c91e66bf595a66c09eb3dbcbda7d58154865f5..b511ff24ea1891904c60174c6ed26ecdd4d5ac51 100644
--- a/core.js
+++ b/core.js
@@ -156,7 +156,7 @@ class APIClient {
@@ -12,7 +12,7 @@ index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d
};
}
diff --git a/core.mjs b/core.mjs
index 8bc7a0ee10d61560d7113cf3f703355bb19f7ddd..5e4c8586ea6b13fe887a22af2de05eaa4700b5ec 100644
index ac267bcfcff44b1f7c9bea5513bba94726a31795..dd5bd9f29609d3f0eea4bd5b225f302893df14ad 100644
--- a/core.mjs
+++ b/core.mjs
@@ -149,7 +149,7 @@ export class APIClient {

View File

@@ -0,0 +1,29 @@
diff --git a/lib/pdf-parse.js b/lib/pdf-parse.js
index 96bfbc705dcb4fb73cb077a75f02c115371b3477..6d02d2bb426063c3a31cb740c3d86841de162a22 100644
--- a/lib/pdf-parse.js
+++ b/lib/pdf-parse.js
@@ -21,12 +21,12 @@ function render_page(pageData) {
for (let item of textContent.items) {
if (lastY == item.transform[5] || !lastY){
text += item.str;
- }
+ }
else{
text += '\n' + item.str;
- }
+ }
lastY = item.transform[5];
- }
+ }
//let strings = textContent.items.map(item => item.str);
//let text = strings.join("\n");
//text = text.replace(/[ ]+/ig," ");
@@ -60,7 +60,7 @@ async function PDF(dataBuffer, options) {
if (typeof options.version != 'string') options.version = DEFAULT_OPTIONS.version;
if (options.version == 'default') options.version = DEFAULT_OPTIONS.version;
- PDFJS = PDFJS ? PDFJS : require(`./pdf.js/${options.version}/build/pdf.js`);
+ PDFJS = PDFJS ? PDFJS : require(`./pdf.js/v1.10.100/build/pdf.js`);
ret.version = PDFJS.version;

47
build/nsis-installer.nsh Normal file
View File

@@ -0,0 +1,47 @@
;Inspired by:
; https://gist.github.com/bogdibota/062919938e1ed388b3db5ea31f52955c
; https://stackoverflow.com/questions/34177547/detect-if-visual-c-redistributable-for-visual-studio-2013-is-installed
; https://stackoverflow.com/a/54391388
; https://github.com/GitCommons/cpp-redist-nsis/blob/main/installer.nsh
;Find latests downloads here:
; https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
!include LogicLib.nsh
; https://github.com/electron-userland/electron-builder/issues/1122
!ifndef BUILD_UNINSTALLER
Function checkVCRedist
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
FunctionEnd
!endif
!macro customInit
Push $0
Call checkVCRedist
${If} $0 != "1"
MessageBox MB_YESNO "\
NOTE: ${PRODUCT_NAME} requires $\r$\n\
'Microsoft Visual C++ Redistributable'$\r$\n\
to function properly.$\r$\n$\r$\n\
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
InstallVCRedist:
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
Call checkVCRedist
${If} $0 == "1"
Goto ContinueInstall
${EndIf}
;InstallError:
MessageBox MB_ICONSTOP "\
There was an unexpected error installing$\r$\n\
Microsoft Visual C++ Redistributable.$\r$\n\
The installation of ${PRODUCT_NAME} cannot continue."
DontInstall:
Abort
${EndIf}
ContinueInstall:
Pop $0
!macroend

View File

@@ -1,6 +1,8 @@
# provider: generic
# url: http://127.0.0.1:8080
# updaterCacheDirName: cherry-studio-updater
provider: github
repo: cherry-studio
owner: kangfenmao
# provider: github
# repo: cherry-studio
# owner: kangfenmao
provider: generic
url: https://cherrystudio.ocool.online

View File

@@ -11,8 +11,25 @@ files:
- '!src'
- '!scripts'
- '!local'
- '!docs'
- '!packages'
- '!stats.html'
- '!*.md'
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
- '!**/{test,tests,__tests__,coverage}/**'
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
- '!**/*.min.*.map'
- '!**/*.d.ts'
- '!**/{.DS_Store,Thumbs.db}'
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer'
- '!node_modules/js-tiktoken'
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!node_modules/html2canvas/dist/{html2canvas.min.js,html2canvas.esm.js}'
asarUnpack:
- resources/**
- '**/*.{node,dll,metal,exp,lib}'
win:
executableName: Cherry Studio
nsis:
@@ -22,14 +39,15 @@ nsis:
createDesktopShortcut: always
allowToChangeInstallationDirectory: true
oneClick: false
include: build/nsis-installer.nsh
mac:
entitlementsInherit: build/entitlements.mac.plist
notarize: false
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
target:
- target: dmg
arch:
@@ -47,22 +65,17 @@ linux:
arch:
- arm64
- x64
# - snap
# - deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${productName}-${version}-${arch}.${ext}
npmRebuild: false
publish:
provider: github
repo: cherry-studio
owner: kangfenmao
provider: generic
url: https://cherrystudio.ocool.online
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
修复快捷键设置错误导致的无法启动问题
修复翻译按钮无法正常输出内容问题
修复检测更新按钮逻辑错误
增加 Genspark 小程序

View File

@@ -1,23 +1,48 @@
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
plugins: [
externalizeDepsPlugin({
exclude: [
'@llm-tools/embedjs',
'@llm-tools/embedjs-openai',
'@llm-tools/embedjs-loader-web',
'@llm-tools/embedjs-loader-markdown',
'@llm-tools/embedjs-loader-msoffice',
'@llm-tools/embedjs-loader-xml',
'@llm-tools/embedjs-loader-pdf',
'@llm-tools/embedjs-loader-sitemap',
'@llm-tools/embedjs-libsql'
]
}),
...visualizerPlugin('main')
],
resolve: {
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
'@shared': resolve('packages/shared')
}
},
build: {
rollupOptions: {
external: ['@libsql/client']
}
}
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
plugins: [react()],
plugins: [react(), ...visualizerPlugin('renderer')],
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
@@ -25,7 +50,7 @@ export default defineConfig({
}
},
optimizeDeps: {
exclude: []
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js']
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.8.23",
"version": "0.9.2",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -11,9 +11,11 @@
"local",
"packages/*"
],
"nohoist": [
"packages/database"
]
"installConfig": {
"hoistingLimits": [
"packages/database"
]
}
},
"scripts": {
"format": "prettier --write .",
@@ -23,22 +25,44 @@
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build:check": "yarn typecheck",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "dotenv npm run build && electron-builder --dir",
"build:win": "dotenv npm run build && electron-builder --win --publish never",
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
"build:win": "dotenv npm run build && electron-builder --win",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
"build:mac": "dotenv electron-vite build && electron-builder --mac",
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
"build:linux": "dotenv electron-vite build && electron-builder --linux",
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
"build:npm": "node scripts/build-npm.js",
"release": "node scripts/version.js",
"publish": "yarn release patch push",
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
"generate:agents": "yarn workspace @cherry-studio/database agents",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build"
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
"@llm-tools/embedjs-loader-csv": "^0.1.25",
"@llm-tools/embedjs-loader-markdown": "^0.1.25",
"@llm-tools/embedjs-loader-msoffice": "^0.1.25",
"@llm-tools/embedjs-loader-pdf": "^0.1.25",
"@llm-tools/embedjs-loader-sitemap": "^0.1.25",
"@llm-tools/embedjs-loader-web": "^0.1.25",
"@llm-tools/embedjs-loader-xml": "^0.1.25",
"@llm-tools/embedjs-openai": "^0.1.25",
"@types/react-infinite-scroll-component": "^5.0.0",
"adm-zip": "^0.5.16",
"apache-arrow": "^18.1.0",
"docx": "^9.0.2",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
@@ -48,6 +72,7 @@
"html2canvas": "^1.4.1",
"markdown-it": "^14.1.0",
"officeparser": "^4.1.1",
"tokenx": "^0.4.1",
"webdav": "4.11.4"
},
"devDependencies": {
@@ -55,7 +80,7 @@
"@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",
"@google/generative-ai": "^0.21.0",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
@@ -66,20 +91,21 @@
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.18.3",
"axios": "^1.7.3",
"antd": "^5.22.5",
"axios": "^1.7.9",
"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.3.3",
"electron-builder": "^24.9.1",
"electron": "31.7.6",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.0.0",
"electron-vite": "^2.3.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"eslint": "^8.56.0",
@@ -87,16 +113,16 @@
"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.10",
"i18next": "^23.11.5",
"lodash": "^4.17.21",
"mime": "^4.0.4",
"openai": "patch:openai@npm%3A4.71.1#~/.yarn/patches/openai-npm-4.71.1-b5940d6401.patch",
"openai": "patch:openai@npm%3A4.76.2#~/.yarn/patches/openai-npm-4.76.2-8ff1374617.patch",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router": "6",
@@ -109,6 +135,7 @@
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"shiki": "^1.22.2",
"styled-components": "^6.1.11",
@@ -122,7 +149,8 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"resolutions": {
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@llm-tools/embedjs-utils@npm:0.1.25": "patch:@llm-tools/embedjs-utils@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-utils-npm-0.1.25-fd8fe8a193.patch"
},
"packageManager": "yarn@4.5.0"
}

View File

@@ -95,18 +95,21 @@ export const ZOOM_SHORTCUTS = [
key: 'zoom_in',
shortcut: ['CommandOrControl', '='],
editable: false,
enabled: true
enabled: true,
system: true
},
{
key: 'zoom_out',
shortcut: ['CommandOrControl', '-'],
editable: false,
enabled: true
enabled: true,
system: true
},
{
key: 'zoom_reset',
shortcut: ['CommandOrControl', '0'],
editable: false,
enabled: true
enabled: true,
system: true
}
]

View File

@@ -0,0 +1,202 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Github Releases Timeline</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
</head>
<body id="app">
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
<div class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<!-- Loading状态 -->
<div v-if="loading" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
</div>
<!-- Error 状态 -->
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
<!-- Release 列表 -->
<div v-else class="space-y-8">
<div v-for="release in releases" :key="release.id" class="relative pl-8"
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
<div class="rounded-lg shadow-sm p-6 transition-shadow"
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
{{ release.name || release.tag_name }}
</h2>
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
{{ formatDate(release.published_at) }}
</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
{{ release.tag_name }}
</span>
</div>
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
v-html="renderMarkdown(release.body)"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const md = window.markdownit({
breaks: true,
linkify: true
})
const { createApp } = Vue
createApp({
data() {
return {
releases: [],
loading: true,
error: null,
isDark: false
}
},
methods: {
async fetchReleases() {
try {
this.loading = true
this.error = null
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
if (!response.ok) {
throw new Error('Failed to fetch releases')
}
this.releases = await response.json()
} catch (err) {
this.error = 'Error loading releases: ' + err.message
} finally {
this.loading = false
}
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
},
renderMarkdown(content) {
if (!content) return ''
return md.render(content)
},
initTheme() {
// 从 URL 参数获取主题设置
const url = new URL(window.location.href)
const theme = url.searchParams.get('theme')
this.isDark = theme === 'dark'
}
},
mounted() {
this.initTheme()
this.fetchReleases()
}
}).mount('#app')
</script>
<style>
/* 基础的 Markdown 样式 */
.prose {
line-height: 1.6;
}
.prose h1 {
font-size: 1.5em;
margin: 1em 0;
}
.prose h2 {
font-size: 1.3em;
margin: 0.8em 0;
}
.prose h3 {
font-size: 1.1em;
margin: 0.6em 0;
}
.prose ul {
list-style-type: disc;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose ol {
list-style-type: decimal;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose code {
padding: 0.2em 0.4em;
border-radius: 0.2em;
font-size: 0.9em;
}
.dark .prose code {
background-color: #1f2937;
}
.prose code {
background-color: #f3f4f6;
}
.prose pre code {
display: block;
padding: 1em;
overflow-x: auto;
}
.prose a {
color: #3b82f6;
text-decoration: underline;
}
.dark .prose a {
color: #60a5fa;
}
.prose blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1em;
margin: 1em 0;
}
.dark .prose blockquote {
border-left-color: #374151;
color: #9ca3af;
}
.dark .prose {
color: #e5e7eb;
}
.dark-bg {
background-color: #151515;
}
.bg {
background-color: #f2f2f2;
}
</style>
</body>
</html>

45
scripts/after-pack.js Normal file
View File

@@ -0,0 +1,45 @@
const { Arch } = require('electron-builder')
const { default: removeLocales } = require('./remove-locales')
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
await removeLocales(context)
const platform = context.packager.platform.name
const arch = context.arch
if (platform === 'mac') {
const node_modules_path = path.join(
context.appOutDir,
'Cherry Studio.app',
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules'
)
removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
}
if (platform === 'linux') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
}
if (platform === 'windows') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
}
}
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {
const modulePath = path.join(nodeModulesPath, packageName)
const dirs = fs.readdirSync(modulePath)
dirs
.filter((dir) => !arch.includes(dir))
.forEach((dir) => {
fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true })
console.log(`Removed dir: ${dir}`, arch)
})
}

40
scripts/build-npm.js Normal file
View File

@@ -0,0 +1,40 @@
const { downloadNpmPackage } = require('./utils')
async function downloadNpm(platform) {
if (!platform || platform === 'mac') {
downloadNpmPackage(
'@libsql/darwin-arm64',
'https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz'
)
downloadNpmPackage('@libsql/darwin-x64', 'https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz')
}
if (!platform || platform === 'linux') {
downloadNpmPackage(
'@libsql/linux-arm64-gnu',
'https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz'
)
downloadNpmPackage(
'@libsql/linux-arm64-musl',
'https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz'
)
downloadNpmPackage(
'@libsql/linux-x64-gnu',
'https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz'
)
downloadNpmPackage(
'@libsql/linux-x64-musl',
'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz'
)
}
if (!platform || platform === 'windows') {
downloadNpmPackage(
'@libsql/win32-x64-msvc',
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
)
}
}
const platformArg = process.argv[2]
downloadNpm(platformArg)

File diff suppressed because it is too large Load Diff

58
scripts/remove-locales.js Normal file
View File

@@ -0,0 +1,58 @@
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
const platform = context.packager.platform.name
// 根据平台确定 locales 目录位置
let resourceDirs = []
if (platform === 'mac') {
// macOS 的语言文件位置
resourceDirs = [
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
path.join(
context.appOutDir,
'Cherry Studio.app',
'Contents',
'Frameworks',
'Electron Framework.framework',
'Resources'
)
]
} else {
// Windows 和 Linux 的语言文件位置
resourceDirs = [path.join(context.appOutDir, 'locales')]
}
// 处理每个资源目录
for (const resourceDir of resourceDirs) {
if (!fs.existsSync(resourceDir)) {
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
continue
}
// 读取所有文件和目录
const items = fs.readdirSync(resourceDir)
// 遍历并删除不需要的语言文件
for (const item of items) {
if (platform === 'mac') {
// 在 macOS 上检查 .lproj 目录
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
const dirPath = path.join(resourceDir, item)
fs.rmSync(dirPath, { recursive: true, force: true })
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
}
} else {
// 其他平台处理 .pak 文件
if (!item.match(/^(en|zh|ru)/)) {
const filePath = path.join(resourceDir, item)
fs.unlinkSync(filePath)
console.log(`Removed locale file: ${item} from ${resourceDir}`)
}
}
}
}
console.log('Locale cleanup completed!')
}

58
scripts/replace-spaces.js Normal file
View File

@@ -0,0 +1,58 @@
// replaceSpaces.js
const fs = require('fs')
const path = require('path')
const directory = 'dist'
// 处理文件名中的空格
function replaceFileNames() {
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
const oldPath = path.join(directory, file)
const newPath = path.join(directory, file.replace(/ /g, '-'))
fs.stat(oldPath, (err, stats) => {
if (err) throw err
if (stats.isFile() && oldPath !== newPath) {
fs.rename(oldPath, newPath, (err) => {
if (err) throw err
console.log(`Renamed: ${oldPath} -> ${newPath}`)
})
}
})
})
})
}
function replaceYmlContent() {
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
if (path.extname(file).toLowerCase() === '.yml') {
const filePath = path.join(directory, file)
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) throw err
// 替换内容
const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-')
// 写回文件
fs.writeFile(filePath, newContent, 'utf8', (err) => {
if (err) throw err
console.log(`Updated content in: ${filePath}`)
})
})
}
})
})
}
// 执行两个操作
replaceFileNames()
replaceYmlContent()

View File

@@ -1,26 +0,0 @@
// replaceSpaces.js
const fs = require('fs')
const path = require('path')
const directory = 'dist'
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
const oldPath = path.join(directory, file)
const newPath = path.join(directory, file.replace(/ /g, '-'))
fs.stat(oldPath, (err, stats) => {
if (err) throw err
if (stats.isFile() && oldPath !== newPath) {
fs.rename(oldPath, newPath, (err) => {
if (err) throw err
console.log(`Renamed: ${oldPath} -> ${newPath}`)
})
}
})
})
})

39
scripts/utils.js Normal file
View File

@@ -0,0 +1,39 @@
const fs = require('fs')
const path = require('path')
const os = require('os')
function downloadNpmPackage(packageName, url) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-download-'))
const targetDir = path.join('./node_modules/', packageName)
const filename = packageName.replace('/', '-') + '.tgz'
// Skip if directory already exists
if (fs.existsSync(targetDir)) {
console.log(`${targetDir} already exists, skipping download...`)
return
}
try {
console.log(`Downloading ${packageName}...`, url)
const { execSync } = require('child_process')
execSync(`curl --fail -o ${filename} ${url}`)
console.log(`Extracting ${filename}...`)
execSync(`tar -xvf ${filename}`)
execSync(`rm -rf ${filename}`)
execSync(`mv package ${targetDir}`)
} catch (error) {
console.error(`Error processing ${packageName}: ${error.message}`)
if (fs.existsSync(filename)) {
fs.unlinkSync(filename)
}
throw error
}
fs.rmSync(tempDir, { recursive: true, force: true })
}
module.exports = {
downloadNpmPackage
}

View File

@@ -1,7 +1,9 @@
import fs from 'node:fs'
import path from 'node:path'
import vm from 'node:vm'
import { Shortcut, ThemeMode } from '@types'
import axios from 'axios'
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
import log from 'electron-log'
@@ -11,6 +13,7 @@ import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager'
import { ExportService } from './services/ExportService'
import FileStorage from './services/FileStorage'
import KnowledgeService from './services/KnowledgeService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { windowService } from './services/WindowService'
import { compress, decompress } from './utils/zip'
@@ -100,6 +103,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// file
ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:openPath', fileManager.openPath)
ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile)
ipcMain.handle('file:upload', fileManager.uploadFile)
@@ -144,4 +148,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
registerShortcuts(mainWindow)
}
})
// knowledge base
ipcMain.handle('knowledge-base:create', KnowledgeService.create)
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset)
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete)
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
// vm
ipcMain.handle('run-js', (_, code: string) => {
const context = vm.createContext(Object.assign({ fetch: fetch, URL: URL, axios: axios }, global))
return vm.runInContext(code, context)
})
}

View File

@@ -2,6 +2,8 @@ import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
import icon from '../../../build/icon.png?asset'
export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater
@@ -43,6 +45,7 @@ export default class AppUpdater {
.showMessageBox({
type: 'info',
title: '安装更新',
icon,
message: `新版本 ${releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'],

View File

@@ -77,7 +77,10 @@ export class ConfigManager {
}
setShortcuts(shortcuts: Shortcut[]) {
this.store.set('shortcuts', shortcuts)
this.store.set(
'shortcuts',
shortcuts.filter((shortcut) => shortcut.system)
)
this.notifySubscribers('shortcuts', shortcuts)
}
}

View File

@@ -8,7 +8,8 @@ import {
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue
SaveDialogReturnValue,
shell
} from 'electron'
import logger from 'electron-log'
import * as fs from 'fs'
@@ -298,6 +299,10 @@ class FileStorage {
}
}
public openPath = async (_: Electron.IpcMainInvokeEvent, path: string): Promise<void> => {
shell.openPath(path).catch((err) => logger.error('[IPC - Error] Failed to open file:', err))
}
public save = async (
_: Electron.IpcMainInvokeEvent,
fileName: string,

View File

@@ -0,0 +1,154 @@
import * as fs from 'node:fs'
import path from 'node:path'
import { LocalPathLoader, RAGApplication, RAGApplicationBuilder, TextLoader } from '@llm-tools/embedjs'
import type { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
import { MarkdownLoader } from '@llm-tools/embedjs-loader-markdown'
import { DocxLoader, ExcelLoader, PptLoader } from '@llm-tools/embedjs-loader-msoffice'
import { PdfLoader } from '@llm-tools/embedjs-loader-pdf'
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { getInstanceName } from '@main/utils'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron'
class KnowledgeService {
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
constructor() {
this.initStorageDir()
}
private initStorageDir = (): void => {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
}
private getRagApplication = async ({
id,
model,
apiKey,
apiVersion,
baseURL,
dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => {
return new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(
apiVersion
? new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions,
batchSize: 15
})
: new OpenAiEmbeddings({
model,
apiKey,
configuration: { baseURL },
dimensions,
batchSize: 15
})
)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build()
}
public create = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
this.getRagApplication(base)
}
public reset = async (_: Electron.IpcMainInvokeEvent, { base }: { base: KnowledgeBaseParams }): Promise<void> => {
const ragApplication = await this.getRagApplication(base)
await ragApplication.reset()
}
public delete = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
const dbPath = path.join(this.storageDir, id)
if (fs.existsSync(dbPath)) {
fs.rmSync(dbPath, { recursive: true })
}
}
public add = async (
_: Electron.IpcMainInvokeEvent,
{ base, item, forceReload = false }: { base: KnowledgeBaseParams; item: KnowledgeItem; forceReload: boolean }
): Promise<AddLoaderReturn> => {
const ragApplication = await this.getRagApplication(base)
if (item.type === 'directory') {
const directory = item.content as string
return await ragApplication.addLoader(new LocalPathLoader({ path: directory }), forceReload)
}
if (item.type === 'url') {
const content = item.content as string
if (content.startsWith('http')) {
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload)
}
}
if (item.type === 'sitemap') {
const content = item.content as string
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload)
}
if (item.type === 'note') {
const content = item.content as string
return await ragApplication.addLoader(new TextLoader({ text: content }), forceReload)
}
if (item.type === 'file') {
const file = item.content as FileType
if (file.ext === '.pdf') {
return await ragApplication.addLoader(new PdfLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (file.ext === '.docx') {
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (file.ext === '.pptx') {
return await ragApplication.addLoader(new PptLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (file.ext === '.xlsx') {
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (['.md', '.mdx'].includes(file.ext)) {
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
const fileContent = fs.readFileSync(file.path, 'utf-8')
return await ragApplication.addLoader(new TextLoader({ text: fileContent }), forceReload)
}
return { entriesAdded: 0, uniqueId: '', loaderType: '' }
}
public remove = async (
_: Electron.IpcMainInvokeEvent,
{ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }
): Promise<void> => {
const ragApplication = await this.getRagApplication(base)
await ragApplication.deleteLoader(uniqueId)
}
public search = async (
_: Electron.IpcMainInvokeEvent,
{ search, base }: { search: string; base: KnowledgeBaseParams }
): Promise<ExtractChunkData[]> => {
const ragApplication = await this.getRagApplication(base)
return await ragApplication.search(search)
}
}
export default new KnowledgeService()

View File

@@ -1,8 +1,9 @@
import { is } from '@electron-toolkit/utils'
import { isLinux, isWin } from '@main/constant'
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import path, { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
@@ -32,6 +33,7 @@ export class WindowService {
const theme = configManager.getTheme()
const isMac = process.platform === 'darwin'
const isLinux = process.platform === 'linux'
this.mainWindow = new BrowserWindow({
x: mainWindowState.x,
@@ -45,7 +47,7 @@ export class WindowService {
transparent: isMac,
vibrancy: 'under-window',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarStyle: isLinux ? 'default' : 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
trafficLightPosition: { x: 8, y: 12 },
@@ -123,12 +125,26 @@ export class WindowService {
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
mainWindow.webContents.on('will-navigate', (event, url) => {
if (url.includes('localhost:5173')) {
return
}
event.preventDefault()
shell.openExternal(url)
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
const { url } = details
if (url.includes('http://file/')) {
const fileName = url.replace('http://file/', '')
const storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
const filePath = storageDir + '/' + fileName
shell.openPath(filePath).catch((err) => Logger.error('Failed to open file:', err))
} else {
shell.openExternal(details.url)
}
return { action: 'deny' }
})

View File

@@ -14,3 +14,11 @@ export function getDataPath() {
}
return dataPath
}
export function getInstanceName(baseURL: string) {
try {
return new URL(baseURL).host.split('.')[0]
} catch (error) {
return ''
}
}

View File

@@ -1,4 +1,5 @@
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
import JaJP from '../../renderer/src/i18n/locales/ja-jp.json'
import RuRu from '../../renderer/src/i18n/locales/ru-ru.json'
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
@@ -7,6 +8,7 @@ const locales = {
'en-US': EnUs,
'zh-CN': ZhCn,
'zh-TW': ZhTw,
'ja-JP': JaJP,
'ru-RU': RuRu
}

View File

@@ -1,8 +1,10 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types'
import { AppInfo, LanguageVarious } from '@renderer/types'
import { AppInfo, KnowledgeBaseParams, KnowledgeItem, LanguageVarious } from '@renderer/types'
import type { OpenDialogOptions } from 'electron'
import type { UpdateInfo } from 'electron-updater'
import { Readable } from 'stream'
declare global {
@@ -10,7 +12,7 @@ declare global {
electron: ElectronAPI
api: {
getAppInfo: () => Promise<AppInfo>
checkForUpdate: () => void
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void
@@ -40,6 +42,7 @@ declare global {
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
openPath: (path: string) => Promise<void>
save: (
path: string,
content: string | NodeJS.ArrayBufferView,
@@ -57,6 +60,25 @@ declare global {
shortcuts: {
update: (shortcuts: Shortcut[]) => Promise<void>
}
knowledgeBase: {
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) => Promise<void>
reset: ({ base }: { base: KnowledgeBaseParams }) => Promise<void>
delete: (id: string) => Promise<void>
add: ({
base,
item,
forceReload = false
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}) => Promise<AddLoaderReturn>
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
}
vm: {
run: (code: string) => Promise<any>
}
}
}
}

View File

@@ -1,5 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload'
import { Shortcut, WebDavConfig } from '@types'
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer
@@ -36,6 +36,7 @@ const api = {
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
openPath: (path: string) => ipcRenderer.invoke('file:openPath', path),
save: (path: string, content: string, options?: { compress: boolean }) =>
ipcRenderer.invoke('file:save', path, content, options),
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
@@ -50,6 +51,28 @@ const api = {
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
shortcuts: {
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
},
knowledgeBase: {
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) =>
ipcRenderer.invoke('knowledge-base:create', { id, model, apiKey, baseURL }),
reset: ({ base }: { base: KnowledgeBaseParams }) => ipcRenderer.invoke('knowledge-base:reset', { base }),
delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id),
add: ({
base,
item,
forceReload = false
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}) => ipcRenderer.invoke('knowledge-base:add', { base, item, forceReload }),
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base })
},
vm: {
run: (code: string) => ipcRenderer.invoke('run-js', code)
}
}

View File

@@ -14,6 +14,7 @@ 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 KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
@@ -30,10 +31,11 @@ function App(): JSX.Element {
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
<g>
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
</g>
<g>
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
</g>
<g>
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.39 115.44">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M25.31,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M40.64,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04c3.17-3.23,7.36-5.01,11.81-5.01s8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M40.64,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M10.19,90.22l.39-.28c.49-.29.83-.43,1.03-.43.45,0,.93.4,1.44,1.21.32.5.47.9.47,1.21s-.1.55-.29.75c-.19.2-.42.38-.68.54-.26.16-.51.31-.74.45-.24.14-.72.33-1.45.56-.73.23-1.44.34-2.12.34s-1.37-.09-2.07-.27c-.7-.18-1.41-.48-2.15-.9-.74-.42-1.4-.94-1.99-1.55-.59-.61-1.07-1.39-1.45-2.35-.38-.96-.57-1.99-.57-3.11s.19-2.14.56-3.05c.37-.91.85-1.67,1.43-2.26.58-.6,1.25-1.09,1.99-1.5,1.41-.78,2.82-1.16,4.24-1.16.67,0,1.36.1,2.06.31.7.21,1.22.42,1.58.64l.52.3c.26.16.46.29.6.39.37.3.56.64.56,1.02s-.15.78-.45,1.2c-.56.78-1.06,1.16-1.51,1.16-.26,0-.62-.16-1.1-.47-.6-.49-1.41-.73-2.41-.73-.93,0-1.85.32-2.76.97-.43.32-.79.76-1.08,1.34-.29.57-.43,1.22-.43,1.95s.14,1.37.43,1.95c.29.57.65,1.03,1.1,1.36.88.63,1.79.95,2.74.95.45,0,.87-.06,1.26-.17.39-.11.68-.23.85-.34Z"/>
<path class="cls-3" d="M24.7,79.2c.11-.22.31-.37.58-.45.27-.09.62-.13,1.03-.13s.75.04.99.11c.24.07.43.16.56.26.13.1.23.24.3.43.07.24.11.62.11,1.12v11.95c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-4.37h-5.71v4.39c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v4.39h5.71v-4.42c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57Z"/>
<path class="cls-3" d="M33.82,90.58h6.63c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-8.53c-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98c0-.73.14-1.23.41-1.5.27-.27.79-.4,1.55-.4h8.49c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-6.61v2.18h4.26c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.82,1.77-.24.09-.62.13-1.12.13h-4.22v2.18Z"/>
<path class="cls-3" d="M83.34,79c.7.49,1.06.96,1.06,1.42,0,.27-.17.65-.5,1.14l-4.65,6.96v4.11c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.11.22-.31.37-.58.45-.27.09-.64.13-1.1.13s-.83-.04-1.1-.13c-.27-.09-.47-.24-.58-.46-.11-.22-.18-.42-.2-.58-.02-.17-.03-.42-.03-.76v-4.07l-4.65-6.96c-.33-.49-.5-.87-.5-1.14,0-.46.32-.9.95-1.32.63-.42,1.08-.64,1.36-.64s.49.06.65.17c.24.16.5.45.78.88l3.34,5.34,3.34-5.34c.27-.43.51-.71.71-.85s.43-.2.7-.2.69.18,1.26.54Z"/>
<g>
<path class="cls-3" d="M1.66,112.96c-.37-.46-.56-.87-.56-1.24s.31-.85.93-1.45c.36-.34.74-.52,1.14-.52s.96.36,1.68,1.08c.2.24.49.48.86.7.37.22.72.33,1.03.33,1.34,0,2-.55,2-1.64,0-.33-.18-.61-.55-.83-.37-.22-.82-.38-1.37-.48-.55-.1-1.13-.26-1.77-.48-.63-.22-1.22-.48-1.77-.79-.55-.3-1-.78-1.37-1.43-.37-.65-.55-1.44-.55-2.36,0-1.26.47-2.37,1.41-3.31s2.22-1.41,3.84-1.41c.86,0,1.65.11,2.36.33.71.22,1.2.45,1.48.68l.54.41c.45.42.67.77.67,1.06s-.17.68-.52,1.18c-.49.72-.99,1.08-1.51,1.08-.3,0-.67-.14-1.12-.43-.04-.03-.13-.1-.25-.22-.12-.11-.23-.21-.33-.28-.3-.19-.69-.28-1.15-.28s-.85.11-1.16.33c-.31.22-.46.53-.46.93s.18.71.55.96c.37.24.82.41,1.37.5.55.09,1.14.22,1.79.4.65.18,1.24.4,1.79.66.55.26,1,.71,1.37,1.35.37.64.55,1.42.55,2.36s-.19,1.76-.56,2.47c-.37.71-.86,1.26-1.46,1.65-1.16.76-2.4,1.14-3.73,1.14-.68,0-1.31-.08-1.92-.25-.6-.17-1.09-.37-1.46-.61-.76-.46-1.29-.9-1.59-1.34l-.19-.24Z"/>
<path class="cls-3" d="M15.02,99.37h11.98c.46,0,.8.05,1.01.16.22.11.36.28.43.51.07.23.11.53.11.9s-.04.67-.11.89c-.07.22-.19.38-.37.46-.26.13-.62.19-1.1.19h-4.11v10.83c0,.33-.01.57-.03.73s-.09.34-.19.55c-.11.21-.3.36-.57.44-.27.09-.63.13-1.08.13s-.8-.04-1.07-.13c-.27-.09-.45-.23-.56-.44-.11-.21-.17-.4-.19-.56-.02-.17-.03-.41-.03-.74v-10.81h-4.14c-.46,0-.8-.05-1.01-.16-.22-.11-.36-.28-.43-.51-.07-.23-.11-.53-.11-.9s.04-.67.11-.89c.07-.22.19-.38.37-.46.26-.13.62-.19,1.1-.19Z"/>
<path class="cls-3" d="M40.05,99.98c.14-.23.35-.39.62-.47.27-.09.61-.13,1.02-.13s.74.04.98.11c.24.07.43.16.56.26.13.1.22.25.28.45.09.24.13.62.13,1.12v6.5c0,1.9-.59,3.62-1.77,5.17-.57.73-1.31,1.32-2.22,1.78s-1.91.68-3,.68-2.1-.23-2.99-.69c-.9-.46-1.63-1.06-2.19-1.81-1.16-1.52-1.74-3.25-1.74-5.17v-6.48c0-.34.01-.6.03-.76.02-.17.09-.36.2-.57.11-.22.31-.37.58-.45.27-.09.64-.13,1.1-.13s.83.04,1.1.13c.27.09.46.24.56.45.17.33.26.78.26,1.36v6.46c0,.88.22,1.71.65,2.5.22.4.54.72.97.97.43.24.94.37,1.53.37,1.05,0,1.83-.39,2.35-1.16.52-.78.78-1.67.78-2.69v-6.59c0-.56.07-.95.22-1.18Z"/>
<path class="cls-3" d="M47.28,99.37l3.98.02c2.08,0,3.91.75,5.49,2.25,1.58,1.5,2.37,3.35,2.37,5.54s-.77,4.07-2.32,5.63c-1.54,1.57-3.41,2.35-5.61,2.35h-3.94c-.88,0-1.42-.18-1.64-.54-.17-.3-.26-.76-.26-1.38v-11.98c0-.34.01-.6.03-.75s.09-.34.2-.56c.2-.39.76-.58,1.68-.58ZM51.27,111.35c1.03,0,1.97-.38,2.8-1.15.83-.77,1.25-1.73,1.25-2.9s-.41-2.14-1.22-2.92c-.81-.78-1.76-1.17-2.85-1.17h-2.07v8.14h2.09Z"/>
<path class="cls-3" d="M60.53,101.29c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v11.98c0,.34-.01.6-.03.75s-.09.34-.2.56c-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98Z"/>
<path class="cls-3" d="M73.47,99.2c2.13,0,3.97.77,5.54,2.3,1.57,1.54,2.35,3.44,2.35,5.72s-.75,4.21-2.24,5.82c-1.49,1.6-3.33,2.4-5.51,2.4s-4.04-.79-5.57-2.37c-1.53-1.58-2.29-3.46-2.29-5.64,0-1.19.22-2.31.65-3.35.43-1.04,1.01-1.91,1.72-2.62.72-.7,1.54-1.26,2.48-1.66.93-.4,1.9-.6,2.89-.6ZM69.55,107.32c0,1.28.41,2.32,1.24,3.11.83.8,1.75,1.2,2.77,1.2s1.94-.39,2.76-1.16c.82-.78,1.23-1.82,1.23-3.12s-.41-2.35-1.24-3.14c-.83-.79-1.75-1.18-2.77-1.18s-1.94.4-2.76,1.2c-.82.8-1.23,1.83-1.23,3.11Z"/>
</g>
<g>
<path class="cls-3" d="M69.11,80.61c-.02-.17-.09-.36-.2-.57-.2-.39-.76-.58-1.68-.58h-4.65c-1.26,0-2.49.46-3.68,1.38-.57.45-1.05,1.05-1.42,1.81-.37.76-.56,1.61-.56,2.54,0,1.62.54,2.96,1.62,4.01-.32.76-.8,1.89-1.46,3.38-.22.52-.32.89-.32,1.12,0,.55.45,1.01,1.34,1.38.46.2.83.3,1.11.3s.51-.07.69-.2c.18-.14.31-.28.4-.42.14-.27.7-1.57,1.68-3.9l.67.04h2.71v2.43c0,.33.01.58.03.74.02.17.09.36.2.57.2.39.76.58,1.68.58,1.01,0,1.59-.27,1.77-.8.09-.24.13-.62.13-1.12v-11.95c0-.33-.01-.58-.03-.74ZM66.19,86.56c-.06.1-.34.54-.88.65-.14.03-.26.02-.34.02-.01.1-.03.19-.04.27-.11.59-.27.74-.4.76-.21.03-.36-.25-.66-.64-.16,0-.32.01-.49.01-.36,0-.72-.02-1.06-.06-.18-.02-.35-.05-.52-.08-.84-.22-1.44-.9-1.44-1.7v-.97c0-.8.61-1.48,1.44-1.7.17-.03.34-.06.52-.08.34-.04.69-.06,1.06-.06s.72.02,1.06.06c.18.02.35.05.52.08.43.12.8.35,1.06.66.24.29.39.65.39,1.05v.97c0,.16,0,.47-.21.78Z"/>
<circle class="cls-2" cx="62.23" cy="85.3" r=".35"/>
<circle class="cls-2" cx="63.4" cy="85.33" r=".35"/>
<circle class="cls-2" cx="64.61" cy="85.33" r=".35"/>
</g>
<g>
<path class="cls-3" d="M43.62,78.61c.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58h4.65c1.26,0,2.49.46,3.68,1.38.57.45,1.05,1.05,1.42,1.81.37.76.56,1.61.56,2.54,0,1.62-.54,2.96-1.62,4.01.32.76.8,1.89,1.46,3.38.22.52.32.89.32,1.12,0,.55-.45,1.01-1.34,1.38-.46.2-.83.3-1.11.3s-.51-.07-.69-.2c-.18-.14-.31-.28-.4-.42-.14-.27-.7-1.57-1.68-3.9l-.67.04h-2.71v2.43c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74ZM46.53,84.56c.06.1.34.54.88.65.14.03.26.02.34.02.01.1.03.19.04.27.11.59.27.74.4.76.21.03.36-.25.66-.64.16,0,.32.01.49.01.36,0,.72-.02,1.06-.06.18-.02.35-.05.52-.08.84-.22,1.44-.9,1.44-1.7v-.97c0-.8-.61-1.48-1.44-1.7-.17-.03-.34-.06-.52-.08-.34-.04-.69-.06-1.06-.06s-.72.02-1.06.06c-.18.02-.35.05-.52.08-.43.12-.8.35-1.06.66-.24.29-.39.65-.39,1.05v.97c0,.16,0,.47.21.78Z"/>
<circle class="cls-2" cx="50.49" cy="83.3" r=".35"/>
<circle class="cls-2" cx="49.32" cy="83.33" r=".35"/>
<circle class="cls-2" cx="48.11" cy="83.33" r=".35"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -42,6 +42,7 @@
--color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333;
--color-group-background: var(--color-background-soft);
--color-reference-background: #0b0e12;
--navbar-background-mac: rgba(30, 30, 30, 0.6);
--navbar-background: rgba(30, 30, 30);
@@ -99,6 +100,7 @@ body[theme-mode='light'] {
--color-active: var(--color-white-soft);
--color-frame-border: #ddd;
--color-group-background: var(--color-white);
--color-reference-background: #f1f7ff;
--navbar-background-mac: rgba(255, 255, 255, 0.6);
--navbar-background: rgba(255, 255, 255);
@@ -169,18 +171,10 @@ body,
border-top: 0.5px solid var(--color-border);
}
body[os='mac'] {
#content-container {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
}
}
body[os='windows'] {
#app-sidebar {
border-right: 0.5px solid var(--color-border);
}
#content-container {
border-top-left-radius: 12px;
border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08);
}
.loader {

View File

@@ -66,6 +66,10 @@
}
}
ul {
list-style: initial;
}
ul,
ol {
padding-left: 1.5em;
@@ -225,11 +229,24 @@
.footnotes {
margin-top: 1em;
margin-bottom: 1em;
padding-top: 1em;
border-top: 1px solid var(--color-border);
background-color: var(--color-reference-background);
border-radius: 8px;
padding: 8px 12px;
h4 {
margin-bottom: 5px;
font-size: 12px;
}
ol {
padding-left: 1em;
margin: 0;
li:last-child {
margin-bottom: 0;
}
}
li {

View File

@@ -14,8 +14,8 @@ body[theme-mode='light'] {
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 5px;
height: 5px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {

View File

@@ -1,210 +0,0 @@
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { SettingRow } from '@renderer/pages/settings'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Col, Divider, Row, Slider, Switch, Tooltip } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ModelAvatar from '../Avatar/ModelAvatar'
import SelectModelPopup from '../Popups/SelectModelPopup'
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: Partial<AssistantSettings>) => void
}
const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
const { t } = useTranslation()
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
updateAssistantSettings({ temperature: value })
}
}
const onContextCountChange = (value) => {
if (!isNaN(value as number)) {
updateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
updateAssistantSettings({ maxTokens: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setContextCount(DEFAULT_CONTEXTCOUNT)
setEnableMaxTokens(false)
setMaxTokens(0)
setStreamOutput(true)
updateAssistantSettings({
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONTEXTCOUNT,
enableMaxTokens: false,
maxTokens: 0,
streamOutput: true
})
}
const onSelectModel = async () => {
const selectedModel = await SelectModelPopup.show({ model: assistant?.model })
if (selectedModel) {
setDefaultModel(selectedModel)
updateAssistant({
...assistant,
defaultModel: selectedModel
})
}
}
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }}>
<Label style={{ marginBottom: 10 }}>{t('assistants.settings.default_model')}</Label>
<Col span={24}>
<HStack alignItems="center">
<Button
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
onClick={onSelectModel}>
{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}
</Button>
</HStack>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<Label>
{t('assistants.settings.auto_reset_model')}{' '}
<Tooltip title={t('assistants.settings.auto_reset_model.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
<Switch
value={autoResetModel}
onChange={(checked) => {
setAutoResetModel(checked)
updateAssistantSettings({ autoResetModel: checked })
}}
/>
</SettingRow>
<Divider style={{ margin: '10px 0' }} />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>
{t('chat.settings.context_count')}{' '}
<Tooltip title={t('chat.settings.context_count.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={20}
onChange={setContextCount}
onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<Row align="middle" justify="space-between">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
updateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
disabled={!enableMaxTokens}
min={0}
max={32000}
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
</Row>
<SettingRow>
<Label>{t('model.stream_output')}</Label>
<Switch
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
updateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<Divider style={{ margin: '15px 0' }} />
<HStack justifyContent="flex-end">
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
{t('chat.settings.reset')}
</Button>
</HStack>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding: 5px;
`
const Label = styled.p`
margin-right: 5px;
font-weight: 500;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 12px;
cursor: pointer;
color: var(--color-text-3);
`
export default AssistantModelSettings

View File

@@ -0,0 +1,15 @@
import { GlobalOutlined } from '@ant-design/icons'
import React, { FC } from 'react'
import styled from 'styled-components'
const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <Icon {...(props as any)} />
}
const Icon = styled(GlobalOutlined)`
color: var(--color-link);
font-size: 12px;
margin-left: 4px;
`
export default WebSearchIcon

View File

@@ -0,0 +1,35 @@
// src/renderer/src/components/IndicatorLight.tsx
import React from 'react'
import styled from 'styled-components'
interface IndicatorLightProps {
color: string
}
const Light = styled.div<{ color: string }>`
width: 8px;
height: 8px;
border-radius: 50%;
background-color: ${({ color }) => color};
box-shadow: 0 0 6px ${({ color }) => color};
animation: pulse 2s infinite;
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
`
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color }) => {
const actualColor = color === 'green' ? '#22c55e' : color
return <Light color={actualColor} />
}
export default IndicatorLight

View File

@@ -0,0 +1,84 @@
import { ReactNode } from 'react'
import styled from 'styled-components'
interface ListItemProps {
active?: boolean
icon?: ReactNode
title: string
subtitle?: string
onClick?: () => void
}
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
const borderRadius = subtitle ? '10px' : '16px'
return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}>
<ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer>
<TitleText>{title}</TitleText>
{subtitle && <SubtitleText>{subtitle}</SubtitleText>}
</TextContainer>
</ListItemContent>
</ListItemContainer>
)
}
const ListItemContainer = styled.div`
padding: 7px 12px;
border-radius: 16px;
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
font-family: Ubuntu;
cursor: pointer;
border: 1px solid transparent;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
border: 1px solid var(--color-border-soft);
}
`
const ListItemContent = styled.div`
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
font-size: 13px;
`
const IconWrapper = styled.span`
margin-right: 8px;
`
const TextContainer = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
`
const TitleText = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const SubtitleText = styled.div`
font-size: 10px;
color: var(--color-text-soft);
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-text-3);
`
export default ListItem

View File

@@ -0,0 +1,65 @@
import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps'
import App from '@renderer/pages/apps/App'
import { Popover } from 'antd'
import { Empty } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components'
import Scrollbar from '../Scrollbar'
interface Props {
children: React.ReactNode
}
const AppStorePopover: FC<Props> = ({ children }) => {
const [open, setOpen] = useState(false)
const apps = getAllMinApps()
useHotkeys('esc', () => {
setOpen(false)
})
const handleClose = () => {
setOpen(false)
}
const content = (
<PopoverContent>
<AppsContainer>
{apps.map((app) => (
<App key={app.id} app={app} onClick={handleClose} size={50} />
))}
{isEmpty(apps) && (
<Center>
<Empty />
</Center>
)}
</AppsContainer>
</PopoverContent>
)
return (
<Popover
open={open}
onOpenChange={setOpen}
content={content}
trigger="click"
placement="bottomRight"
overlayInnerStyle={{ padding: 25 }}>
{children}
</Popover>
)
}
const PopoverContent = styled(Scrollbar)``
const AppsContainer = styled.div`
display: grid;
grid-template-columns: repeat(6, minmax(90px, 1fr));
gap: 25px;
`
export default AppStorePopover

View File

@@ -14,7 +14,7 @@ interface PromptPopupShowParams {
}
interface Props extends PromptPopupShowParams {
resolve: (value: string) => void
resolve: (value: any) => void
}
const PromptPopupContainer: React.FC<Props> = ({
@@ -30,18 +30,21 @@ const PromptPopupContainer: React.FC<Props> = ({
const onOk = () => {
setOpen(false)
resolve(value)
}
const handleCancel = () => {
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(value)
resolve(null)
}
PromptPopup.hide = onCancel
return (
<Modal title={title} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose} centered>
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered>
<Box mb={8}>{message}</Box>
<Input.TextArea
placeholder={inputPlaceholder}
@@ -57,10 +60,12 @@ const PromptPopupContainer: React.FC<Props> = ({
)
}
const TopViewKey = 'PromptPopup'
export default class PromptPopup {
static topviewId = 0
static hide() {
TopView.hide('PromptPopup')
TopView.hide(TopViewKey)
}
static show(props: PromptPopupShowParams) {
return new Promise<string>((resolve) => {
@@ -69,7 +74,7 @@ export default class PromptPopup {
{...props}
resolve={(v) => {
resolve(v)
this.hide()
TopView.hide(TopViewKey)
}}
/>,
'PromptPopup'

View File

@@ -1,17 +1,18 @@
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isVisionModel } from '@renderer/config/models'
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
import { first, reverse, sortBy } from 'lodash'
import { first, sortBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import WebSearchIcon from '../Icons/WebSearchIcon'
import { HStack } from '../Layout'
import Scrollbar from '../Scrollbar'
@@ -47,7 +48,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
await db.settings.put({ id: 'pinned:models', value: validPinnedModels })
}
setPinnedModels(validPinnedModels)
setPinnedModels(sortBy(validPinnedModels, ['group', 'name']))
}
loadPinnedModels()
}, [providers])
@@ -58,13 +59,14 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
: [...pinnedModels, modelId]
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
setPinnedModels(newPinnedModels)
setPinnedModels(sortBy(newPinnedModels, ['group', 'name']))
}
const filteredItems: MenuItem[] = providers
.filter((p) => p.models && p.models.length > 0)
.map((p) => {
const filteredModels = reverse(sortBy(p.models, 'name'))
const filteredModels = sortBy(p.models, ['group', 'name'])
.filter((m) => !isEmbeddingModel(m))
.filter((m) =>
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
)
@@ -73,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
label: (
<ModelItem>
<span>
{m?.name} {isVisionModel(m) && <VisionIcon />}
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />}
</span>
<PinIcon
onClick={(e) => {
@@ -113,7 +115,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
.flatMap((p) => p.models || [])
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
.map((m) => ({
key: getModelUniqId(m),
key: getModelUniqId(m) + '_pinned',
label: (
<ModelItem>
{m?.name} {isVisionModel(m) && <VisionIcon />}
@@ -141,7 +143,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
if (pinnedItems.length > 0) {
filteredItems.unshift({
key: 'pinned',
label: t('model.pinned'),
label: t('models.pinned'),
type: 'group',
children: pinnedItems
} as MenuItem)
@@ -187,7 +189,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
</SearchIcon>
}
ref={inputRef}
placeholder={t('model.search')}
placeholder={t('models.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear

View File

@@ -4,6 +4,7 @@ import { TextAreaProps } from 'antd/lib/input'
import { TextAreaRef } from 'antd/lib/input/TextArea'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../TopView'
@@ -11,13 +12,14 @@ interface ShowParams {
text: string
textareaProps?: TextAreaProps
modalProps?: ModalProps
children?: (props: { onOk?: () => void; onCancel?: () => void }) => React.ReactNode
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve }) => {
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve, children }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [textValue, setTextValue] = useState(text)
@@ -73,12 +75,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
onInput={resizeTextArea}
onChange={(e) => setTextValue(e.target.value)}
/>
<ChildrenContainer>{children && children({ onOk, onCancel })}</ChildrenContainer>
</Modal>
)
}
const TopViewKey = 'TextEditPopup'
const ChildrenContainer = styled.div`
position: relative;
`
export default class TextEditPopup {
static topviewId = 0
static hide() {

View File

@@ -1,4 +1,4 @@
import { FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
@@ -22,7 +22,7 @@ const Sidebar: FC = () => {
const { generating } = useRuntime()
const { t } = useTranslation()
const navigate = useNavigate()
const { windowStyle } = useSettings()
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
const { theme, toggleTheme } = useTheme()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
@@ -79,20 +79,31 @@ const Sidebar: FC = () => {
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}>
<FolderOutlined />
{showMinappIcon && (
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" />
</Icon>
</StyledLink>
</Tooltip>
)}
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/knowledge')}>
<Icon className={isRoute('/knowledge')}>
<FileSearchOutlined />
</Icon>
</StyledLink>
</Tooltip>
{showFilesIcon && (
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}>
<FolderOutlined />
</Icon>
</StyledLink>
</Tooltip>
)}
</Menus>
</MainMenus>
<Menus onClick={MinApp.onClose}>

View File

@@ -1,5 +1,3 @@
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
@@ -8,14 +6,18 @@ import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp'
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
@@ -119,19 +121,6 @@ const _apps: MinAppType[] = [
url: 'https://claude.ai/',
logo: ClaudeAppLogo
},
{
id: '360-ai-so',
name: '360AI搜索',
logo: AiSearchAppLogo,
url: 'https://so.360.com/'
},
{
id: '360-ai-bot',
name: 'AI 助手',
logo: AiAssistantAppLogo,
url: 'https://bot.360.com/',
bodered: true
},
{
id: 'baidu-ai-chat',
name: '文心一言',
@@ -210,6 +199,12 @@ const _apps: MinAppType[] = [
url: 'https://felo.ai/',
bodered: true
},
{
id: 'duckduckgo',
name: 'DuckDuckGo',
logo: DuckDuckGoAppLogo,
url: 'https://duck.ai'
},
{
id: 'bolt',
name: 'bolt',
@@ -218,10 +213,30 @@ const _apps: MinAppType[] = [
bodered: true
},
{
id: 'duckduckgo',
name: 'DuckDuckGo',
logo: DuckDuckGoAppLogo,
url: 'https://duck.ai'
id: 'nm',
name: '纳米AI搜索',
logo: NamiAiSearchLogo,
url: 'https://www.n.cn/',
bodered: true
},
{
id: 'thinkany',
name: 'ThinkAny',
logo: ThinkAnyLogo,
url: 'https://thinkany.ai/',
bodered: true
},
{
id: 'github-copilot',
name: 'GitHub Copilot',
logo: GithubCopilotLogo,
url: 'https://github.com/copilot'
},
{
id: 'genspark',
name: 'Genspark',
logo: GensparkLogo,
url: 'https://www.genspark.ai/'
}
]

View File

@@ -10,8 +10,8 @@ import AisingaporeModelLogo from '@renderer/assets/images/models/aisingapore.png
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.png'
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.png'
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.webp'
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.webp'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
import ChatGLMModelLogoDark from '@renderer/assets/images/models/chatglm_dark.png'
import ChatGptModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
@@ -121,6 +121,7 @@ import WenxinModelLogo from '@renderer/assets/images/models/wenxin.png'
import WenxinModelLogoDark from '@renderer/assets/images/models/wenxin_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types'
import OpenAI from 'openai'
@@ -136,7 +137,7 @@ const visionAllowedModels = [
'qwen-vl',
'qwen2-vl',
'internvl2',
'grok',
'grok-vision-beta',
'pixtral',
'gpt-4(?:-[\\w-]+)',
'gpt-4o(?:-[\\w-]+)?',
@@ -150,9 +151,9 @@ export const VISION_REGEX = new RegExp(
'i'
)
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|base|retrieval|uae-)/i
const NOT_SUPPORTED_REGEX = /(?:^text-|embed|tts|rerank|whisper|speech|davinci|babbage|bge-|base|retrieval|uae-)/i
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina)/i
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
export function getModelLogo(modelId: string) {
const isLight = true
@@ -165,7 +166,7 @@ export function getModelLogo(modelId: string) {
pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark,
jina: isLight ? JinaModelLogo : JinaModelLogoDark,
abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
'o1-': isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark,
'gpt-4': isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,
gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,
@@ -264,22 +265,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
ollama: [],
silicon: [
{
id: 'Qwen/Qwen2.5-72B-Instruct',
id: 'deepseek-ai/DeepSeek-V2.5',
name: 'deepseek-ai/DeepSeek-V2.5',
provider: 'silicon',
name: 'Qwen2.5-72B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-32B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-32B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-14B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-14B-Instruct',
group: 'Qwen2.5'
group: 'deepseek-ai'
},
{
id: 'Qwen/Qwen2.5-7B-Instruct',
@@ -288,79 +277,17 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2-7B-Instruct',
id: 'meta-llama/Llama-3.3-70B-Instruct',
name: 'meta-llama/Llama-3.3-70B-Instruct',
provider: 'silicon',
name: 'Qwen2-7B-Instruct',
group: 'Qwen2'
},
{
id: 'Qwen/Qwen2-72B-Instruct',
provider: 'silicon',
name: 'Qwen2-72B-Instruct',
group: 'Qwen2'
},
{
id: 'THUDM/glm-4-9b-chat',
provider: 'silicon',
name: 'GLM-4-9B-Chat',
group: 'GLM'
},
{
id: 'deepseek-ai/DeepSeek-V2-Chat',
provider: 'silicon',
name: 'DeepSeek-V2-Chat',
group: 'DeepSeek'
},
{
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
provider: 'silicon',
name: 'DeepSeek-Coder-V2-Instruct',
group: 'DeepSeek'
group: 'meta-llama'
}
],
openai: [
{
id: 'gpt-4o',
provider: 'openai',
name: ' GPT-4o',
group: 'GPT 4o'
},
{
id: 'gpt-4o-mini',
provider: 'openai',
name: ' GPT-4o-mini',
group: 'GPT 4o'
},
{
id: 'chatgpt-4o-latest',
provider: 'openai',
name: ' GPT-4o-latest',
group: 'GPT 4o'
},
{
id: 'gpt-4-turbo',
provider: 'openai',
name: ' GPT-4 Turbo',
group: 'GPT 4'
},
{
id: 'gpt-4',
provider: 'openai',
name: ' GPT-4',
group: 'GPT 4'
},
{
id: 'o1-mini',
provider: 'openai',
name: ' o1-mini',
group: 'o1'
},
{
id: 'o1-preview',
provider: 'openai',
name: ' o1-preview',
group: 'o1'
}
{ id: 'gpt-4o', provider: 'openai', name: ' GPT-4o', group: 'GPT 4o' },
{ id: 'gpt-4o-mini', provider: 'openai', name: ' GPT-4o-mini', group: 'GPT 4o' },
{ id: 'o1-mini', provider: 'openai', name: ' o1-mini', group: 'o1' },
{ id: 'o1-preview', provider: 'openai', name: ' o1-preview', group: 'o1' }
],
'azure-openai': [
{
@@ -384,10 +311,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Gemini 1.5'
},
{
id: 'gemini-1.5-pro-exp-0801',
id: 'gemini-1.5-pro',
name: 'Gemini 1.5 Pro',
provider: 'gemini',
name: 'Gemini 1.5 Pro Experimental 0801',
group: 'Gemini 1.5'
group: 'gemini-1.5'
}
],
anthropic: [
@@ -587,42 +514,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
}
],
yi: [
{
id: 'yi-large',
provider: 'yi',
name: 'Yi-Large',
group: 'Yi'
},
{
id: 'yi-large-turbo',
provider: 'yi',
name: 'Yi-Large-Turbo',
group: 'Yi'
},
{
id: 'yi-large-rag',
provider: 'yi',
name: 'Yi-Large-Rag',
group: 'Yi'
},
{
id: 'yi-medium',
provider: 'yi',
name: 'Yi-Medium',
group: 'Yi'
},
{
id: 'yi-medium-200k',
provider: 'yi',
name: 'Yi-Medium-200k',
group: 'Yi'
},
{
id: 'yi-spark',
provider: 'yi',
name: 'Yi-Spark',
group: 'Yi'
}
{ id: 'yi-lightning', name: 'yi-lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
{ id: 'yi-medium', name: 'yi-medium', provider: 'yi', group: 'yi-medium', owned_by: '01.ai' },
{ id: 'yi-large', name: 'yi-large', provider: 'yi', group: 'yi-large', owned_by: '01.ai' },
{ id: 'yi-vision', name: 'yi-vision', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
],
zhipu: [
{
@@ -676,22 +571,11 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
],
moonshot: [
{
id: 'moonshot-v1-8k',
id: 'moonshot-v1-auto',
name: 'moonshot-v1-auto',
provider: 'moonshot',
name: 'Moonshot V1 8k',
group: 'Moonshot V1'
},
{
id: 'moonshot-v1-32k',
provider: 'moonshot',
name: 'Moonshot V1 32k',
group: 'Moonshot V1'
},
{
id: 'moonshot-v1-128k',
provider: 'moonshot',
name: 'Moonshot V1 128k',
group: 'Moonshot V1'
group: 'moonshot-v1',
owned_by: 'moonshot'
}
],
baichuan: [
@@ -715,24 +599,11 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
}
],
bailian: [
{
id: 'qwen-turbo',
provider: 'dashscope',
name: 'Qwen Turbo',
group: 'Qwen'
},
{
id: 'qwen-plus',
provider: 'dashscope',
name: 'Qwen Plus',
group: 'Qwen'
},
{
id: 'qwen-max',
provider: 'dashscope',
name: 'Qwen Max',
group: 'Qwen'
}
{ id: 'qwen-vl-plus', name: 'qwen-vl-plus', provider: 'dashscope', group: 'qwen-vl', owned_by: 'system' },
{ id: 'qwen-coder-plus', name: 'qwen-coder-plus', provider: 'dashscope', group: 'qwen-coder', owned_by: 'system' },
{ id: 'qwen-turbo', name: 'qwen-turbo', provider: 'dashscope', group: 'qwen-turbo', owned_by: 'system' },
{ id: 'qwen-plus', name: 'qwen-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
{ id: 'qwen-max', name: 'qwen-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' }
],
stepfun: [
{
@@ -807,6 +678,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'grok',
name: 'Grok Beta',
group: 'Grok'
},
{
id: 'grok-vision-beta',
provider: 'grok',
name: 'Grok Vision Beta',
group: 'Grok'
}
],
mistral: [
@@ -823,7 +700,56 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Mistral'
}
],
jina: [],
jina: [
{
id: 'jina-clip-v1',
provider: 'jina',
name: 'jina-clip-v1',
group: 'Jina Clip'
},
{
id: 'jina-clip-v2',
provider: 'jina',
name: 'jina-clip-v2',
group: 'Jina Clip'
},
{
id: 'jina-embeddings-v2-base-en',
provider: 'jina',
name: 'jina-embeddings-v2-base-en',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-es',
provider: 'jina',
name: 'jina-embeddings-v2-base-es',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-de',
provider: 'jina',
name: 'jina-embeddings-v2-base-de',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-zh',
provider: 'jina',
name: 'jina-embeddings-v2-base-zh',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-code',
provider: 'jina',
name: 'jina-embeddings-v2-base-code',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v3',
provider: 'jina',
name: 'jina-embeddings-v3',
group: 'Jina Embeddings V3'
}
],
aihubmix: [
{
id: 'gpt-4o-mini',
@@ -1040,13 +966,43 @@ export function isTextToImageModel(model: Model): boolean {
}
export function isEmbeddingModel(model: Model): boolean {
return EMBEDDING_REGEX.test(model.id)
if (!model) {
return false
}
if (['anthropic'].includes(model?.provider)) {
return false
}
return EMBEDDING_REGEX.test(model.id) || model.type?.includes('embedding') || false
}
export function isVisionModel(model: Model): boolean {
if (!model) {
return false
}
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
}
export function isSupportedModel(model: OpenAI.Models.Model): boolean {
if (!model) {
return false
}
return !NOT_SUPPORTED_REGEX.test(model.id)
}
export function isWebSearchModel(model: Model): boolean {
if (!model) {
return false
}
const provider = getProviderByModel(model)
if (!provider) {
return false
}
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp'
}

View File

@@ -45,7 +45,24 @@ export const AGENT_PROMPT = `
`
export const SUMMARIZE_PROMPT =
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号'
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号'
export const TRANSLATE_PROMPT =
'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.'
export const REFERENCE_PROMPT = `请根据参考资料回答问题,并使用脚注格式引用数据来源。请忽略无关的参考资料。
## 脚注格式:
1. **脚注标记**:在正文中使用 [^数字] 的形式标记脚注,例如 [^1]。
2. **脚注内容**:在文档末尾使用 [^数字]: 脚注内容 的形式定义脚注的具体内容
3. **脚注内容**:应该尽量简洁
## 我的问题是:
{question}
## 参考资料:
{references}
`

View File

@@ -355,11 +355,11 @@ export const PROVIDER_CONFIG = {
},
aihubmix: {
api: {
url: 'https://aihubmix.com'
url: 'https://aihubmix.com?aff=SJyh'
},
websites: {
official: 'https://aihubmix.com/',
apiKey: 'https://aihubmix.com/token',
official: 'https://aihubmix.com?aff=SJyh',
apiKey: 'https://aihubmix.com?aff=SJyh',
docs: 'https://doc.aihubmix.com/',
models: 'https://aihubmix.com/models'
}

View File

@@ -2,6 +2,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { LanguageVarious } from '@renderer/types'
import { ConfigProvider, theme } from 'antd'
import enUS from 'antd/locale/en_US'
import jaJP from 'antd/locale/ja_JP'
import ruRU from 'antd/locale/ru_RU'
import zhCN from 'antd/locale/zh_CN'
import zhTW from 'antd/locale/zh_TW'
@@ -31,6 +32,13 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
Menu: {
activeBarBorderWidth: 0,
darkItemBg: 'transparent'
},
Button: {
boxShadow: 'none',
boxShadowSecondary: 'none',
defaultShadow: 'none',
dangerShadow: 'none',
primaryShadow: 'none'
}
},
token: {
@@ -52,6 +60,8 @@ function getAntdLocale(language: LanguageVarious) {
return enUS
case 'ru-RU':
return ruRU
case 'ja-JP':
return jaJP
default:
return zhCN

View File

@@ -54,13 +54,15 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
const codeToHtml = async (code: string, language: string) => {
if (!highlighter) return ''
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
try {
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
if (language in bundledLanguages || language === 'text') {
await highlighter.loadLanguage(language as BundledLanguage)
console.log(`Loaded language: ${language}`)
} else {
return `<pre style="padding: 10px"><code>${code}</code></pre>`
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
}
}
@@ -70,7 +72,7 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
})
} catch (error) {
console.warn(`Error highlighting code for language '${language}':`, error)
return `<pre style="padding: 10px"><code>${code}</code></pre>`
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
}
}

View File

@@ -1,4 +1,4 @@
import { FileType, Topic } from '@renderer/types'
import { FileType, KnowledgeItem, Topic } from '@renderer/types'
import { Dexie, type EntityTable } from 'dexie'
// Database declaration (move this to its own module also)
@@ -6,6 +6,7 @@ export const db = new Dexie('CherryStudio') as Dexie & {
files: EntityTable<FileType, 'id'>
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
settings: EntityTable<{ id: string; value: any }, 'id'>
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
}
db.version(1).stores({
@@ -18,4 +19,11 @@ db.version(2).stores({
settings: '&id, value'
})
db.version(3).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id, messages',
settings: '&id, value',
knowledge_notes: '&id, baseId, type, content, created_at, updated_at'
})
export default db

View File

@@ -19,5 +19,6 @@ declare global {
modal: HookAPI
keyv: KeyvStorage
mermaid: any
store: any
}
}

View File

@@ -2,23 +2,36 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils'
import { useLiveQuery } from 'dexie-react-hooks'
import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant'
import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler'
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode } = useSettings()
const {
proxyUrl,
language,
windowStyle,
manualUpdateCheck,
proxyMode,
webdavAutoSync,
webdavSyncInterval,
customCss
} = useSettings()
const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
useUpdateHandler()
useEffect(() => {
avatar?.value && dispatch(setAvatar(avatar.value))
}, [avatar, dispatch])
@@ -28,11 +41,12 @@ export function useAppInit() {
runAsyncFunction(async () => {
const { isPackaged } = await window.api.getAppInfo()
if (isPackaged && !manualUpdateCheck) {
setTimeout(window.api.checkForUpdate, 3000)
await delay(2)
const { updateInfo } = await window.api.checkForUpdate()
dispatch(setUpdateState({ info: updateInfo }))
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [dispatch, manualUpdateCheck])
useEffect(() => {
if (proxyMode === 'system') {
@@ -69,4 +83,26 @@ export function useAppInit() {
dispatch(setFilesPath(info.filesPath))
})
}, [dispatch])
useEffect(() => {
webdavAutoSync ? startAutoSync() : stopAutoSync()
}, [webdavAutoSync, webdavSyncInterval])
useEffect(() => {
import('@renderer/queue/KnowledgeQueue')
}, [])
useEffect(() => {
const oldCustomCss = document.getElementById('user-defined-custom-css')
if (oldCustomCss) {
oldCustomCss.remove()
}
if (customCss) {
const style = document.createElement('style')
style.id = 'user-defined-custom-css'
style.textContent = customCss
document.head.appendChild(style)
}
}, [customCss])
}

View File

@@ -1,3 +1,4 @@
import { db } from '@renderer/databases'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
@@ -50,8 +51,20 @@ export function useAssistant(id: string) {
dispatch(removeTopic({ assistantId: assistant.id, topic }))
},
moveTopic: (topic: Topic, toAssistant: Assistant) => {
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic } }))
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } }))
dispatch(removeTopic({ assistantId: assistant.id, topic }))
// update topic messages in database
db.topics
.where('id')
.equals(topic.id)
.modify((dbTopic) => {
if (dbTopic.messages) {
dbTopic.messages = dbTopic.messages.map((message) => ({
...message,
assistantId: toAssistant.id
}))
}
})
},
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),

View File

@@ -0,0 +1,279 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { db } from '@renderer/databases/index'
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
import FileManager from '@renderer/services/FileManager'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { RootState } from '@renderer/store'
import {
addBase,
addFiles as addFilesAction,
addItem,
clearAllProcessing,
clearCompletedProcessing,
deleteBase,
removeItem as removeItemAction,
renameBase,
updateBase,
updateBases,
updateItemProcessingStatus,
updateNotes
} from '@renderer/store/knowledge'
import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types'
import { KnowledgeItem } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { v4 as uuidv4 } from 'uuid'
export const useKnowledge = (baseId: string) => {
const dispatch = useDispatch()
const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId))
// 重命名知识库
const renameKnowledgeBase = (name: string) => {
dispatch(renameBase({ baseId, name }))
}
// 更新知识库
const updateKnowledgeBase = (base: KnowledgeBase) => {
dispatch(updateBase(base))
}
// 批量添加文件
const addFiles = (files: FileType[]) => {
const filesItems: KnowledgeItem[] = files.map((file) => ({
id: uuidv4(),
type: 'file' as const,
content: file,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}))
dispatch(addFilesAction({ baseId, items: filesItems }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 添加URL
const addUrl = (url: string) => {
const newUrlItem: KnowledgeItem = {
id: uuidv4(),
type: 'url' as const,
content: url,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newUrlItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 添加笔记
const addNote = async (content: string) => {
const noteId = uuidv4()
const note: KnowledgeItem = {
id: noteId,
type: 'note',
content,
created_at: Date.now(),
updated_at: Date.now()
}
// 存储完整笔记到数据库
await db.knowledge_notes.add(note)
// 在 store 中只存储引用
const noteRef: KnowledgeItem = {
id: noteId,
baseId,
type: 'note',
content: '', // store中不需要存储实际内容
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(updateNotes({ baseId, item: noteRef }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 更新笔记内容
const updateNoteContent = async (noteId: string, content: string) => {
const note = await db.knowledge_notes.get(noteId)
if (note) {
const updatedNote = {
...note,
content,
updated_at: Date.now()
}
await db.knowledge_notes.put(updatedNote)
dispatch(updateNotes({ baseId, item: updatedNote }))
}
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 获取笔记内容
const getNoteContent = async (noteId: string) => {
return await db.knowledge_notes.get(noteId)
}
// 移除项目
const removeItem = async (item: KnowledgeItem) => {
dispatch(removeItemAction({ baseId, item }))
if (base) {
if (item?.uniqueId) {
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) })
}
if (item.type === 'file' && typeof item.content === 'object') {
await FileManager.deleteFile(item.content.id)
}
}
}
// 更新处理状态
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
dispatch(
updateItemProcessingStatus({
baseId,
itemId,
status,
progress,
error
})
)
}
// 获取特定项目的处理状态
const getProcessingStatus = (itemId: string) => {
return base?.items.find((item) => item.id === itemId)?.processingStatus
}
// 获取特定类型的所有处理项
const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => {
return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || []
}
// 清除已完成的项目
const clearCompleted = () => {
dispatch(clearCompletedProcessing({ baseId }))
}
// 清除所有处理状态
const clearAll = () => {
dispatch(clearAllProcessing({ baseId }))
}
// 添加 Sitemap
const addSitemap = (url: string) => {
const newSitemapItem: KnowledgeItem = {
id: uuidv4(),
type: 'sitemap' as const,
content: url,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newSitemapItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// Add directory support
const addDirectory = (path: string) => {
const newDirectoryItem: KnowledgeItem = {
id: uuidv4(),
type: 'directory',
content: path,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newDirectoryItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
const fileItems = base?.items.filter((item) => item.type === 'file') || []
const directoryItems = base?.items.filter((item) => item.type === 'directory') || []
const urlItems = base?.items.filter((item) => item.type === 'url') || []
const sitemapItems = base?.items.filter((item) => item.type === 'sitemap') || []
const [noteItems, setNoteItems] = useState<KnowledgeItem[]>([])
useEffect(() => {
const notes = base?.items.filter((item) => item.type === 'note') || []
runAsyncFunction(async () => {
const newNoteItems = await Promise.all(
notes.map(async (item) => {
const note = await db.knowledge_notes.get(item.id)
return { ...item, content: note?.content || '' }
})
)
setNoteItems(newNoteItems.filter((note) => note !== undefined) as KnowledgeItem[])
})
}, [base?.items])
return {
base,
fileItems,
urlItems,
sitemapItems,
noteItems,
renameKnowledgeBase,
updateKnowledgeBase,
addFiles,
addUrl,
addSitemap,
addNote,
updateNoteContent,
getNoteContent,
updateItemStatus,
getProcessingStatus,
getProcessingItemsByType,
clearCompleted,
clearAll,
removeItem,
directoryItems,
addDirectory
}
}
export const useKnowledgeBases = () => {
const dispatch = useDispatch()
const bases = useSelector((state: RootState) => state.knowledge.bases)
const addKnowledgeBase = (base: KnowledgeBase) => {
dispatch(addBase(base))
}
const renameKnowledgeBase = (baseId: string, name: string) => {
dispatch(renameBase({ baseId, name }))
}
const deleteKnowledgeBase = (baseId: string) => {
dispatch(deleteBase({ baseId }))
}
const updateKnowledgeBases = (bases: KnowledgeBase[]) => {
dispatch(updateBases(bases))
}
return {
bases,
addKnowledgeBase,
renameKnowledgeBase,
deleteKnowledgeBase,
updateKnowledgeBases
}
}

View File

@@ -1,8 +1,9 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import {
SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut,
setTheme,
SettingsState,
setTopicPosition,
setTray,
setWindowStyle
@@ -41,3 +42,7 @@ export function useMessageStyle() {
isBubbleStyle
}
}
export const getStoreSetting = (key: keyof SettingsState) => {
return store.getState().settings[key]
}

View File

@@ -1,3 +1,4 @@
import { isMac, isWindows } from '@renderer/config/constant'
import { useAppSelector } from '@renderer/store'
import { useCallback } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
@@ -38,7 +39,7 @@ export const useShortcut = (
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey)
useHotkeys(
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : '',
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : 'none',
(e) => {
if (options.preventDefault) {
e.preventDefault()
@@ -49,7 +50,8 @@ export const useShortcut = (
},
{
enableOnFormTags: options.enableOnFormTags,
description: options.description || shortcutConfig?.key
description: options.description || shortcutConfig?.key,
enabled: !!shortcutConfig?.enabled
}
)
}
@@ -58,3 +60,31 @@ export function useShortcuts() {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
return { shortcuts }
}
export function useShortcutDisplay(key: string) {
const formatShortcut = useCallback((shortcut: string[]) => {
return shortcut
.map((key) => {
switch (key.toLowerCase()) {
case 'control':
return isMac ? '⌃' : 'Ctrl'
case 'ctrl':
return isMac ? '⌃' : 'Ctrl'
case 'command':
return isMac ? '⌘' : isWindows ? 'Win' : 'Super'
case 'alt':
return isMac ? '⌥' : 'Alt'
case 'shift':
return isMac ? '⇧' : 'Shift'
case 'commandorcontrol':
return isMac ? '⌘' : 'Ctrl'
default:
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()
}
})
.join('+')
}, [])
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
const shortcutConfig = shortcuts.find((s) => s.key === key)
return shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : ''
}

View File

@@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings'
import { setShowAssistants, setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings'
export function useShowAssistants() {
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
@@ -7,6 +7,7 @@ export function useShowAssistants() {
return {
showAssistants,
setShowAssistants: (show: boolean) => dispatch(setShowAssistants(show)),
toggleShowAssistants: () => dispatch(toggleShowAssistants())
}
}

View File

@@ -0,0 +1,68 @@
import { useAppDispatch } from '@renderer/store'
import { setUpdateState } from '@renderer/store/runtime'
import type { ProgressInfo, UpdateInfo } from 'electron-updater'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
export default function useUpdateHandler() {
const dispatch = useAppDispatch()
const { t } = useTranslation()
useEffect(() => {
const ipcRenderer = window.electron.ipcRenderer
const removers = [
ipcRenderer.on('update-not-available', () => {
dispatch(setUpdateState({ checking: false }))
if (window.location.hash.includes('settings/about')) {
window.message.success(t('settings.about.updateNotAvailable'))
}
}),
ipcRenderer.on('update-available', (_, releaseInfo: UpdateInfo) => {
dispatch(
setUpdateState({
checking: false,
downloading: true,
info: releaseInfo,
available: true
})
)
}),
ipcRenderer.on('download-update', () => {
dispatch(
setUpdateState({
checking: false,
downloading: true
})
)
}),
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => {
dispatch(
setUpdateState({
downloading: progress.percent < 100,
downloadProgress: progress.percent
})
)
}),
ipcRenderer.on('update-downloaded', () => {
dispatch(setUpdateState({ downloading: false }))
}),
ipcRenderer.on('update-error', (_, error) => {
dispatch(
setUpdateState({
checking: false,
downloading: false,
downloadProgress: 0
})
)
if (window.location.hash.includes('settings/about')) {
window.modal.info({
title: t('settings.about.updateError'),
content: error?.message || t('settings.about.updateError'),
icon: null
})
}
})
]
return () => removers.forEach((remover) => remover())
}, [dispatch, t])
}

View File

@@ -2,6 +2,7 @@ import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import enUS from './locales/en-us.json'
import jaJP from './locales/ja-jp.json'
import ruRU from './locales/ru-ru.json'
import zhCN from './locales/zh-cn.json'
import zhTW from './locales/zh-tw.json'
@@ -10,6 +11,7 @@ const resources = {
'en-US': enUS,
'zh-CN': zhCN,
'zh-TW': zhTW,
'ja-JP': jaJP,
'ru-RU': ruRU
}

View File

@@ -72,7 +72,7 @@
"input.estimated_tokens.tip": "Estimated tokens",
"input.expand": "Expand",
"input.new.context": "Clear Context",
"input.new_topic": "New Topic {{Command}}+N",
"input.new_topic": "New Topic {{Command}}",
"input.pause": "Pause",
"input.placeholder": "Type your message here...",
"input.send": "Send",
@@ -80,8 +80,11 @@
"input.topics": " Topics ",
"input.translate": "Translate to English",
"input.upload": "Upload image or document file",
"input.web_search": "Enable web search",
"input.knowledge_base": "Knowledge Base",
"message.new.branch": "New Branch",
"message.new.branch.created": "New Branch Created",
"message.regenerate.model": "Switch Model",
"message.new.context": "New Context",
"save": "Save",
"settings.code_collapsible": "Code block collapsible",
@@ -95,6 +98,8 @@
"settings.show_line_numbers": "Show line numbers in code",
"settings.temperature": "Temperature",
"settings.temperature.tip": "Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.",
"settings.top_p": "Top-P",
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
"suggestions.title": "Suggested Questions",
"topics.auto_rename": "Auto Rename",
"topics.clear.title": "Clear Messages",
@@ -107,7 +112,8 @@
"topics.list": "Topic List",
"topics.move_to": "Move to",
"topics.title": "Topics",
"translate": "Translate"
"translate": "Translate",
"resend": "Resend"
},
"common": {
"and": "and",
@@ -218,11 +224,13 @@
"assistant.added.content": "Assistant added successfully",
"backup.failed": "Backup failed",
"backup.success": "Backup successful",
"backup.start.success": "Backup started",
"chat.completion.paused": "Chat completion paused",
"copied": "Copied!",
"error.enter.api.host": "Please enter your API host first",
"error.enter.api.key": "Please enter your API key first",
"error.enter.model": "Please select a model first",
"error.enter.name": "Please enter the name of the knowledge base",
"error.invalid.proxy.url": "Invalid proxy URL",
"error.invalid.webdav": "Invalid WebDAV settings",
"message.code_style": "Code style",
@@ -241,21 +249,13 @@
"upgrade.success.button": "Restart",
"upgrade.success.content": "Please restart the application to complete the upgrade",
"upgrade.success.title": "Upgrade successfully",
"regenerate.confirm": "Regenerating will replace current message"
"regenerate.confirm": "Regenerating will replace current message",
"copy.success": "Copied!",
"error.get_embedding_dimensions": "Failed to get embedding dimensions"
},
"minapp": {
"title": "MinApp"
},
"model": {
"pinned": "Pinned",
"search": "Search models...",
"stream_output": "Stream output",
"type": {
"select": "Select Model Types",
"text": "Text",
"vision": "Vision"
}
},
"ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
"keep_alive_time.placeholder": "Minutes",
@@ -316,6 +316,7 @@
"settings": {
"about": "About & Feedback",
"about.checkUpdate": "Check Update",
"about.checkUpdate.available": "Update",
"about.checkingUpdate": "Checking for updates...",
"about.contact.button": "Email",
"about.contact.title": "Contact",
@@ -333,6 +334,7 @@
"about.updateNotAvailable": "You are using the latest version",
"about.website.button": "Website",
"about.website.title": "Official Website",
"about.social.title": "Social Accounts",
"advanced.auto_switch_to_topics": "Auto switch to topic",
"advanced.title": "Advanced Settings",
"assistant": "Default Assistant",
@@ -356,6 +358,8 @@
"webdav.password": "WebDAV Password",
"webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "Auto Sync",
"webdav.minutes": "Minutes",
"webdav.restore.button": "Restore from WebDAV",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV User"
@@ -373,17 +377,26 @@
"general.user_name": "User Name",
"general.user_name.placeholder": "Enter your name",
"general.view_webdav_settings": "View WebDAV settings",
"general.display.title": "Display Settings",
"display.sidebar.minapp.icon": "Show MinApp icon",
"display.sidebar.files.icon": "Show Files icon",
"display.sidebar.title": "Sidebar Settings",
"display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */",
"input.auto_translate_with_space": "Quickly translate with 3 spaces",
"messages.divider": "Show divider between messages",
"messages.input.paste_long_text_as_file": "Paste long text as file",
"messages.input.send_shortcuts": "Send shortcuts",
"messages.input.show_estimated_tokens": "Show estimated tokens",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input msg",
"messages.math_engine": "Math render engine",
"messages.model.title": "Model Settings",
"messages.title": "Message Settings",
"messages.use_serif_font": "Use serif font",
"messages.input.paste_long_text_threshold": "Paste long text length",
"model": "Default Model",
"models.add.add_model": "Add Model",
"models.add.group_name": "Group Name",
@@ -405,6 +418,7 @@
"models.translate_model_prompt_title": "Translate Model Prompt",
"models.topic_naming_model_setting_title": "Topic Naming Model Settings",
"models.enable_topic_naming": "Topic Auto Naming",
"models.topic_naming_prompt": "Topic Naming Prompt",
"provider": {
"add.name": "Provider Name",
"add.name.placeholder": "Example: OpenAI",
@@ -468,7 +482,10 @@
"press_shortcut": "Press Shortcut",
"alt_warning": "Mac does not support Option + letters as shortcuts",
"reset_to_default": "Reset to Default",
"clear_shortcut": "Clear Shortcut"
"clear_shortcut": "Clear Shortcut",
"toggle_show_assistants": "Toggle Assistants",
"toggle_show_topics": "Toggle Topics",
"copy_last_message": "Copy Last Message"
},
"theme.auto": "Auto",
"theme.dark": "Dark",
@@ -505,7 +522,83 @@
},
"words": {
"knowledgeGraph": "Knowledge Graph",
"visualization": "Visualization"
"visualization": "Visualization",
"show_window": "Show Window",
"quit": "Quit"
},
"knowledge_base": {
"title": "Knowledge Base",
"search": "Search knowledge base",
"empty": "No knowledge base found",
"drag_file": "Drag file here",
"file_hint": "Support {{file_types}}",
"add": {
"title": "Add Knowledge Base"
},
"notes": "Notes",
"notes_placeholder": "Enter additional information or context for this knowledge base...",
"delete": "Delete",
"rename": "Rename",
"urls": "URLs",
"add_url": "Add URL",
"url_placeholder": "Enter URL",
"invalid_url": "Invalid URL",
"add_file": "Add File",
"status": "Status",
"index_all": "Index All",
"index_started": "Indexing started",
"cancel_index": "Cancel Indexing",
"index_cancelled": "Indexing cancelled",
"status_new": "Added",
"status_pending": "Pending",
"status_processing": "Processing",
"status_completed": "Completed",
"status_failed": "Failed",
"url_added": "URL added",
"search_placeholder": "Enter text to search",
"add_note": "Add Note",
"no_bases": "No knowledge bases available",
"clear_selection": "Clear selection",
"delete_confirm": "Are you sure you want to delete this knowledge base?",
"sitemaps": "Websites",
"add_sitemap": "Website Map",
"sitemap_placeholder": "Enter Website Map URL",
"directories": "Directories",
"add_directory": "Add Directory",
"directory_placeholder": "Enter Directory Path",
"model_info": "Model Info",
"not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base",
"source": "Source"
},
"models": {
"pinned": "Pinned",
"search": "Search models...",
"stream_output": "Stream output",
"type": {
"select": "Select Model Types",
"text": "Text",
"vision": "Vision",
"embedding": "Embedding"
},
"all": "All",
"vision": "Vision",
"websearch": "WebSearch",
"free": "Free",
"embedding": "Embedding",
"embedding_model": "Embedding Model",
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
"dimensions": "Dimensions {{dimensions}}",
"custom_parameters": "Custom Parameters",
"add_parameter": "Add Parameter",
"parameter_name": "Parameter Name",
"parameter_type": {
"string": "Text",
"number": "Number",
"boolean": "Boolean"
}
},
"prompts": {
"summarize": "You are an assistant who is good at conversation. You need to summarize the user's conversation into a title of 10 characters or less, ensuring it matches the user's primary language without using punctuation or other special symbols."
}
}
}

View File

@@ -0,0 +1,587 @@
{
"translation": {
"agents": {
"add.button": "アシスタントに追加",
"add.name": "名前",
"add.name.placeholder": "名前を入力",
"add.prompt": "プロンプト",
"add.prompt.placeholder": "プロンプトを入力",
"add.title": "エージェントを作成",
"delete.popup.content": "このエージェントを削除してもよろしいですか?",
"edit.message.add.title": "追加",
"edit.message.assistant.placeholder": "アシスタントのメッセージを入力",
"edit.message.assistant.title": "アシスタント",
"edit.message.empty.content": "会話の入力内容が空です",
"edit.message.group.title": "メッセージグループ",
"edit.message.title": "プリセットメッセージ",
"edit.message.user.placeholder": "ユーザーメッセージを入力",
"edit.message.user.title": "ユーザー",
"edit.model.select.title": "モデルを選択",
"edit.settings.hide_preset_messages": "プリセットメッセージを非表示",
"edit.title": "エージェントを編集",
"manage.title": "エージェントを管理",
"my_agents": "マイエージェント",
"search.no_results": "結果が見つかりません",
"sorting.title": "並び替え",
"tag.agent": "エージェント",
"tag.default": "デフォルト",
"tag.new": "新規",
"tag.system": "システム",
"title": "エージェント"
},
"assistants": {
"abbr": "アシスタント",
"clear.content": "トピックをクリアすると、アシスタント内のすべてのトピックとファイルが削除されます。続行しますか?",
"clear.title": "トピックをクリア",
"copy.title": "アシスタントをコピー",
"delete.content": "アシスタントを削除すると、そのアシスタントのすべてのトピックとファイルが削除されます。削除しますか?",
"delete.title": "アシスタントを削除",
"edit.title": "アシスタントを編集",
"save.success": "保存に成功しました",
"save.title": "エージェントに保存",
"search": "アシスタントを検索...",
"settings.auto_reset_model": "自動リセットモデル",
"settings.auto_reset_model.tip": "新しいトピックを作成する際にモデルを自動的にリセットします",
"settings.default_model": "デフォルトモデル",
"settings.model": "モデル設定",
"settings.preset_messages": "プリセットメッセージ",
"settings.prompt": "プロンプト設定",
"title": "アシスタント"
},
"button": {
"add": "追加",
"added": "追加済み",
"collapse": "折りたたむ",
"manage": "管理",
"select_model": "モデルを選択",
"show.all": "すべて表示"
},
"chat": {
"add.assistant.title": "アシスタントを追加",
"artifacts.button.download": "ダウンロード",
"artifacts.button.preview": "プレビュー",
"assistant.search.placeholder": "検索",
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
"default.name": "⭐️ デフォルトアシスタント",
"default.topic.name": "デフォルトトピック",
"input.clear": "クリア",
"input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?",
"input.clear.title": "すべてのメッセージをクリアしますか?",
"input.collapse": "折りたたむ",
"input.context_count.tip": "コンテキスト数",
"input.estimated_tokens.tip": "推定トークン数",
"input.expand": "展開",
"input.new.context": "コンテキストをクリア",
"input.new_topic": "新しいトピック {{Command}}",
"input.pause": "一時停止",
"input.placeholder": "ここにメッセージを入力...",
"input.send": "送信",
"input.settings": "設定",
"input.topics": " トピック ",
"input.translate": "英語に翻訳",
"input.upload": "画像またはドキュメントをアップロード",
"input.web_search": "ウェブ検索を有効にする",
"input.knowledge_base": "ナレッジベース",
"message.new.branch": "新しいブランチ",
"message.new.branch.created": "新しいブランチが作成されました",
"message.regenerate.model": "モデルを切り替え",
"message.new.context": "新しいコンテキスト",
"save": "保存",
"settings.code_collapsible": "コードブロックを折りたたむ",
"settings.context_count": "コンテキスト",
"settings.context_count.tip": "コンテキストに保持する以前のメッセージの数",
"settings.max": "最大",
"settings.max_tokens": "最大トークン制限を有効にする",
"settings.max_tokens.tip": "モデルが生成できる最大トークン数。通常のチャットでは500-800、短いテキスト生成では800-2000、コード生成では2000-3600、長いテキスト生成では4000以上を推奨",
"settings.reset": "リセット",
"settings.set_as_default": "デフォルトのアシスタントに適用",
"settings.show_line_numbers": "コードに行番号を表示",
"settings.temperature": "温度",
"settings.temperature.tip": "低い値はモデルをより創造的で予測不可能にし、高い値はより決定論的で正確にします",
"settings.top_p": "Top-P",
"settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します",
"suggestions.title": "提案された質問",
"topics.auto_rename": "自動リネーム",
"topics.clear.title": "メッセージをクリア",
"topics.edit.placeholder": "新しい名前を入力",
"topics.edit.title": "名前を編集",
"topics.export.image": "画像としてエクスポート",
"topics.export.md": "Markdownとしてエクスポート",
"topics.export.title": "エクスポート",
"topics.export.word": "Wordとしてエクスポート",
"topics.list": "トピックリスト",
"topics.move_to": "移動先",
"topics.title": "トピック",
"translate": "翻訳",
"resend": "再送信"
},
"common": {
"and": "と",
"assistant": "アシスタント",
"avatar": "アバター",
"back": "戻る",
"cancel": "キャンセル",
"chat": "チャット",
"close": "閉じる",
"copy": "コピー",
"cut": "切り取り",
"default": "デフォルト",
"delete": "削除",
"description": "説明",
"docs": "ドキュメント",
"download": "ダウンロード",
"duplicate": "複製",
"edit": "編集",
"footnotes": "脚注",
"language": "言語",
"model": "モデル",
"models": "モデル",
"name": "名前",
"paste": "貼り付け",
"prompt": "プロンプト",
"provider": "プロバイダー",
"regenerate": "再生成",
"rename": "名前を変更",
"reset": "リセット",
"save": "保存",
"search": "検索",
"select": "選択",
"topics": "トピック",
"warning": "警告",
"you": "あなた",
"clear": "クリア",
"add": "追加"
},
"error": {
"backup.file_format": "バックアップファイルの形式エラー",
"chat.response": "エラーが発生しました。APIキーが設定されていない場合は、設定 > プロバイダーでキーを設定してください",
"no_api_key": "APIキーが設定されていません",
"provider_disabled": "モデルプロバイダーが有効になっていません",
"render": {
"title": "レンダリングエラー",
"description": "数式のレンダリングに失敗しました。数式の形式が正しいか確認してください"
}
},
"export": {
"assistant": "アシスタント",
"attached_files": "添付ファイル",
"conversation_details": "会話の詳細",
"conversation_history": "会話履歴",
"created": "作成日",
"last_updated": "最終更新日",
"messages": "メッセージ",
"user": "ユーザー"
},
"files": {
"actions": "操作",
"all": "すべてのファイル",
"count": "数",
"created_at": "作成日",
"document": "ドキュメント",
"file": "ファイル",
"image": "画像",
"name": "名前",
"open": "開く",
"size": "サイズ",
"text": "テキスト",
"title": "ファイル"
},
"history": {
"continue_chat": "チャットを続ける",
"locate.message": "メッセージを探す",
"search.messages": "すべてのメッセージを検索",
"search.placeholder": "トピックまたはメッセージを検索...",
"search.topics.empty": "トピックが見つかりませんでした。Enterキーを押してすべてのメッセージを検索",
"title": "トピック検索"
},
"languages": {
"arabic": "アラビア語",
"chinese": "中国語",
"chinese-traditional": "繁体字中国語",
"english": "英語",
"french": "フランス語",
"italian": "イタリア語",
"japanese": "日本語",
"korean": "韓国語",
"portuguese": "ポルトガル語",
"russian": "ロシア語",
"spanish": "スペイン語"
},
"mermaid": {
"download": {
"png": "PNGをダウンロード",
"svg": "SVGをダウンロード"
},
"tabs": {
"preview": "プレビュー",
"source": "ソース"
},
"title": "Mermaid図"
},
"message": {
"api.connection.failed": "接続に失敗しました",
"api.connection.success": "接続に成功しました",
"assistant.added.content": "アシスタントが追加されました",
"backup.failed": "バックアップに失敗しました",
"backup.success": "バックアップに成功しました",
"backup.start.success": "バックアップを開始しました",
"chat.completion.paused": "チャットの完了が一時停止されました",
"copied": "コピーしました!",
"error.enter.api.host": "APIホストを入力してください",
"error.enter.api.key": "APIキーを入力してください",
"error.enter.model": "モデルを選択してください",
"error.invalid.proxy.url": "無効なプロキシURL",
"error.invalid.webdav": "無効なWebDAV設定",
"message.code_style": "コードスタイル",
"message.delete.content": "このメッセージを削除してもよろしいですか?",
"message.delete.title": "メッセージを削除",
"message.style": "メッセージスタイル",
"message.style.bubble": "バブル",
"message.style.plain": "プレーン",
"reset.confirm.content": "すべてのデータをリセットしてもよろしいですか?",
"reset.double.confirm.content": "すべてのデータが失われます。続行しますか?",
"reset.double.confirm.title": "データが失われます!!!",
"restore.success": "復元に成功しました",
"save.success.title": "保存に成功しました",
"switch.disabled": "アシスタントが生成中は切り替えが無効です",
"topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動",
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
"upgrade.success.title": "アップグレードに成功しました",
"regenerate.confirm": "再生成すると現在のメッセージが置き換えられます",
"copy.success": "コピーしました!"
},
"minapp": {
"title": "ミニアプリ"
},
"ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",
"keep_alive_time.placeholder": "分",
"keep_alive_time.title": "保持時間",
"title": "Ollama"
},
"paintings": {
"button.delete.image": "画像を削除",
"button.delete.image.confirm": "この画像を削除してもよろしいですか?",
"button.new.image": "新しい画像",
"guidance_scale": "ガイダンススケール",
"guidance_scale_tip": "分類器なしのガイダンス。モデルが関連する画像を探す際にプロンプトにどれだけ従うかを制御します",
"image.size": "画像サイズ",
"inference_steps": "推論ステップ数",
"inference_steps_tip": "実行する推論ステップ数。ステップ数が多いほど品質が向上しますが、時間がかかります",
"negative_prompt": "ネガティブプロンプト",
"negative_prompt_tip": "画像に含めたくない内容を説明します",
"number_images": "生成数",
"number_images_tip": "生成する画像の数1-4",
"prompt_placeholder": "作成したい画像を説明します。例:夕日の湖畔、遠くに山々",
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"title": "画像"
},
"provider": {
"aihubmix": "AiHubMix",
"anthropic": "Anthropic",
"azure-openai": "Azure OpenAI",
"baichuan": "百川",
"dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek",
"doubao": "豆包",
"fireworks": "Fireworks",
"gemini": "Gemini",
"github": "GitHub Models",
"graphrag-kylin-mountain": "GraphRAG",
"grok": "Grok",
"groq": "Groq",
"hunyuan": "腾讯混元",
"hyperbolic": "Hyperbolic",
"jina": "Jina",
"minimax": "MiniMax",
"mistral": "Mistral",
"moonshot": "月の暗面",
"nvidia": "NVIDIA",
"ocoolai": "ocoolAI",
"ollama": "Ollama",
"openai": "OpenAI",
"openrouter": "OpenRouter",
"silicon": "SiliconFlow",
"stepfun": "StepFun",
"together": "Together",
"yi": "零一万物",
"zhinao": "360智脳",
"zhipu": "智譜AI"
},
"settings": {
"about": "について",
"about.checkUpdate": "更新を確認",
"about.checkUpdate.available": "今すぐ更新",
"about.checkingUpdate": "更新を確認中...",
"about.contact.button": "メール",
"about.contact.title": "連絡先",
"about.description": "クリエイターのための強力なAIアシスタント",
"about.downloading": "ダウンロード中...",
"about.feedback.button": "フィードバック",
"about.feedback.title": "フィードバック",
"about.license.button": "ライセンス",
"about.license.title": "ライセンス",
"about.releases.button": "リリース",
"about.releases.title": "リリースノート",
"about.title": "について",
"about.updateAvailable": "新しいバージョン {{version}} が見つかりました",
"about.updateError": "更新エラー",
"about.updateNotAvailable": "最新バージョンを使用しています",
"about.website.button": "ウェブサイト",
"about.website.title": "公式ウェブサイト",
"about.social.title": "ソーシャルアカウント",
"advanced.auto_switch_to_topics": "トピックに自動的に切り替える",
"advanced.title": "詳細設定",
"assistant": "デフォルトアシスタント",
"assistant.model_params": "モデルパラメータ",
"assistant.title": "デフォルトアシスタント",
"data": {
"app_data": "アプリデータ",
"app_logs": "アプリログ",
"clear_cache": {
"button": "キャッシュをクリア",
"confirm": "キャッシュをクリアすると、アプリのキャッシュデータ(ミニアプリデータを含む)が削除されます。この操作は元に戻せません。続行しますか?",
"error": "キャッシュのクリアに失敗しました",
"success": "キャッシュがクリアされました",
"title": "キャッシュをクリア"
},
"data.title": "データディレクトリ",
"title": "データ設定",
"webdav.backup.button": "WebDAVにバックアップ",
"webdav.host": "WebDAVホスト",
"webdav.host.placeholder": "http://localhost:8080",
"webdav.password": "WebDAVパスワード",
"webdav.path": "WebDAVパス",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同期",
"webdav.minutes": "分",
"webdav.restore.button": "WebDAVから復元",
"webdav.title": "WebDAV",
"webdav.user": "WebDAVユーザー"
},
"display.title": "表示設定",
"font_size.title": "メッセージのフォントサイズ",
"general": "一般設定",
"general.backup.button": "バックアップ",
"general.backup.title": "データのバックアップと復元",
"general.manually_check_update.title": "更新チェックを無効にする",
"general.reset.button": "リセット",
"general.reset.title": "データをリセット",
"general.restore.button": "復元",
"general.title": "一般設定",
"general.user_name": "ユーザー名",
"general.user_name.placeholder": "ユーザー名を入力",
"general.view_webdav_settings": "WebDAV設定を表示",
"general.display.title": "表示設定",
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
"display.sidebar.files.icon": "ファイルのアイコンを表示",
"display.sidebar.title": "サイドバー設定",
"display.topic.title": "トピック設定",
"display.custom.css": "カスタムCSS",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
"input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
"messages.input.send_shortcuts": "送信ショートカット",
"messages.input.show_estimated_tokens": "推定トークン数を表示",
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
"messages.input.title": "入力設定",
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"messages.math_engine": "数式エンジン",
"messages.model.title": "モデル設定",
"messages.title": "メッセージ設定",
"messages.use_serif_font": "セリフフォントを使用",
"messages.input.paste_long_text_threshold": "長いテキストの長さ",
"model": "デフォルトモデル",
"models.add.add_model": "モデルを追加",
"models.add.group_name": "グループ名",
"models.add.group_name.placeholder": "例ChatGPT",
"models.add.group_name.tooltip": "例ChatGPT",
"models.add.model_id": "モデルID",
"models.add.model_id.placeholder": "必須 例gpt-3.5-turbo",
"models.add.model_id.tooltip": "例gpt-3.5-turbo",
"models.add.model_name": "モデル名",
"models.add.model_name.placeholder": "例GPT-3.5",
"models.default_assistant_model": "デフォルトアシスタントモデル",
"models.default_assistant_model_description": "新しいアシスタントを作成する際に使用されるモデル。アシスタントがモデルを設定していない場合、このモデルが使用されます",
"models.empty": "モデルが見つかりません",
"models.topic_naming_model": "トピック命名モデル",
"models.topic_naming_model_description": "新しいトピックを自動的に命名する際に使用されるモデル",
"models.translate_model": "翻訳モデル",
"models.translate_model_description": "翻訳サービスに使用されるモデル",
"models.translate_model_prompt_message": "翻訳モデルのプロンプトを入力してください",
"models.translate_model_prompt_title": "翻訳モデルのプロンプト",
"models.topic_naming_model_setting_title": "トピック命名モデルの設定",
"models.enable_topic_naming": "トピックの自動命名",
"models.topic_naming_prompt": "トピック命名プロンプト",
"provider": {
"add.name": "プロバイダー名",
"add.name.placeholder": "例OpenAI",
"add.title": "プロバイダーを追加",
"add.type": "プロバイダータイプ",
"api.url.preview": "プレビュー: {{url}}",
"api.url.reset": "リセット",
"api.url.tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します",
"api_host": "APIホスト",
"api_key": "APIキー",
"api_key.tip": "複数のキーはカンマで区切ります",
"api_version": "APIバージョン",
"check": "チェック",
"check_all_keys": "すべてのキーをチェック",
"check_multiple_keys": "複数のAPIキーをチェック",
"delete.content": "このプロバイダーを削除してもよろしいですか?",
"delete.title": "プロバイダーを削除",
"docs_check": "チェック",
"docs_more_details": "詳細を確認",
"get_api_key": "APIキーを取得",
"no_models": "API接続をチェックする前に、モデルを追加してください",
"not_checked": "未チェック",
"remove_duplicate_keys": "重複キーを削除",
"remove_invalid_keys": "無効なキーを削除",
"search_placeholder": "モデルIDまたは名前を検索",
"title": "モデルプロバイダー"
},
"proxy": {
"mode": {
"custom": "カスタムプロキシ",
"none": "プロキシを使用しない",
"system": "システムプロキシ",
"title": "プロキシモード"
},
"title": "プロキシ設定"
},
"proxy.title": "プロキシアドレス",
"shortcuts": {
"action": "操作",
"key": "キー",
"new_topic": "新しいトピック",
"title": "ショートカット",
"zoom_in": "ズームイン",
"zoom_out": "ズームアウト",
"zoom_reset": "ズームをリセット",
"show_app": "アプリを表示",
"reset_defaults": "デフォルトのショートカットをリセット",
"reset_defaults_confirm": "すべてのショートカットをリセットしてもよろしいですか?",
"press_shortcut": "ショートカットを押す",
"alt_warning": "MacではOption + 文字をショートカットとして使用できません",
"reset_to_default": "デフォルトにリセット",
"clear_shortcut": "ショートカットをクリア",
"toggle_show_assistants": "アシスタントの表示を切り替え",
"toggle_show_topics": "トピックの表示を切り替え",
"copy_last_message": "最後のメッセージをコピー"
},
"theme.auto": "自動",
"theme.dark": "ダークテーマ",
"theme.light": "ライトテーマ",
"theme.title": "テーマ",
"theme.window.style.opaque": "不透明ウィンドウ",
"theme.window.style.title": "ウィンドウスタイル",
"theme.window.style.transparent": "透明ウィンドウ",
"title": "設定",
"topic.position": "トピックの位置",
"topic.position.left": "左",
"topic.position.right": "右",
"topic.show.time": "トピックの時間を表示",
"tray.title": "システムトレイアイコンを有効にする"
},
"translate": {
"any.language": "任意の言語",
"button.translate": "翻訳",
"confirm": {
"content": "翻訳すると元のテキストが上書きされます。続行しますか?",
"title": "翻訳確認"
},
"error.not_configured": "翻訳モデルが設定されていません",
"error.failed": "翻訳に失敗しました",
"input.placeholder": "翻訳するテキストを入力",
"output.placeholder": "翻訳",
"processing": "翻訳中...",
"title": "翻訳",
"close": "閉じる"
},
"tray": {
"quit": "終了",
"show_window": "ウィンドウを表示"
},
"words": {
"knowledgeGraph": "ナレッジグラフ",
"visualization": "可視化",
"show_window": "ウィンドウを表示",
"quit": "終了"
},
"knowledge_base": {
"title": "ナレッジベース",
"search": "ナレッジベースを検索",
"empty": "ナレッジベースが見つかりません",
"drag_file": "ファイルをここにドラッグ",
"file_hint": "{{file_types}} 形式をサポート",
"add": {
"title": "ナレッジベースを追加"
},
"notes": "ノート",
"notes_placeholder": "このナレッジベースの追加情報やコンテキストを入力...",
"delete": "削除",
"rename": "名前を変更",
"urls": "URL",
"add_url": "URLを追加",
"url_placeholder": "URLを入力",
"invalid_url": "無効なURL",
"add_file": "ファイルを追加",
"status": "状態",
"index_all": "すべてをインデックス",
"index_started": "インデックスを開始",
"cancel_index": "インデックスをキャンセル",
"index_cancelled": "インデックスがキャンセルされました",
"status_new": "追加済み",
"status_pending": "保留中",
"status_processing": "処理中",
"status_completed": "完了",
"status_failed": "失敗",
"url_added": "URLが追加されました",
"search_placeholder": "検索するテキストを入力",
"add_note": "ノートを追加",
"no_bases": "ナレッジベースがありません",
"clear_selection": "選択をクリア",
"delete_confirm": "このナレッジベースを削除してもよろしいですか?",
"sitemaps": "サイトマップ",
"add_sitemap": "サイトマップを追加",
"sitemap_placeholder": "サイトマップURLを入力",
"directories": "ディレクトリ",
"add_directory": "ディレクトリを追加",
"directory_placeholder": "ディレクトリパスを入力"
},
"models": {
"pinned": "固定済み",
"search": "モデルを検索...",
"stream_output": "ストリーム出力",
"type": {
"select": "モデルタイプを選択",
"text": "テキスト",
"vision": "画像",
"embedding": "埋め込み"
},
"all": "すべて",
"vision": "画像モデル",
"websearch": "ウェブ検索モデル",
"free": "無料モデル",
"embedding": "埋め込みモデル",
"embedding_model": "埋め込みモデル",
"embedding_model_tooltip": "設定->モデルサービス->管理で追加",
"dimensions": "{{dimensions}} 次元",
"custom_parameters": "カスタムパラメータ",
"add_parameter": "パラメータを追加",
"parameter_name": "パラメータ名",
"parameter_type": {
"string": "テキスト",
"number": "数値",
"boolean": "真偽値"
}
},
"prompts": {
"summarize": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
}
}
}

View File

@@ -72,7 +72,7 @@
"input.estimated_tokens.tip": "Затраты токенов",
"input.expand": "Развернуть",
"input.new.context": "Очистить контекст",
"input.new_topic": "Новый топик {{Command}}+N",
"input.new_topic": "Новый топик {{Command}}",
"input.pause": "Остановить",
"input.placeholder": "Введите ваше сообщение здесь...",
"input.send": "Отправить",
@@ -80,8 +80,11 @@
"input.topics": " Топики ",
"input.translate": "Перевести на английский",
"input.upload": "Загрузить изображение или документ",
"input.web_search": "Включить веб-поиск",
"input.knowledge_base": "База знаний",
"message.new.branch": "Новая ветка",
"message.new.branch.created": "Новая ветка создана",
"message.regenerate.model": "Переключить модель",
"message.new.context": "Новый контекст",
"save": "Сохранить",
"settings.code_collapsible": "Блок кода свернут",
@@ -95,6 +98,8 @@
"settings.show_line_numbers": "Показать номера строк в коде",
"settings.temperature": "Температура",
"settings.temperature.tip": "Меньшие значения делают модель более креативной и непредсказуемой, в то время как большие значения делают её более детерминированной и точной.",
"settings.top_p": "Top-P",
"settings.top_p.tip": "Значение по умолчанию 1, чем меньше значение, тем меньше вариативности в ответах, тем проще понять, чем больше значение, тем больше вариативности в ответах, тем больше разнообразие",
"suggestions.title": "Предложенные вопросы",
"topics.auto_rename": "Автопереименование",
"topics.clear.title": "Очистить сообщения",
@@ -107,7 +112,8 @@
"topics.list": "Список топиков",
"topics.move_to": "Переместить в",
"topics.title": "Топики",
"translate": "Перевести"
"translate": "Перевести",
"resend": "Переотправить"
},
"common": {
"and": "и",
@@ -218,11 +224,13 @@
"assistant.added.content": "Ассистент успешно добавлен",
"backup.failed": "Создание резервной копии не удалось",
"backup.success": "Резервная копия успешно создана",
"backup.start.success": "Создание резервной копии начато",
"chat.completion.paused": "Завершение чата приостановлено",
"copied": "Скопировано!",
"error.enter.api.host": "Пожалуйста, введите ваш API хост",
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
"error.enter.model": "Пожалуйста, выберите модель",
"error.enter.name": "Пожалуйста, введите название базы знаний",
"error.invalid.proxy.url": "Неверный URL прокси",
"error.invalid.webdav": "Неверные настройки WebDAV",
"message.code_style": "Стиль кода",
@@ -241,21 +249,13 @@
"upgrade.success.button": "Перезапустить",
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
"upgrade.success.title": "Обновление успешно",
"regenerate.confirm": "Перегенерация заменит текущее сообщение"
"regenerate.confirm": "Перегенерация заменит текущее сообщение",
"copy.success": "Скопировано!",
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
},
"minapp": {
"title": "Встроенные приложения"
},
"model": {
"pinned": "Закреплено",
"search": "Поиск моделей...",
"stream_output": "Потоковый вывод",
"type": {
"select": "Выберите тип модели",
"text": "Текст",
"vision": "Изображение"
}
},
"ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
"keep_alive_time.placeholder": "Минуты",
@@ -316,6 +316,7 @@
"settings": {
"about": "О программе и обратная связь",
"about.checkUpdate": "Проверить обновления",
"about.checkUpdate.available": "Обновить",
"about.checkingUpdate": "Проверка обновлений...",
"about.contact.button": "Электронная почта",
"about.contact.title": "Контакты",
@@ -333,6 +334,7 @@
"about.updateNotAvailable": "Вы используете последнюю версию",
"about.website.button": "Сайт",
"about.website.title": "Официальный сайт",
"about.social.title": "Социальные аккаунты",
"advanced.auto_switch_to_topics": "Автоматически переключаться на топик",
"advanced.title": "Расширенные настройки",
"assistant": "Ассистент по умолчанию",
@@ -356,6 +358,8 @@
"webdav.password": "Пароль WebDAV",
"webdav.path": "Путь WebDAV",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "Автоматическая синхронизация",
"webdav.minutes": "минут",
"webdav.restore.button": "Восстановление с WebDAV",
"webdav.title": "WebDAV",
"webdav.user": "Пользователь WebDAV"
@@ -373,17 +377,26 @@
"general.user_name": "Имя пользователя",
"general.user_name.placeholder": "Введите ваше имя",
"general.view_webdav_settings": "Просмотр настроек WebDAV",
"general.display.title": "Настройки отображения",
"display.sidebar.minapp.icon": "Показывать иконку мини-приложения",
"display.sidebar.files.icon": "Показывать иконку файлов",
"display.sidebar.title": "Настройки боковой панели",
"display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
"messages.divider": "Показывать разделитель между сообщениями",
"messages.input.paste_long_text_as_file": "Вставлять длинный текст как файл",
"messages.input.send_shortcuts": "Горячие клавиши для отправки",
"messages.input.show_estimated_tokens": "Показывать затраты токенов",
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
"messages.input.title": "Настройки ввода",
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
"messages.math_engine": "Математический движок",
"messages.model.title": "Настройки модели",
"messages.title": "Настройки сообщений",
"messages.use_serif_font": "Использовать serif шрифт",
"messages.input.paste_long_text_threshold": "Длина вставки длинного текста",
"model": "Модель по умолчанию",
"models.add.add_model": "Добавить модель",
"models.add.group_name": "Имя группы",
@@ -405,6 +418,7 @@
"models.translate_model_prompt_title": "Модель перевода",
"models.topic_naming_model_setting_title": "Настройки модели именования топика",
"models.enable_topic_naming": "Автоматическое переименование топика",
"models.topic_naming_prompt": "Подсказка для именования топика",
"provider": {
"add.name": "Имя провайдера",
"add.name.placeholder": "Пример: OpenAI",
@@ -468,7 +482,10 @@
"press_shortcut": "Нажмите сочетание клавиш",
"alt_warning": "Mac не поддерживает Option + буквы как горячие клавиши",
"reset_to_default": "Сбросить настройки по умолчанию",
"clear_shortcut": "Очистить сочетание клавиш"
"clear_shortcut": "Очистить сочетание клавиш",
"toggle_show_assistants": "Переключить отображение ассистентов",
"toggle_show_topics": "Переключить отображение топиков",
"copy_last_message": "Копировать последнее сообщение"
},
"theme.auto": "Автоматически",
"theme.dark": "Темная",
@@ -505,7 +522,83 @@
},
"words": {
"knowledgeGraph": "Граф знаний",
"visualization": "Визуализация"
"visualization": "Визуализация",
"show_window": "Показать окно",
"quit": "Выйти"
},
"knowledge_base": {
"title": "База знаний",
"search": "Поиск в базе знаний",
"empty": "База знаний не найдена",
"drag_file": "Перетащите файл сюда",
"file_hint": "Поддерживаются {{file_types}}",
"add": {
"title": "Добавить базу знаний"
},
"notes": "Заметки",
"notes_placeholder": "Введите дополнительную информацию или контекст для этой базы знаний...",
"delete": "Удалить",
"rename": "Переименовать",
"urls": "URL-адреса",
"add_url": "Добавить URL",
"url_placeholder": "Введите URL",
"invalid_url": "Неверный URL",
"add_file": "Добавить файл",
"status": "Статус",
"index_all": "Индексировать все",
"index_started": "Индексирование началось",
"cancel_index": "Отменить индексирование",
"index_cancelled": "Индексирование отменено",
"status_new": "Добавлено",
"status_pending": "Ожидание",
"status_processing": "Обработка",
"status_completed": "Завершено",
"status_failed": "Ошибка",
"url_added": "URL добавлен",
"search_placeholder": "Введите текст для поиска",
"add_note": "Добавить запись",
"no_bases": "База знаний не найдена",
"clear_selection": "Очистить выбор",
"delete_confirm": "Вы уверены, что хотите удалить эту базу знаний?",
"sitemaps": "Сайты",
"add_sitemap": "Карта сайта",
"sitemap_placeholder": "Введите URL карты сайта",
"directories": "Директории",
"add_directory": "Добавить директорию",
"directory_placeholder": "Введите путь к директории",
"model_info": "Модель информации",
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"source": "Источник"
},
"models": {
"pinned": "Закреплено",
"search": "Поиск моделей...",
"stream_output": "Потоковый вывод",
"type": {
"select": "Выберите тип модели",
"text": "Текст",
"vision": "Изображение",
"embedding": "Встраиваемые"
},
"all": "Все",
"vision": "Визуальные модели",
"websearch": "Веб-поисковые модели",
"free": "Бесплатные модели",
"embedding": "Встраиваемые модели",
"embedding_model": "Встраиваемые модели",
"embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление",
"dimensions": "{{dimensions}} мер",
"custom_parameters": "Пользовательские параметры",
"add_parameter": "Добавить параметр",
"parameter_name": "Имя параметра",
"parameter_type": {
"string": "Текст",
"number": "Число",
"boolean": "Логическое"
}
},
"prompts": {
"summarize": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов"
}
}
}

View File

@@ -72,7 +72,7 @@
"input.estimated_tokens.tip": "预估 token 数",
"input.expand": "展开",
"input.new.context": "清除上下文",
"input.new_topic": "新话题 {{Command}}+N",
"input.new_topic": "新话题 {{Command}}",
"input.pause": "暂停",
"input.placeholder": "在这里输入消息...",
"input.send": "发送",
@@ -80,8 +80,11 @@
"input.topics": " 话题 ",
"input.translate": "翻译成英文",
"input.upload": "上传图片或文档",
"input.web_search": "开启网络搜索",
"input.knowledge_base": "知识库",
"message.new.branch": "新分支",
"message.new.branch.created": "新分支已创建",
"message.regenerate.model": "切换模型",
"message.new.context": "清除上下文",
"save": "保存",
"settings.code_collapsible": "代码块可折叠",
@@ -95,6 +98,8 @@
"settings.show_line_numbers": "代码显示行号",
"settings.temperature": "模型温度",
"settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7",
"settings.top_p": "Top-P",
"settings.top_p.tip": "默认值为 1值越小AI 生成的内容越单调也越容易理解值越大AI 回复的词汇围越大,越多样化",
"suggestions.title": "建议的问题",
"topics.auto_rename": "生成话题名",
"topics.clear.title": "清空消息",
@@ -107,7 +112,8 @@
"topics.list": "话题列表",
"topics.move_to": "移动到",
"topics.title": "话题",
"translate": "翻译"
"translate": "翻译",
"resend": "重新发送"
},
"common": {
"and": "和",
@@ -144,7 +150,8 @@
"warning": "警告",
"you": "用户",
"clear": "清除",
"add": "添加"
"add": "添加",
"footnotes": "引用内容"
},
"error": {
"backup.file_format": "备份文件格式错误",
@@ -218,11 +225,13 @@
"assistant.added.content": "智能体添加成功",
"backup.failed": "备份失败",
"backup.success": "备份成功",
"backup.start.success": "开始备份",
"chat.completion.paused": "会话已停止",
"copied": "已复制",
"error.enter.api.host": "请输入您的 API 地址",
"error.enter.api.key": "请输入您的 API 密钥",
"error.enter.model": "请选择一个模型",
"error.enter.name": "请输入知识库名称",
"error.invalid.proxy.url": "无效的代理地址",
"error.invalid.webdav": "无效的 WebDAV 设置",
"message.code_style": "代码风格",
@@ -241,21 +250,13 @@
"upgrade.success.button": "重启",
"upgrade.success.content": "重启用以完成升级",
"upgrade.success.title": "升级成功",
"regenerate.confirm": "重新生成会覆盖当前消息"
"regenerate.confirm": "重新生成会覆盖当前消息",
"copy.success": "复制成功",
"error.get_embedding_dimensions": "获取嵌入维度失败"
},
"minapp": {
"title": "小程序"
},
"model": {
"pinned": "已固定",
"search": "搜索模型...",
"stream_output": "流式输出",
"type": {
"select": "选择模型类型",
"text": "文本",
"vision": "图像"
}
},
"ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟",
"keep_alive_time.placeholder": "分钟",
@@ -316,6 +317,7 @@
"settings": {
"about": "关于我们",
"about.checkUpdate": "检查更新",
"about.checkUpdate.available": "立即更新",
"about.checkingUpdate": "正在检查更新...",
"about.contact.button": "邮件",
"about.contact.title": "邮件联系",
@@ -333,6 +335,7 @@
"about.updateNotAvailable": "你的软件已是最新版本",
"about.website.button": "查看",
"about.website.title": "官方网站",
"about.social.title": "社交账号",
"advanced.auto_switch_to_topics": "自动切换到话题",
"advanced.title": "高级设置",
"assistant": "默认助手",
@@ -356,6 +359,8 @@
"webdav.password": "WebDAV 密码",
"webdav.path": "WebDAV 路径",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自动同步",
"webdav.minutes": "分钟",
"webdav.restore.button": "从 WebDAV 恢复",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV 用户名"
@@ -373,17 +378,26 @@
"general.user_name": "用户名",
"general.user_name.placeholder": "请输入用户名",
"general.view_webdav_settings": "查看 WebDAV 设置",
"general.display.title": "显示设置",
"display.sidebar.minapp.icon": "显示小程序图标",
"display.sidebar.files.icon": "显示文件图标",
"display.sidebar.title": "侧边栏设置",
"display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
"input.auto_translate_with_space": "快速敲击3次空格翻译",
"messages.divider": "消息分割线",
"messages.input.paste_long_text_as_file": "长文本粘贴为文件",
"messages.input.send_shortcuts": "发送快捷键",
"messages.input.show_estimated_tokens": "显示预估 Token 数",
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
"messages.input.title": "输入设置",
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
"messages.math_engine": "数学公式引擎",
"messages.model.title": "模型设置",
"messages.title": "消息设置",
"messages.use_serif_font": "使用衬线字体",
"messages.input.paste_long_text_threshold": "长文本长度",
"model": "默认模型",
"models.add.add_model": "添加模型",
"models.add.group_name": "分组名称",
@@ -405,6 +419,7 @@
"models.translate_model_prompt_title": "翻译模型提示词",
"models.topic_naming_model_setting_title": "话题命名模型设置",
"models.enable_topic_naming": "话题自动重命名",
"models.topic_naming_prompt": "话题命名提示词",
"provider": {
"add.name": "提供商名称",
"add.name.placeholder": "例如 OpenAI",
@@ -456,7 +471,10 @@
"press_shortcut": "按下快捷键",
"alt_warning": "Mac 系统不能使用 Option + 字母作为快捷键",
"reset_to_default": "重置为默认",
"clear_shortcut": "清除快捷键"
"clear_shortcut": "清除快捷键",
"toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示",
"copy_last_message": "复制上一条消息"
},
"theme.auto": "跟随系统",
"theme.dark": "深色主题",
@@ -493,7 +511,83 @@
},
"words": {
"knowledgeGraph": "知识图谱",
"visualization": "可视化"
"visualization": "可视化",
"show_window": "显示窗口",
"quit": "退出"
},
"knowledge_base": {
"title": "知识库",
"search": "搜索知识库",
"empty": "暂无知识库",
"drag_file": "拖拽文件到这里",
"file_hint": "支持 {{file_types}} 格式",
"add": {
"title": "添加知识库"
},
"notes": "笔记",
"notes_placeholder": "输入此知识库的附加信息或上下文...",
"delete": "删除",
"rename": "重命名",
"urls": "网址",
"add_url": "添加网址",
"url_placeholder": "请输入网址",
"invalid_url": "无效的网址",
"add_file": "添加文件",
"status": "状态",
"index_all": "索引全部",
"index_started": "索引开始",
"cancel_index": "取消索引",
"index_cancelled": "索引已取消",
"status_new": "已添加",
"status_pending": "等待中",
"status_processing": "处理中",
"status_completed": "已完成",
"status_failed": "失败",
"url_added": "网址已添加",
"search_placeholder": "输入查询内容",
"add_note": "添加笔记",
"no_bases": "暂无知识库",
"clear_selection": "清除选择",
"delete_confirm": "确定要删除此知识库吗?",
"sitemaps": "网站",
"add_sitemap": "站点地图",
"sitemap_placeholder": "请输入站点地图 URL",
"directories": "目录",
"add_directory": "添加目录",
"directory_placeholder": "请输入目录路径",
"model_info": "模型信息",
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
"source": "来源"
},
"models": {
"pinned": "已固定",
"search": "搜索模型...",
"stream_output": "流式输出",
"type": {
"select": "选择模型类型",
"text": "文本",
"vision": "图像",
"embedding": "嵌入"
},
"all": "全部",
"vision": "视觉模型",
"websearch": "网络搜索模型",
"free": "免费模型",
"embedding": "嵌入模型",
"embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"dimensions": "{{dimensions}} 维",
"custom_parameters": "自定义参数",
"add_parameter": "添加参数",
"parameter_name": "参数名称",
"parameter_type": {
"string": "文本",
"number": "数字",
"boolean": "布尔值"
}
},
"prompts": {
"summarize": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号"
}
}
}

View File

@@ -72,7 +72,7 @@
"input.estimated_tokens.tip": "預估 Token 數",
"input.expand": "展開",
"input.new.context": "清除上下文",
"input.new_topic": "新話題 {{Command}}+N",
"input.new_topic": "新話題 {{Command}}",
"input.pause": "暫停",
"input.placeholder": "在此輸入您的訊息...",
"input.send": "發送",
@@ -80,8 +80,11 @@
"input.topics": " 話題 ",
"input.translate": "翻譯成英文",
"input.upload": "上傳圖片或文檔",
"input.web_search": "開啟網路搜索",
"input.knowledge_base": "知識庫",
"message.new.branch": "新分支",
"message.new.branch.created": "新分支已建立",
"message.regenerate.model": "切換模型",
"message.new.context": "新上下文",
"save": "保存",
"settings.code_collapsible": "代码块可折叠",
@@ -95,6 +98,8 @@
"settings.show_line_numbers": "代码顯示行號",
"settings.temperature": "溫度",
"settings.temperature.tip": "較低的值使模型更具創造性和不可預測性,較高的值則使其更具確定性和精確性。",
"settings.top_p": "Top-P",
"settings.top_p.tip": "模型生成文本的隨機程度。值越小AI 生成的內容越單調也越容易理解值越大AI 回覆的詞彙範圍越大,越多樣化",
"suggestions.title": "建議的問題",
"topics.auto_rename": "自動重新命名",
"topics.clear.title": "清空消息",
@@ -107,7 +112,8 @@
"topics.list": "話題列表",
"topics.move_to": "移動到",
"topics.title": "話題",
"translate": "翻譯"
"translate": "翻譯",
"resend": "重新發送"
},
"common": {
"and": "與",
@@ -126,7 +132,6 @@
"download": "下載",
"duplicate": "複製",
"edit": "編輯",
"footnotes": "引用",
"language": "語言",
"model": "模型",
"models": "模型",
@@ -144,7 +149,8 @@
"warning": "警告",
"you": "您",
"clear": "清除",
"add": "添加"
"add": "添加",
"footnotes": "引用"
},
"error": {
"backup.file_format": "備份文件格式錯誤",
@@ -162,7 +168,7 @@
"conversation_details": "會話詳情",
"conversation_history": "會話歷史",
"created": "創建時間",
"last_updated": "最後<EFBFBD><EFBFBD>新",
"last_updated": "最後新",
"messages": "訊息數",
"user": "用戶"
},
@@ -218,11 +224,13 @@
"assistant.added.content": "智能體添加成功",
"backup.failed": "備份失敗",
"backup.success": "備份成功",
"backup.start.success": "開始備份",
"chat.completion.paused": "聊天完成已暫停",
"copied": "已複製",
"error.enter.api.host": "請先輸入您的 API 主機地址",
"error.enter.api.key": "請先輸入您的 API 密鑰",
"error.enter.model": "請先選擇一個模型",
"error.enter.name": "請先輸入知識庫名稱",
"error.invalid.proxy.url": "無效的代理 URL",
"error.invalid.webdav": "無效的 WebDAV 設定",
"message.code_style": "程式碼風格",
@@ -241,21 +249,13 @@
"upgrade.success.button": "重新啟動",
"upgrade.success.content": "請重新啟動應用以完成升級",
"upgrade.success.title": "升級成功",
"regenerate.confirm": "重新生成會覆蓋當前訊息"
"regenerate.confirm": "重新生成會覆蓋當前訊息",
"copy.success": "複製成功",
"error.get_embedding_dimensions": "獲取嵌入維度失敗"
},
"minapp": {
"title": "小程序"
},
"model": {
"pinned": "已固定",
"search": "搜尋模型...",
"stream_output": "串流輸出",
"type": {
"select": "選擇模型類型",
"text": "文字",
"vision": "圖像"
}
},
"ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
"keep_alive_time.placeholder": "分鐘",
@@ -316,6 +316,7 @@
"settings": {
"about": "關於與回饋",
"about.checkUpdate": "檢查更新",
"about.checkUpdate.available": "立即更新",
"about.checkingUpdate": "正在檢查更新...",
"about.contact.button": "郵件",
"about.contact.title": "聯繫方式",
@@ -333,6 +334,7 @@
"about.updateNotAvailable": "您正在使用最新版本",
"about.website.button": "網站",
"about.website.title": "官方網站",
"about.social.title": "社交帳號",
"advanced.auto_switch_to_topics": "自動切換到話題",
"advanced.title": "進階設定",
"assistant": "預設助手",
@@ -356,6 +358,8 @@
"webdav.password": "WebDAV 密碼",
"webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同步",
"webdav.minutes": "分鐘",
"webdav.restore.button": "從 WebDAV 恢復",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV 使用者名稱"
@@ -373,17 +377,26 @@
"general.user_name": "使用者名稱",
"general.user_name.placeholder": "輸入您的名稱",
"general.view_webdav_settings": "查看 WebDAV 設定",
"general.display.title": "顯示設定",
"display.sidebar.minapp.icon": "顯示小程序圖示",
"display.sidebar.files.icon": "顯示文件圖示",
"display.sidebar.title": "側邊欄設定",
"display.topic.title": "話題設定",
"display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
"messages.divider": "訊息間顯示分隔線",
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
"messages.input.send_shortcuts": "發送快捷鍵",
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
"messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
"messages.input.title": "輸入設定",
"messages.math_engine": "Markdown 渲染輸入訊息",
"messages.math_render_engine": "數學公式引擎",
"messages.model.title": "模型設定",
"messages.title": "訊息設定",
"messages.use_serif_font": "使用襯線字體",
"messages.input.paste_long_text_threshold": "長文本長度",
"model": "預設模型",
"models.add.add_model": "添加模型",
"models.add.group_name": "群組名稱",
@@ -405,6 +418,7 @@
"models.translate_model_prompt_title": "翻譯模型提示詞",
"models.topic_naming_model_setting_title": "話題命名模型設定",
"models.enable_topic_naming": "話題自動重命名",
"models.topic_naming_prompt": "話題命名提示詞",
"provider": {
"add.name": "提供者名稱",
"add.name.placeholder": "例如OpenAI",
@@ -456,7 +470,10 @@
"press_shortcut": "按下快捷鍵",
"alt_warning": "Mac 不能使用 Option + 字母作為快捷鍵",
"reset_to_default": "重置為預設",
"clear_shortcut": "清除快捷鍵"
"clear_shortcut": "清除快捷鍵",
"toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示",
"copy_last_message": "複製上一条消息"
},
"theme.auto": "自動",
"theme.dark": "深色主題",
@@ -493,7 +510,83 @@
},
"words": {
"knowledgeGraph": "知識圖譜",
"visualization": "可視化"
"visualization": "可視化",
"show_window": "顯示視窗",
"quit": "退出"
},
"knowledge_base": {
"title": "知識庫",
"search": "搜尋知識庫",
"empty": "暫無知識庫",
"drag_file": "拖拽文件到這裡",
"file_hint": "支持 {{file_types}} 格式",
"add": {
"title": "添加知識庫"
},
"notes": "筆記",
"notes_placeholder": "輸入此知識庫的附加資訊或上下文...",
"delete": "刪除",
"rename": "重命名",
"urls": "網址",
"add_url": "添加網址",
"url_placeholder": "請輸入網址",
"invalid_url": "無效的網址",
"add_file": "添加文件",
"status": "狀態",
"index_all": "索引全部",
"index_started": "索引開始",
"cancel_index": "取消索引",
"index_cancelled": "索引已取消",
"status_new": "已添加",
"status_pending": "等待中",
"status_processing": "處理中",
"status_completed": "已完成",
"status_failed": "失敗",
"url_added": "網址已添加",
"search_placeholder": "輸入查詢內容",
"add_note": "添加筆記",
"no_bases": "暫無知識庫",
"clear_selection": "清除選擇",
"delete_confirm": "確定要刪除此知識庫嗎?",
"sitemaps": "網站",
"add_sitemap": "網站地圖",
"sitemap_placeholder": "請輸入網站地圖 URL",
"directories": "目錄",
"add_directory": "添加目錄",
"directory_placeholder": "請輸入目錄路徑",
"model_info": "模型信息",
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
"source": "來源"
},
"models": {
"pinned": "已固定",
"search": "搜尋模型...",
"stream_output": "串流輸出",
"type": {
"select": "選擇模型類型",
"text": "文字",
"vision": "圖像",
"embedding": "嵌入"
},
"all": "全部",
"vision": "視覺模型",
"websearch": "網路搜索模型",
"free": "免費模型",
"embedding": "嵌入模型",
"embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"dimensions": "{{dimensions}} 維",
"custom_parameters": "自定義參數",
"add_parameter": "添加參數",
"parameter_name": "參數名稱",
"parameter_type": {
"string": "文字",
"number": "數字",
"boolean": "布林值"
}
},
"prompts": {
"summarize": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號"
}
}
}

View File

@@ -290,6 +290,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
padding: 7px 15px !important;
border: 0.5px solid transparent;
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
user-select: none;
.ant-tabs-tab-btn {
white-space: nowrap;
overflow: hidden;

View File

@@ -4,6 +4,7 @@ export type GroupTranslations = {
'zh-CN': string
'zh-TW': string
'ru-RU': string
'ja-JP': string
}
}
@@ -12,204 +13,238 @@ export const groupTranslations: GroupTranslations = {
'en-US': 'My Agents',
'zh-CN': '我的',
'zh-TW': '我的',
'ru-RU': 'Мои агенты'
'ru-RU': 'Мои агенты',
'ja-JP': '私のエージェント'
},
: {
'en-US': 'Career',
'zh-CN': '职业',
'zh-TW': '職業',
'ru-RU': 'Карьера'
'ru-RU': 'Карьера',
'ja-JP': 'キャリア'
},
: {
'en-US': 'Business',
'zh-CN': '商业',
'zh-TW': '商業',
'ru-RU': 'Бизнес'
'ru-RU': 'Бизнес',
'ja-JP': 'ビジネス'
},
: {
'en-US': 'Tools',
'zh-CN': '工具',
'zh-TW': '工具',
'ru-RU': 'Инструменты'
'ru-RU': 'Инструменты',
'ja-JP': 'ツール'
},
: {
'en-US': 'Language',
'zh-CN': '语言',
'zh-TW': '語言',
'ru-RU': 'Язык'
'ru-RU': 'Язык',
'ja-JP': '言語'
},
: {
'en-US': 'Office',
'zh-CN': '办公',
'zh-TW': '辦公',
'ru-RU': 'Офис'
'ru-RU': 'Офис',
'ja-JP': 'オフィス'
},
: {
'en-US': 'General',
'zh-CN': '通用',
'zh-TW': '通用',
'ru-RU': 'Общее'
'ru-RU': 'Общее',
'ja-JP': '一般'
},
: {
'en-US': 'Writing',
'zh-CN': '写作',
'zh-TW': '寫作',
'ru-RU': 'Письмо'
'ru-RU': 'Письмо',
'ja-JP': '書き込み'
},
: {
'en-US': 'Featured',
'zh-CN': '精选',
'zh-TW': '精選',
'ru-RU': 'Избранное'
'ru-RU': 'Избранное',
'ja-JP': '特集'
},
: {
'en-US': 'Programming',
'zh-CN': '编程',
'zh-TW': '編程',
'ru-RU': 'Программирование'
'ru-RU': 'Программирование',
'ja-JP': 'プログラミング'
},
: {
'en-US': 'Emotion',
'zh-CN': '情感',
'zh-TW': '情感',
'ru-RU': 'Эмоции'
'ru-RU': 'Эмоции',
'ja-JP': '感情'
},
: {
'en-US': 'Education',
'zh-CN': '教育',
'zh-TW': '教育',
'ru-RU': 'Образование'
'ru-RU': 'Образование',
'ja-JP': '教育'
},
: {
'en-US': 'Creative',
'zh-CN': '创意',
'zh-TW': '創意',
'ru-RU': 'Креатив'
'ru-RU': 'Креатив',
'ja-JP': 'クリエイティブ'
},
: {
'en-US': 'Academic',
'zh-CN': '学术',
'zh-TW': '學術',
'ru-RU': 'Академический'
'ru-RU': 'Академический',
'ja-JP': 'アカデミック'
},
: {
'en-US': 'Design',
'zh-CN': '设计',
'zh-TW': '設計',
'ru-RU': 'Дизайн'
'ru-RU': 'Дизайн',
'ja-JP': 'デザイン'
},
: {
'en-US': 'Art',
'zh-CN': '艺术',
'zh-TW': '藝術',
'ru-RU': 'Искусство'
'ru-RU': 'Искусство',
'ja-JP': 'アート'
},
: {
'en-US': 'Entertainment',
'zh-CN': '娱乐',
'zh-TW': '娛樂',
'ru-RU': 'Развлечения'
'ru-RU': 'Развлечения',
'ja-JP': 'エンターテイメント'
},
: {
'en-US': 'Life',
'zh-CN': '生活',
'zh-TW': '生活',
'ru-RU': 'Жизнь'
'ru-RU': 'Жизнь',
'ja-JP': '生活'
},
: {
'en-US': 'Medical',
'zh-CN': '医疗',
'zh-TW': '醫療',
'ru-RU': 'Медицина'
'ru-RU': 'Медицина',
'ja-JP': '医療'
},
: {
'en-US': 'Games',
'zh-CN': '游戏',
'zh-TW': '遊戲',
'ru-RU': 'Игры'
'ru-RU': 'Игры',
'ja-JP': 'ゲーム'
},
: {
'en-US': 'Translation',
'zh-CN': '翻译',
'zh-TW': '翻譯',
'ru-RU': 'Перевод'
'ru-RU': 'Перевод',
'ja-JP': '翻訳'
},
: {
'en-US': 'Music',
'zh-CN': '音乐',
'zh-TW': '音樂',
'ru-RU': 'Музыка'
'ru-RU': 'Музыка',
'ja-JP': '音楽'
},
: {
'en-US': 'Review',
'zh-CN': '点评',
'zh-TW': '點評',
'ru-RU': 'Обзор'
'ru-RU': 'Обзор',
'ja-JP': 'レビュー'
},
: {
'en-US': 'Copywriting',
'zh-CN': '文案',
'zh-TW': '文案',
'ru-RU': 'Копирайтинг'
'ru-RU': 'Копирайтинг',
'ja-JP': 'コピーライティング'
},
: {
'en-US': 'Encyclopedia',
'zh-CN': '百科',
'zh-TW': '百科',
'ru-RU': 'Энциклопедия'
'ru-RU': 'Энциклопедия',
'ja-JP': '百科事典'
},
: {
'en-US': 'Health',
'zh-CN': '健康',
'zh-TW': '健康',
'ru-RU': 'Здоровье'
'ru-RU': 'Здоровье',
'ja-JP': '健康'
},
: {
'en-US': 'Marketing',
'zh-CN': '营销',
'zh-TW': '營銷',
'ru-RU': 'Маркетинг'
'ru-RU': 'Маркетинг',
'ja-JP': 'マーケティング'
},
: {
'en-US': 'Science',
'zh-CN': '科学',
'zh-TW': '科學',
'ru-RU': 'Наука'
'ru-RU': 'Наука',
'ja-JP': '科学'
},
: {
'en-US': 'Analysis',
'zh-CN': '分析',
'zh-TW': '分析',
'ru-RU': 'Анализ'
'ru-RU': 'Анализ',
'ja-JP': '分析'
},
: {
'en-US': 'Legal',
'zh-CN': '法律',
'zh-TW': '法律',
'ru-RU': 'Право'
'ru-RU': 'Право',
'ja-JP': '法律'
},
: {
'en-US': 'Consulting',
'zh-CN': '咨询',
'zh-TW': '諮詢',
'ru-RU': 'Консалтинг'
'ru-RU': 'Консалтинг',
'ja-JP': 'コンサルティング'
},
: {
'en-US': 'Finance',
'zh-CN': '金融',
'zh-TW': '金融',
'ru-RU': 'Финансы'
'ru-RU': 'Финансы',
'ja-JP': '金融'
},
: {
'en-US': 'Travel',
'zh-CN': '旅游',
'zh-TW': '旅遊',
'ru-RU': 'Путешествия'
'ru-RU': 'Путешествия',
'ja-JP': '旅行'
},
: {
'en-US': 'Management',
'zh-CN': '管理',
'zh-TW': '管理',
'ru-RU': 'Управление'
'ru-RU': 'Управление',
'ja-JP': '管理'
}
}

View File

@@ -1,6 +1,6 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import { useAgents } from '@renderer/hooks/useAgents'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types'
import { Col } from 'antd'

View File

@@ -5,16 +5,26 @@ import styled from 'styled-components'
interface Props {
app: MinAppType
onClick?: () => void
size?: number
}
const App: FC<Props> = ({ app }) => {
const onClick = () => {
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const handleClick = () => {
MinApp.start(app)
onClick?.()
}
return (
<Container onClick={onClick}>
<AppIcon src={app.logo} style={{ border: app.bodered ? '0.5px solid var(--color-border)' : 'none' }} />
<Container onClick={handleClick}>
<AppIcon
src={app.logo}
style={{
border: app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`
}}
/>
<AppTitle>{app.name}</AppTitle>
</Container>
)
@@ -26,12 +36,10 @@ const Container = styled.div`
justify-content: center;
align-items: center;
cursor: pointer;
width: 65px;
overflow: hidden;
`
const AppIcon = styled.img`
width: 60px;
height: 60px;
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
@@ -43,6 +51,7 @@ const AppTitle = styled.div`
color: var(--color-text-soft);
text-align: center;
user-select: none;
white-space: nowrap;
`
export default App

View File

@@ -74,12 +74,12 @@ const ContentContainer = styled.div`
const AppsContainer = styled.div`
display: flex;
min-width: 900px;
max-width: 900px;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
gap: 50px;
min-width: 930px;
max-width: 930px;
max-height: 500px;
display: grid;
grid-template-columns: repeat(8, minmax(90px, 1fr));
gap: 25px 25px;
`
export default AppsPage

View File

@@ -221,6 +221,7 @@ const SideNav = styled.div`
width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
padding: 7px 12px;
user-select: none;
.ant-menu {
border-inline-end: none !important;

View File

@@ -21,6 +21,7 @@ let _message: Message | undefined
const TopicsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState(_search)
const [searchKeywords, setSearchKeywords] = useState(_search)
const [stack, setStack] = useState<Route[]>(_stack)
const [topic, setTopic] = useState<Topic | undefined>(_topic)
const [message, setMessage] = useState<Message | undefined>(_message)
@@ -40,6 +41,7 @@ const TopicsPage: FC = () => {
}
const onSearch = () => {
setSearchKeywords(search)
setStack(['topics', 'search'])
setTopic(undefined)
}
@@ -84,7 +86,7 @@ const TopicsPage: FC = () => {
/>
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
<SearchResults
keywords={isShow('search') ? search : ''}
keywords={isShow('search') ? searchKeywords : ''}
onMessageClick={onMessageClick}
onTopicClick={onTopicClick}
style={{ display: isShow('search') }}

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useSettings } from '@renderer/hooks/useSettings'
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
@@ -15,6 +16,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
const SearchMessage: FC<Props> = ({ message, ...props }) => {
const navigate = NavigationService.navigate!
const { messageStyle } = useSettings()
const { t } = useTranslation()
if (!message) {
@@ -22,7 +24,7 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
}
return (
<MessagesContainer {...props}>
<MessagesContainer {...props} className={messageStyle}>
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
<MessageItem message={message} />
<Button
@@ -45,6 +47,7 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
const MessagesContainer = styled.div`
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
overflow-y: scroll;

View File

@@ -11,19 +11,16 @@ interface Props {
files: FileType[]
setFiles: (files: FileType[]) => void
ToolbarButton: any
disabled?: boolean
}
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton }) => {
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton, disabled }) => {
const { t } = useTranslation()
const extensions = isVisionModel(model)
? [...imageExts, ...documentExts, ...textExts]
: [...documentExts, ...textExts]
const onSelectFile = async () => {
if (files.length > 0) {
return setFiles([])
}
const _files = await window.api.file.select({
properties: ['openFile', 'multiSelections'],
filters: [
@@ -34,12 +31,14 @@ const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton })
]
})
_files && setFiles(_files)
if (_files) {
setFiles([...files, ..._files])
}
}
return (
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile}>
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile} disabled={disabled}>
<PaperClipOutlined style={{ rotate: '135deg' }} />
</ToolbarButton>
</Tooltip>

View File

@@ -4,18 +4,18 @@ import {
FormOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
GlobalOutlined,
PauseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons'
import { PicCenterOutlined } from '@ant-design/icons'
import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant'
import { isVisionModel } from '@renderer/config/models'
import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore'
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
@@ -24,7 +24,7 @@ import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/Toke
import { translateText } from '@renderer/services/TranslateService'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, Message, Topic } from '@renderer/types'
import { Assistant, FileType, KnowledgeBase, Message, Topic } from '@renderer/types'
import { delay, getFileExtension, uuid } from '@renderer/utils'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Button, Popconfirm, Tooltip } from 'antd'
@@ -37,6 +37,7 @@ import styled from 'styled-components'
import AttachmentButton from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview'
import KnowledgeBaseButton from './KnowledgeBaseButton'
import SendMessageButton from './SendMessageButton'
import TokenCount from './TokenCount'
@@ -47,15 +48,17 @@ interface Props {
let _text = ''
let _files: FileType[] = []
let _base: KnowledgeBase | undefined
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false)
const { addTopic, model, setModel } = useAssistant(assistant.id)
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id)
const {
sendMessageShortcut,
fontSize,
pasteLongTextAsFile,
pasteLongTextThreshold,
showInputEstimatedTokens,
clickAssistantToShowTopic,
language,
@@ -76,6 +79,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [spaceClickCount, setSpaceClickCount] = useState(0)
const spaceClickTimer = useRef<NodeJS.Timeout>()
const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
@@ -85,16 +89,19 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
[estimateTextTokens, showInputEstimatedTokens, text]
)
const newTopicShortcut = useShortcutDisplay('new_topic')
const inputEmpty = isEmpty(text.trim()) && files.length === 0
_text = text
_files = files
_base = selectedKnowledgeBase
const sendMessage = useCallback(async () => {
if (generating) {
return
}
if (isEmpty(text.trim())) {
if (inputEmpty) {
return
}
@@ -109,6 +116,10 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
status: 'success'
}
if (selectedKnowledgeBase) {
message.knowledgeBaseIds = [selectedKnowledgeBase.id]
}
if (files.length > 0) {
message.files = await FileManager.uploadFiles(files)
}
@@ -121,7 +132,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
setTimeout(() => resizeTextArea(), 0)
setExpend(false)
}, [assistant.id, assistant.topics, generating, files, text])
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
const translate = async () => {
if (isTranslating) {
@@ -183,6 +194,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
sendMessage()
return event.preventDefault()
}
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
sendMessage()
return event.preventDefault()
}
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
sendMessage()
return event.preventDefault()
}
}
const addNewTopic = useCallback(async () => {
@@ -280,7 +301,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const item = event.clipboardData?.items[0]
if (item && item.kind === 'string' && item.type === 'text/plain') {
item.getAsString(async (pasteText) => {
if (pasteText.length > 1500) {
if (pasteText.length > pasteLongTextThreshold) {
const tempFilePath = await window.api.file.create('pasted_text.txt')
await window.api.file.write(tempFilePath, pasteText)
const selectedFile = await window.api.file.get(tempFilePath)
@@ -292,7 +313,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
}
},
[pasteLongTextAsFile, supportExts, text]
[pasteLongTextAsFile, pasteLongTextThreshold, supportExts, text]
)
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
@@ -360,6 +381,12 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
}, [])
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
setSelectedKnowledgeBase(base)
}
return (
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
<AttachmentPreview files={files} setFiles={setFiles} />
@@ -372,7 +399,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
autoFocus
contextMenu="true"
variant="borderless"
rows={isBubbleStyle ? 2 : 1}
rows={textareaRows}
ref={textareaRef}
style={{ fontSize }}
styles={{ textarea: TextareaStyle }}
@@ -385,11 +412,22 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
/>
<Toolbar>
<ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: isMac ? '⌘' : 'Ctrl' })} arrow>
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<FormOutlined />
</ToolbarButton>
</Tooltip>
{isWebSearchModel(model) && (
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<ToolbarButton
type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
<GlobalOutlined
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
)}
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm
title={t('chat.input.clear.content')}
@@ -413,7 +451,19 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<KnowledgeBaseButton
selectedBase={selectedKnowledgeBase}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
<AttachmentButton
model={model}
files={files}
setFiles={setFiles}
ToolbarButton={ToolbarButton}
disabled={!!selectedKnowledgeBase}
/>
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />
@@ -443,7 +493,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton>
</Tooltip>
)}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
@@ -457,7 +507,7 @@ const Container = styled.div`
`
const InputBarContainer = styled.div`
border: 1px solid var(--color-border-soft);
border: 1px solid var(--color-border);
transition: all 0.3s ease;
position: relative;
margin: 0 20px 15px 20px;

View File

@@ -0,0 +1,83 @@
import { FileSearchOutlined } from '@ant-design/icons'
import { useAppSelector } from '@renderer/store'
import { KnowledgeBase } from '@renderer/types'
import { Button, Popover, Tooltip } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
selectedBase?: KnowledgeBase
onSelect: (base?: KnowledgeBase) => void
disabled?: boolean
ToolbarButton?: any
}
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
const { t } = useTranslation()
const knowledgeState = useAppSelector((state) => state.knowledge)
return (
<SelectorContainer>
{knowledgeState.bases.length === 0 ? (
<EmptyMessage>{t('knowledge.no_bases')}</EmptyMessage>
) : (
<>
{selectedBase && (
<Button type="link" block onClick={() => onSelect(undefined)} style={{ textAlign: 'left' }}>
{t('knowledge.clear_selection')}
</Button>
)}
{knowledgeState.bases.map((base) => (
<Button
key={base.id}
type={selectedBase?.id === base.id ? 'primary' : 'text'}
block
onClick={() => onSelect(base)}
style={{ textAlign: 'left' }}>
{base.name}
</Button>
))}
</>
)}
</SelectorContainer>
)
}
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, ToolbarButton }) => {
const { t } = useTranslation()
if (selectedBase) {
return (
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
<ToolbarButton type="text" onClick={() => onSelect(undefined)}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</Tooltip>
)
}
return (
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
<Popover
placement="top"
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
trigger="click">
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</Popover>
</Tooltip>
)
}
const SelectorContainer = styled.div`
max-height: 300px;
overflow-y: auto;
`
const EmptyMessage = styled.div`
padding: 8px;
`
export default KnowledgeBaseButton

View File

@@ -8,6 +8,7 @@ import styled from 'styled-components'
import Artifacts from './Artifacts'
import Mermaid from './Mermaid'
import SvgPreview from './SvgPreview'
interface CodeBlockProps {
children: string
@@ -79,8 +80,20 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
return <Mermaid chart={children} />
}
if (language === 'svg') {
return (
<CodeBlockWrapper className="code-block">
<CodeHeader>
<CodeLanguage>{'<SVG>'}</CodeLanguage>
<CopyButton text={children} />
</CodeHeader>
<SvgPreview>{children}</SvgPreview>
</CodeBlockWrapper>
)
}
return match ? (
<div className="code-block">
<CodeBlockWrapper className="code-block">
<CodeHeader>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{codeCollapsible && shouldShowExpandButton && (
@@ -118,7 +131,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
</CodeFooter>
)}
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
</div>
</CodeBlockWrapper>
) : (
<code className={className}>{children}</code>
)
@@ -142,6 +155,8 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
)
}
const CodeBlockWrapper = styled.div``
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
.shiki {
padding: 1em;

View File

@@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types'
import { escapeBrackets } from '@renderer/utils/formula'
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula'
import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -19,7 +19,7 @@ import ImagePreview from './ImagePreview'
import Link from './Link'
const ALLOWED_ELEMENTS =
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr)/i
/<(style|p|div|span|b|i|strong|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6]|blockquote|pre|code|br|hr|svg|path|circle|rect|line|polyline|polygon|text|g|defs|title|desc|tspan|sub|sup)/i
interface Props {
message: Message
@@ -35,7 +35,7 @@ const Markdown: FC<Props> = ({ message }) => {
const empty = isEmpty(message.content)
const paused = message.status === 'paused'
const content = empty && paused ? t('message.chat.completion.paused') : message.content
return escapeBrackets(content)
return removeSvgEmptyLines(escapeBrackets(content))
}, [message.content, message.status, t])
const rehypePlugins = useMemo(() => {
@@ -52,7 +52,6 @@ const Markdown: FC<Props> = ({ message }) => {
className="markdown"
rehypePlugins={rehypePlugins}
remarkPlugins={[remarkMath, remarkGfm]}
disallowedElements={mathEngine === 'KaTeX' ? ['style'] : []}
components={
{
a: Link,

View File

@@ -17,6 +17,7 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const mermaidId = `mermaid-popup-${Date.now()}`
const [activeTab, setActiveTab] = useState('preview')
const onOk = () => {
setOpen(false)
@@ -86,6 +87,11 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
}
}
const handleCopy = () => {
navigator.clipboard.writeText(chart)
window.message.success(t('message.copy.success'))
}
useEffect(() => {
window?.mermaid?.contentLoaded()
}, [])
@@ -101,11 +107,18 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
centered
footer={[
<Space key="download-buttons">
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
{activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>}
{activeTab === 'preview' && (
<>
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
</>
)}
</Space>
]}>
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key)}
items={[
{
key: 'preview',

View File

@@ -0,0 +1,16 @@
const SvgPreview = ({ children }: { children: string }) => {
return (
<div
dangerouslySetInnerHTML={{ __html: children }}
style={{
padding: '1em',
backgroundColor: 'white',
border: '0.5px solid var(--color-code-background)',
borderTopLeftRadius: 0,
borderTopRightRadius: 0
}}
/>
)
}
export default SvgPreview

View File

@@ -86,9 +86,12 @@ const MessageItem: FC<Props> = ({
}
useEffect(() => {
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler),
EventEmitter.on(EVENT_NAMES.RESEND_MESSAGE + ':' + message.id, onEditMessage)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [message])
}, [message, onEditMessage])
useEffect(() => {
if (message.role === 'user' && !message.usage) {
@@ -136,9 +139,11 @@ const MessageItem: FC<Props> = ({
if (message.type === 'clear') {
return (
<Divider dashed style={{ padding: '0 20px' }} plain>
{t('chat.message.new.context')}
</Divider>
<NewContextMessage onClick={() => EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)}>
<Divider dashed style={{ padding: '0 20px' }} plain>
{t('chat.message.new.context')}
</Divider>
</NewContextMessage>
)
}
@@ -176,6 +181,7 @@ const MessageItem: FC<Props> = ({
setModel={setModel}
onEditMessage={onEditMessage}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageFooter>
)}
@@ -228,4 +234,8 @@ const MessageFooter = styled.div`
gap: 20px;
`
const NewContextMessage = styled.div`
cursor: pointer;
`
export default memo(MessageItem)

View File

@@ -35,9 +35,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const getUserName = useCallback(() => {
if (isLocalAi && message.role !== 'user') return APP_NAME
if (message.role === 'assistant') return model?.name || model?.id || ''
if (message.role === 'assistant') return model?.name || model?.id || message.modelId || ''
return userName || t('common.you')
}, [message.role, model?.id, model?.name, t, userName])
}, [message.modelId, message.role, model?.id, model?.name, t, userName])
const isAssistantMessage = message.role === 'assistant'

View File

@@ -15,7 +15,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { translateText } from '@renderer/services/TranslateService'
import { Message, Model } from '@renderer/types'
import { removeTrailingDoubleSpaces } from '@renderer/utils'
import { Dropdown, Popconfirm, Tooltip } from 'antd'
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -31,6 +31,7 @@ interface Props {
setModel: (model: Model) => void
onEditMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => void
onGetMessages?: () => Message[]
}
const MessageMenubar: FC<Props> = (props) => {
@@ -43,7 +44,8 @@ const MessageMenubar: FC<Props> = (props) => {
assistantModel,
setModel,
onEditMessage,
onDeleteMessage
onDeleteMessage,
onGetMessages
} = props
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
@@ -75,10 +77,43 @@ const MessageMenubar: FC<Props> = (props) => {
})
}, [index, t])
const onResend = useCallback(() => {
const _messages = onGetMessages?.() || []
const index = _messages.findIndex((m) => m.id === message.id)
const nextIndex = index + 1
const nextMessage = _messages[nextIndex]
if (nextMessage && nextMessage.role === 'assistant') {
EventEmitter.emit(EVENT_NAMES.RESEND_MESSAGE + ':' + nextMessage.id, {
...nextMessage,
content: '',
status: 'sending',
modelId: assistantModel?.id || model?.id,
translatedContent: undefined
})
}
}, [assistantModel?.id, message.id, model?.id, onGetMessages])
const onEdit = useCallback(async () => {
const editedText = await TextEditPopup.show({ text: message.content })
let resendMessage = false
const editedText = await TextEditPopup.show({
text: message.content,
children: (props) => (
<ReSendButton
icon={<i className="iconfont icon-ic_send" style={{ color: 'var(--color-primary)' }} />}
onClick={() => {
props.onOk?.()
resendMessage = true
}}>
{t('chat.resend')}
</ReSendButton>
)
})
editedText && onEditMessage?.({ ...message, content: editedText })
}, [message, onEditMessage])
resendMessage && onResend()
}, [message, onEditMessage, onResend, t])
const handleTranslate = useCallback(
async (language: string) => {
@@ -133,7 +168,7 @@ const MessageMenubar: FC<Props> = (props) => {
onClick: () => handleTranslate('chinese')
},
{
label: '🇹🇼 ' + t('languages.chinese-traditional'),
label: '🇭🇰 ' + t('languages.chinese-traditional'),
key: 'translate-chinese-traditional',
onClick: () => handleTranslate('chinese-traditional')
},
@@ -205,13 +240,15 @@ const MessageMenubar: FC<Props> = (props) => {
destroyTooltipOnHide
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={onDeleteAndRegenerate}>
<ActionButton className="message-action-button">
<SyncOutlined />
</ActionButton>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button">
<SyncOutlined />
</ActionButton>
</Tooltip>
</Popconfirm>
)}
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
<i className="iconfont icon-at1"></i>
</ActionButton>
@@ -285,4 +322,10 @@ const ActionButton = styled.div`
}
`
const ReSendButton = styled(Button)`
position: absolute;
top: 10px;
left: 0;
`
export default MessageMenubar

View File

@@ -1,6 +1,7 @@
import { useRuntime } from '@renderer/hooks/useRuntime'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Message } from '@renderer/types'
import { t } from 'i18next'
import styled from 'styled-components'
const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ message, isLastMessage }) => {
@@ -27,9 +28,25 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
}
if (message.role === 'assistant') {
let metrixs = ''
let hasMetrics = false
if (message?.metrics?.completion_tokens && message?.metrics?.time_completion_millsec) {
hasMetrics = true
metrixs = t('settings.messages.metrics', {
time_first_token_millsec: message?.metrics?.time_first_token_millsec,
token_speed: (message?.metrics?.completion_tokens / (message?.metrics?.time_completion_millsec / 1000)).toFixed(
0
)
})
}
return (
<MessageMetadata className="message-tokens" onClick={locateMessage}>
Tokens: {message?.usage?.total_tokens} | {message?.usage?.prompt_tokens} | {message?.usage?.completion_tokens}
<MessageMetadata className={`message-tokens ${hasMetrics ? 'has-metrics' : ''}`} onClick={locateMessage}>
<span className="metrics">{metrixs}</span>
<span className="tokens">
Tokens: {message?.usage?.total_tokens} {message?.usage?.prompt_tokens} {message?.usage?.completion_tokens}
</span>
</MessageMetadata>
)
}
@@ -38,11 +55,30 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
}
const MessageMetadata = styled.div`
font-size: 12px;
font-size: 11px;
color: var(--color-text-2);
user-select: text;
margin: 2px 0;
cursor: pointer;
text-align: right;
.metrics {
display: none;
}
.tokens {
display: block;
}
&.has-metrics:hover {
.metrics {
display: block;
}
.tokens {
display: none;
}
}
`
export default MessgeTokens

View File

@@ -2,6 +2,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { getTopic, TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { getDefaultTopic } from '@renderer/services/AssistantService'
@@ -17,8 +18,10 @@ import { estimateHistoryTokens } from '@renderer/services/TokenService'
import { Assistant, Message, Model, Topic } from '@renderer/types'
import { captureScrollableDiv, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next'
import { flatten, last, reverse, take } from 'lodash'
import { flatten, last, take } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
import Suggestions from '../components/Suggestions'
@@ -31,13 +34,53 @@ interface Props {
setActiveTopic: (topic: Topic) => void
}
interface LoaderProps {
$loading: boolean
}
const LoaderContainer = styled.div<LoaderProps>`
display: flex;
justify-content: center;
padding: 10px;
width: 100%;
background: var(--color-background);
opacity: ${(props) => (props.$loading ? 1 : 0)};
transition: opacity 0.3s ease;
pointer-events: none;
`
const ScrollContainer = styled.div`
display: flex;
flex-direction: column-reverse;
`
interface ContainerProps {
right?: boolean
}
const Container = styled(Scrollbar)<ContainerProps>`
display: flex;
flex-direction: column-reverse;
padding: 10px 0;
padding-bottom: 20px;
overflow-x: hidden;
background-color: var(--color-background);
`
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
const [messages, setMessages] = useState<Message[]>([])
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
const [hasMore, setHasMore] = useState(true)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const messagesRef = useRef(messages)
const { updateTopic, addTopic } = useAssistant(assistant.id)
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings()
const messagesRef = useRef(messages)
const INITIAL_MESSAGES_COUNT = 20
const LOAD_MORE_COUNT = 20
messagesRef.current = messages
const maxWidth = useMemo(() => {
@@ -116,6 +159,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
setMessages([])
setDisplayMessages([])
const defaultTopic = getDefaultTopic(assistant.id)
updateTopic({ ...topic, name: defaultTopic.name, messages: [] })
TopicManager.clearTopicMessages(topic.id)
@@ -158,7 +202,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
setActiveTopic(newTopic)
autoRenameTopic()
// 由于复制了消,消息中附带的文件的总数变了,需要更新
// 由于复制了消<EFBFBD><EFBFBD><EFBFBD>,消息中附带的文件的总数变了,需要更新
const filesArr = branchMessages.map((m) => m.files)
const files = flatten(filesArr).filter(Boolean)
files.map(async (f) => {
@@ -197,7 +241,39 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
})
}, [assistant, messages])
const memoizedMessages = useMemo(() => reverse([...messages]), [messages])
// 初始化显示最新的消息
useEffect(() => {
if (messages.length > 0) {
const reversedMessages = [...messages].reverse()
setDisplayMessages(reversedMessages.slice(0, INITIAL_MESSAGES_COUNT))
setHasMore(messages.length > INITIAL_MESSAGES_COUNT)
}
}, [messages])
// 加载更多历史消息
const loadMoreMessages = useCallback(() => {
if (!hasMore || isLoadingMore) return
setIsLoadingMore(true)
setTimeout(() => {
const currentLength = displayMessages.length
const reversedMessages = [...messages].reverse()
const moreMessages = reversedMessages.slice(currentLength, currentLength + LOAD_MORE_COUNT)
setDisplayMessages((prev) => [...prev, ...moreMessages])
setHasMore(currentLength + LOAD_MORE_COUNT < messages.length)
setIsLoadingMore(false)
}, 300)
}, [displayMessages, hasMore, isLoadingMore, messages])
useShortcut('copy_last_message', () => {
const lastMessage = last(messages)
if (lastMessage) {
navigator.clipboard.writeText(lastMessage.content)
window.message.success(t('message.copy.success'))
}
})
return (
<Container
@@ -207,30 +283,34 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
ref={containerRef}
right={topicPosition === 'left'}>
<Suggestions assistant={assistant} messages={messages} />
{memoizedMessages.map((message, index) => (
<MessageItem
key={message.id}
message={message}
topic={topic}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
))}
<InfiniteScroll
dataLength={displayMessages.length}
next={loadMoreMessages}
hasMore={hasMore}
loader={null}
inverse={true}
scrollableTarget="messages">
<ScrollContainer>
<LoaderContainer $loading={isLoadingMore}>
<BeatLoader size={8} color="var(--color-text-2)" />
</LoaderContainer>
{displayMessages.map((message, index) => (
<MessageItem
key={message.id}
message={message}
topic={topic}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
))}
</ScrollContainer>
</InfiniteScroll>
<Prompt assistant={assistant} key={assistant.prompt} />
</Container>
)
}
const Container = styled(Scrollbar)`
display: flex;
flex-direction: column-reverse;
padding: 10px 0;
padding-bottom: 20px;
overflow-x: hidden;
background-color: var(--color-background);
`
export default Messages

View File

@@ -1,4 +1,4 @@
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'

View File

@@ -1,12 +1,15 @@
import { SearchOutlined } from '@ant-design/icons'
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import { HStack } from '@renderer/components/Layout'
import AppStorePopover from '@renderer/components/Popups/AppStorePopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
@@ -25,26 +28,38 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
const { topicPosition } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
useShortcut('toggle_show_assistants', () => {
toggleShowAssistants()
})
useShortcut('toggle_show_topics', () => {
if (topicPosition === 'right') {
toggleShowTopics()
} else {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
})
return (
<Navbar>
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
<i className="iconfont icon-hide-sidebar" />
</NewButton>
<NewButton onClick={() => SearchPopup.show()}>
<SearchOutlined />
</NewButton>
</NavbarIcon>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
<FormOutlined />
</NavbarIcon>
</NavbarLeft>
)}
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
<HStack alignItems="center">
{!showAssistants && (
<NewButton
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
<i className="iconfont icon-show-sidebar" />
</NewButton>
</NavbarIcon>
)}
<TitleText
style={{ marginRight: 10, cursor: 'pointer' }}
@@ -55,10 +70,18 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center">
<NavbarIcon onClick={() => SearchPopup.show()}>
<SearchOutlined />
</NavbarIcon>
<AppStorePopover>
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}>
<i className="iconfont icon-appstore" />
</NavbarIcon>
</AppStorePopover>
{topicPosition === 'right' && (
<NewButton onClick={toggleShowTopics}>
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}>
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
</NewButton>
</NavbarIcon>
)}
</HStack>
</NavbarRight>
@@ -66,7 +89,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
)
}
export const NewButton = styled.div`
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
@@ -86,6 +109,9 @@ export const NewButton = styled.div`
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
@@ -101,6 +127,7 @@ const TitleText = styled.span`
margin-left: 5px;
font-family: Ubuntu;
font-size: 13px;
user-select: none;
`
export default HeaderNavbar

View File

@@ -1,11 +1,11 @@
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
import DragableList from '@renderer/components/DragableList'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppSelector } from '@renderer/store'
@@ -175,6 +175,7 @@ const Container = styled(Scrollbar)`
display: flex;
flex-direction: column;
padding-top: 11px;
user-select: none;
`
const AssistantItem = styled.div`

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