Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bc1f4b640 | ||
|
|
ccb216e76a | ||
|
|
60931b85ff | ||
|
|
dc1dbc7bb6 | ||
|
|
5d2efbd62b | ||
|
|
5337017648 | ||
|
|
c409256ae9 | ||
|
|
4ac608052c | ||
|
|
5e6aaabb23 | ||
|
|
8812daeeee | ||
|
|
13e3a8478c | ||
|
|
8687985ccb | ||
|
|
7d54f9b4fa | ||
|
|
6b7ba35183 | ||
|
|
5b42a6d054 | ||
|
|
153e7a9299 | ||
|
|
77e0c5172e | ||
|
|
c50ac440c8 | ||
|
|
34ebab0af8 | ||
|
|
b85765915e | ||
|
|
960f50e4e4 | ||
|
|
65e19d187c | ||
|
|
aa4f94f8a4 | ||
|
|
aa3812eddc | ||
|
|
6b9e58171b | ||
|
|
2f64653b1e | ||
|
|
03dd3038e0 | ||
|
|
f1f7e8e11b | ||
|
|
fbd189c5e1 | ||
|
|
87c3716f75 | ||
|
|
37477587b6 | ||
|
|
d558572d97 | ||
|
|
7506d04c55 | ||
|
|
35fd5aef22 | ||
|
|
8f11d2b1c9 | ||
|
|
9aa2a4727d | ||
|
|
ca6027dd83 | ||
|
|
c2462fd51c | ||
|
|
0739758469 | ||
|
|
b2554333a9 | ||
|
|
6ced973b35 | ||
|
|
ccbeefc546 | ||
|
|
7fdc2db522 | ||
|
|
978f1342e4 | ||
|
|
ff935a656e | ||
|
|
15539a5609 | ||
|
|
88cd4f2144 | ||
|
|
daf2e035b2 | ||
|
|
7ceb4920ec | ||
|
|
0074d5c8b4 | ||
|
|
96737ed695 | ||
|
|
356da1ea67 | ||
|
|
debf996146 | ||
|
|
8d73d1e844 | ||
|
|
b0d777293b | ||
|
|
1a9fbbc0a2 | ||
|
|
ab99a7b96d | ||
|
|
7d561dbfb7 | ||
|
|
6af07c278d | ||
|
|
9c18b851cc | ||
|
|
b1ebe13b5f | ||
|
|
9b258734c4 | ||
|
|
25eb97902b | ||
|
|
2fae6e4a3e | ||
|
|
f312c5fc40 | ||
|
|
afa96549a3 | ||
|
|
6beee78ce8 |
30
.github/workflows/release.yml
vendored
@@ -2,11 +2,6 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Version (e.g. v1.2.3)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*.*.*
|
- v*.*.*
|
||||||
@@ -34,18 +29,37 @@ jobs:
|
|||||||
- name: Install corepack
|
- name: Install corepack
|
||||||
run: corepack enable && corepack prepare yarn@4.3.1 --activate
|
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
|
- name: Install Dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: Build Linux
|
- name: Build Linux
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: yarn build:linux
|
run: |
|
||||||
|
yarn build:npm linux
|
||||||
|
yarn build:linux
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Build Mac
|
- name: Build Mac
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: yarn build:mac
|
run: |
|
||||||
|
yarn build:npm mac
|
||||||
|
yarn build:mac
|
||||||
env:
|
env:
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
@@ -61,7 +75,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
|
||||||
- name: Replace spaces in filenames
|
- name: Replace spaces in filenames
|
||||||
run: node scripts/replaceSpaces.js
|
run: node scripts/replace-spaces.js
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -36,6 +36,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
out
|
out
|
||||||
build/icons
|
build/icons
|
||||||
|
stats.html
|
||||||
|
|
||||||
# ENV
|
# ENV
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -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}`);
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
17
.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch
Normal 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;
|
||||||
@@ -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 {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/core.js b/core.js
|
diff --git a/core.js b/core.js
|
||||||
index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d82573ea5 100644
|
index 30c91e66bf595a66c09eb3dbcbda7d58154865f5..b511ff24ea1891904c60174c6ed26ecdd4d5ac51 100644
|
||||||
--- a/core.js
|
--- a/core.js
|
||||||
+++ b/core.js
|
+++ b/core.js
|
||||||
@@ -156,7 +156,7 @@ class APIClient {
|
@@ -156,7 +156,7 @@ class APIClient {
|
||||||
@@ -12,7 +12,7 @@ index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
diff --git a/core.mjs b/core.mjs
|
diff --git a/core.mjs b/core.mjs
|
||||||
index 8bc7a0ee10d61560d7113cf3f703355bb19f7ddd..5e4c8586ea6b13fe887a22af2de05eaa4700b5ec 100644
|
index ac267bcfcff44b1f7c9bea5513bba94726a31795..dd5bd9f29609d3f0eea4bd5b225f302893df14ad 100644
|
||||||
--- a/core.mjs
|
--- a/core.mjs
|
||||||
+++ b/core.mjs
|
+++ b/core.mjs
|
||||||
@@ -149,7 +149,7 @@ export class APIClient {
|
@@ -149,7 +149,7 @@ export class APIClient {
|
||||||
29
.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch
Normal 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;
|
||||||
|
|
||||||
@@ -11,8 +11,25 @@ files:
|
|||||||
- '!src'
|
- '!src'
|
||||||
- '!scripts'
|
- '!scripts'
|
||||||
- '!local'
|
- '!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:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
|
- '**/*.{node,dll,metal,exp,lib}'
|
||||||
win:
|
win:
|
||||||
executableName: Cherry Studio
|
executableName: Cherry Studio
|
||||||
nsis:
|
nsis:
|
||||||
@@ -24,12 +41,12 @@ nsis:
|
|||||||
oneClick: false
|
oneClick: false
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
notarize: false
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
|
||||||
target:
|
target:
|
||||||
- target: dmg
|
- target: dmg
|
||||||
arch:
|
arch:
|
||||||
@@ -47,27 +64,24 @@ linux:
|
|||||||
arch:
|
arch:
|
||||||
- arm64
|
- arm64
|
||||||
- x64
|
- x64
|
||||||
# - snap
|
|
||||||
# - deb
|
|
||||||
maintainer: electronjs.org
|
maintainer: electronjs.org
|
||||||
category: Utility
|
category: Utility
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||||
npmRebuild: false
|
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: generic
|
||||||
url: https://cherrystudio.ocool.online
|
url: https://cherrystudio.ocool.online
|
||||||
electronDownload:
|
electronDownload:
|
||||||
mirror: https://npmmirror.com/mirrors/electron/
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
|
afterPack: scripts/after-pack.js
|
||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
增加快捷键切换助手和话题显示
|
Azure OpenAI 嵌入模型支持
|
||||||
历史消息懒加载 by @1355873789
|
增加嵌入模型专用服务商 JINA
|
||||||
更快的应用更新下载速度 by @1355873789
|
支持选择图片和文件后不输入内容即可发送
|
||||||
更加清晰的模型分组
|
增加 GitHub Copilot 小程序
|
||||||
修复部分代码块无法正常显示问题
|
模型编辑支持显示模型 ID
|
||||||
增加应用更新内容显示
|
模型选择按钮增加服务商显示
|
||||||
消息发送增加 Ctrl + Enter 快捷键
|
修复话题移动到不同的助手后搜索结果不能定位问题
|
||||||
清除上下文消息点击可以撤销
|
修复输出包含 sub 无法正常显示问题
|
||||||
增加 Top-P 设置选项
|
|
||||||
|
|||||||
@@ -1,23 +1,48 @@
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
import { resolve } from 'path'
|
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({
|
export default defineConfig({
|
||||||
main: {
|
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: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@main': resolve('src/main'),
|
'@main': resolve('src/main'),
|
||||||
'@types': resolve('src/renderer/src/types'),
|
'@types': resolve('src/renderer/src/types'),
|
||||||
'@shared': resolve('packages/shared')
|
'@shared': resolve('packages/shared')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['@libsql/client']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin()]
|
plugins: [externalizeDepsPlugin()]
|
||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
plugins: [react()],
|
plugins: [react(), ...visualizerPlugin('renderer')],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src'),
|
'@renderer': resolve('src/renderer/src'),
|
||||||
@@ -25,7 +50,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ['chunk-7UIZINC5.js', 'chunk-7OJJKI46.js']
|
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
56
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "0.8.24",
|
"version": "0.9.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -11,9 +11,11 @@
|
|||||||
"local",
|
"local",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"nohoist": [
|
"installConfig": {
|
||||||
|
"hoistingLimits": [
|
||||||
"packages/database"
|
"packages/database"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
@@ -23,23 +25,44 @@
|
|||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
|
"build:check": "yarn typecheck",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||||
"build:win": "dotenv npm run build && electron-builder --win --publish never",
|
"build:win": "dotenv npm run build && electron-builder --win",
|
||||||
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
|
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||||
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
|
"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",
|
"release": "node scripts/version.js",
|
||||||
"publish": "yarn release patch push",
|
"publish": "yarn release patch push",
|
||||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
"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": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"@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",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
"apache-arrow": "^18.1.0",
|
||||||
"docx": "^9.0.2",
|
"docx": "^9.0.2",
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
@@ -49,7 +72,7 @@
|
|||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"officeparser": "^4.1.1",
|
"officeparser": "^4.1.1",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"tokenx": "^0.4.1",
|
||||||
"webdav": "4.11.4"
|
"webdav": "4.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -57,7 +80,7 @@
|
|||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||||
"@electron-toolkit/tsconfig": "^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",
|
"@hello-pangea/dnd": "^16.6.0",
|
||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
@@ -68,20 +91,21 @@
|
|||||||
"@types/node": "^18.19.9",
|
"@types/node": "^18.19.9",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@types/tinycolor2": "^1",
|
"@types/tinycolor2": "^1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"antd": "^5.18.3",
|
"antd": "^5.22.5",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"electron": "^28.3.3",
|
"electron": "31.7.6",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.3.0",
|
||||||
"emittery": "^1.0.3",
|
"emittery": "^1.0.3",
|
||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
@@ -89,16 +113,16 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
"gpt-tokens": "^1.3.10",
|
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime": "^4.0.4",
|
"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",
|
"prettier": "^3.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hotkeys-hook": "^4.6.1",
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
@@ -111,6 +135,7 @@
|
|||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.77.2",
|
"sass": "^1.77.2",
|
||||||
"shiki": "^1.22.2",
|
"shiki": "^1.22.2",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
@@ -124,7 +149,8 @@
|
|||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"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"
|
"packageManager": "yarn@4.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
45
scripts/after-pack.js
Normal 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
@@ -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)
|
||||||
@@ -7,7 +7,7 @@ const config = {
|
|||||||
VERSION_DB: 'versions.json',
|
VERSION_DB: 'versions.json',
|
||||||
LOG_FILE: 'logs.json',
|
LOG_FILE: 'logs.json',
|
||||||
MAX_LOGS: 1000 // 最多保存多少条日志
|
MAX_LOGS: 1000 // 最多保存多少条日志
|
||||||
};
|
}
|
||||||
|
|
||||||
// Worker 入口函数
|
// Worker 入口函数
|
||||||
const worker = {
|
const worker = {
|
||||||
@@ -19,61 +19,67 @@ const worker = {
|
|||||||
// 定时器执行函数 - 只负责检查和更新
|
// 定时器执行函数 - 只负责检查和更新
|
||||||
async scheduled(event, env, ctx) {
|
async scheduled(event, env, ctx) {
|
||||||
try {
|
try {
|
||||||
await initDataFiles(env);
|
await initDataFiles(env)
|
||||||
console.log('开始定时检查新版本...');
|
console.log('开始定时检查新版本...')
|
||||||
// 使用新的 checkNewRelease 函数
|
// 使用新的 checkNewRelease 函数
|
||||||
await checkNewRelease(env);
|
await checkNewRelease(env)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('定时任务执行失败:', error);
|
console.error('定时任务执行失败:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// HTTP 请求处理函数 - 只负责返回数据
|
// HTTP 请求处理函数 - 只负责返回数据
|
||||||
async fetch(request, env, ctx) {
|
async fetch(request, env, ctx) {
|
||||||
if (!env || !env.R2_BUCKET) {
|
if (!env || !env.R2_BUCKET) {
|
||||||
return new Response(JSON.stringify({
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
error: 'R2 存储桶未正确配置'
|
error: 'R2 存储桶未正确配置'
|
||||||
}), {
|
}),
|
||||||
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url)
|
||||||
const filename = url.pathname.slice(1);
|
const filename = url.pathname.slice(1)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 处理文件下载请求
|
// 处理文件下载请求
|
||||||
if (filename) {
|
if (filename) {
|
||||||
return await handleDownload(env, filename);
|
return await handleDownload(env, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只返回缓存的版本信息
|
// 只返回缓存的版本信息
|
||||||
return await getCachedRelease(env);
|
return await getCachedRelease(env)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return new Response(JSON.stringify({
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
}), {
|
}),
|
||||||
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default worker;
|
export default worker
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加日志记录函数
|
* 添加日志记录函数
|
||||||
*/
|
*/
|
||||||
async function addLog(env, type, event, details = null) {
|
async function addLog(env, type, event, details = null) {
|
||||||
try {
|
try {
|
||||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
|
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
|
||||||
let logs = { logs: [] };
|
let logs = { logs: [] }
|
||||||
|
|
||||||
if (logFile) {
|
if (logFile) {
|
||||||
logs = JSON.parse(await logFile.text());
|
logs = JSON.parse(await logFile.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
logs.logs.unshift({
|
logs.logs.unshift({
|
||||||
@@ -81,16 +87,16 @@ async function addLog(env, type, event, details = null) {
|
|||||||
type,
|
type,
|
||||||
event,
|
event,
|
||||||
details
|
details
|
||||||
});
|
})
|
||||||
|
|
||||||
// 保持日志数量在限制内
|
// 保持日志数量在限制内
|
||||||
if (logs.logs.length > config.MAX_LOGS) {
|
if (logs.logs.length > config.MAX_LOGS) {
|
||||||
logs.logs = logs.logs.slice(0, config.MAX_LOGS);
|
logs.logs = logs.logs.slice(0, config.MAX_LOGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2));
|
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('写入日志失败:', error);
|
console.error('写入日志失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,90 +105,93 @@ async function addLog(env, type, event, details = null) {
|
|||||||
*/
|
*/
|
||||||
async function getLatestRelease(env) {
|
async function getLatestRelease(env) {
|
||||||
try {
|
try {
|
||||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
// 如果缓存不存在,先检查版本数据库
|
// 如果缓存不存在,先检查版本数据库
|
||||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
if (versionDB) {
|
if (versionDB) {
|
||||||
const versions = JSON.parse(await versionDB.text());
|
const versions = JSON.parse(await versionDB.text())
|
||||||
if (versions.latestVersion) {
|
if (versions.latestVersion) {
|
||||||
// 从版本数据库重建缓存
|
// 从版本数据库重建缓存
|
||||||
const latestVersion = versions.versions[versions.latestVersion];
|
const latestVersion = versions.versions[versions.latestVersion]
|
||||||
const cacheData = {
|
const cacheData = {
|
||||||
version: latestVersion.version,
|
version: latestVersion.version,
|
||||||
publishedAt: latestVersion.publishedAt,
|
publishedAt: latestVersion.publishedAt,
|
||||||
changelog: latestVersion.changelog,
|
changelog: latestVersion.changelog,
|
||||||
downloads: latestVersion.files
|
downloads: latestVersion.files
|
||||||
.filter(file => file.uploaded)
|
.filter((file) => file.uploaded)
|
||||||
.map(file => ({
|
.map((file) => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||||
size: formatFileSize(file.size)
|
size: formatFileSize(file.size)
|
||||||
}))
|
}))
|
||||||
};
|
}
|
||||||
// 更新缓存
|
// 更新缓存
|
||||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||||
return new Response(JSON.stringify(cacheData), {
|
return new Response(JSON.stringify(cacheData), {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果版本数据库也没有数据,才执行检查更新
|
// 如果版本数据库也没有数据,才执行检查更新
|
||||||
const data = await checkNewRelease(env);
|
const data = await checkNewRelease(env)
|
||||||
return new Response(JSON.stringify(data), {
|
return new Response(JSON.stringify(data), {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await cached.text();
|
const data = await cached.text()
|
||||||
return new Response(data, {
|
return new Response(data, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await addLog(env, 'ERROR', '获取版本信息失败', error.message);
|
await addLog(env, 'ERROR', '获取版本信息失败', error.message)
|
||||||
return new Response(JSON.stringify({
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
error: '获取版本信息失败: ' + error.message,
|
error: '获取版本信息失败: ' + error.message,
|
||||||
detail: '请稍<E8AFB7><E7A88D><EFBFBD>再试'
|
detail: '请稍<E8AFB7><E7A88D><EFBFBD>再试'
|
||||||
}), {
|
}),
|
||||||
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改下载处理函数,直接接收 env
|
// 修改下载处理函数,直接接收 env
|
||||||
async function handleDownload(env, filename) {
|
async function handleDownload(env, filename) {
|
||||||
try {
|
try {
|
||||||
const object = await env.R2_BUCKET.get(filename);
|
const object = await env.R2_BUCKET.get(filename)
|
||||||
|
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return new Response('文件未找到', { status: 404 });
|
return new Response('文件未找到', { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应头
|
// 设置响应头
|
||||||
const headers = new Headers();
|
const headers = new Headers()
|
||||||
object.writeHttpMetadata(headers);
|
object.writeHttpMetadata(headers)
|
||||||
headers.set('etag', object.httpEtag);
|
headers.set('etag', object.httpEtag)
|
||||||
headers.set('Content-Disposition', `attachment; filename="${filename}"`);
|
headers.set('Content-Disposition', `attachment; filename="${filename}"`)
|
||||||
|
|
||||||
return new Response(object.body, {
|
return new Response(object.body, {
|
||||||
headers
|
headers
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载文件时发生错误:', error);
|
console.error('下载文件时发生错误:', error)
|
||||||
return new Response('获取文件失败', { status: 500 });
|
return new Response('获取文件失败', { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,15 +199,15 @@ async function handleDownload(env, filename) {
|
|||||||
* 根据文件扩展名获取对应的 Content-Type
|
* 根据文件扩展名获取对应的 Content-Type
|
||||||
*/
|
*/
|
||||||
function getContentType(filename) {
|
function getContentType(filename) {
|
||||||
const ext = filename.split('.').pop().toLowerCase();
|
const ext = filename.split('.').pop().toLowerCase()
|
||||||
const types = {
|
const types = {
|
||||||
'exe': 'application/x-msdownload', // Windows 可执行文件
|
exe: 'application/x-msdownload', // Windows 可执行文件
|
||||||
'dmg': 'application/x-apple-diskimage', // macOS 安装包
|
dmg: 'application/x-apple-diskimage', // macOS 安装包
|
||||||
'zip': 'application/zip', // 压缩包
|
zip: 'application/zip', // 压缩包
|
||||||
'AppImage': 'application/x-executable', // Linux 可执行文件
|
AppImage: 'application/x-executable', // Linux 可执行文件
|
||||||
'blockmap': 'application/octet-stream' // 更新文件
|
blockmap: 'application/octet-stream' // 更新文件
|
||||||
};
|
}
|
||||||
return types[ext] || 'application/octet-stream';
|
return types[ext] || 'application/octet-stream'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,16 +215,16 @@ function getContentType(filename) {
|
|||||||
* 将字节转换为人类可读的格式(B, KB, MB, GB)
|
* 将字节转换为人类可读的格式(B, KB, MB, GB)
|
||||||
*/
|
*/
|
||||||
function formatFileSize(bytes) {
|
function formatFileSize(bytes) {
|
||||||
const units = ['B', 'KB', 'MB', 'GB'];
|
const units = ['B', 'KB', 'MB', 'GB']
|
||||||
let size = bytes;
|
let size = bytes
|
||||||
let unitIndex = 0;
|
let unitIndex = 0
|
||||||
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
size /= 1024;
|
size /= 1024
|
||||||
unitIndex++;
|
unitIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,19 +232,19 @@ function formatFileSize(bytes) {
|
|||||||
* 用于对版本号进行排序
|
* 用于对版本号进行排序
|
||||||
*/
|
*/
|
||||||
function compareVersions(a, b) {
|
function compareVersions(a, b) {
|
||||||
const partsA = a.replace('v', '').split('.');
|
const partsA = a.replace('v', '').split('.')
|
||||||
const partsB = b.replace('v', '').split('.');
|
const partsB = b.replace('v', '').split('.')
|
||||||
|
|
||||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||||
const numA = parseInt(partsA[i] || 0);
|
const numA = parseInt(partsA[i] || 0)
|
||||||
const numB = parseInt(partsB[i] || 0);
|
const numB = parseInt(partsB[i] || 0)
|
||||||
|
|
||||||
if (numA !== numB) {
|
if (numA !== numB) {
|
||||||
return numA - numB;
|
return numA - numB
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,78 +253,83 @@ function compareVersions(a, b) {
|
|||||||
async function initDataFiles(env) {
|
async function initDataFiles(env) {
|
||||||
try {
|
try {
|
||||||
// 检查并初始化版本数据库
|
// 检查并初始化版本数据库
|
||||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
if (!versionDB) {
|
if (!versionDB) {
|
||||||
const initialVersions = {
|
const initialVersions = {
|
||||||
versions: {},
|
versions: {},
|
||||||
latestVersion: null,
|
latestVersion: null,
|
||||||
lastChecked: new Date().toISOString()
|
lastChecked: new Date().toISOString()
|
||||||
};
|
}
|
||||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2));
|
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2))
|
||||||
await addLog(env, 'INFO', 'versions.json 初始化成功');
|
await addLog(env, 'INFO', 'versions.json 初始化成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查并初始化日志文件
|
// 检查并初始化日志文件
|
||||||
const logFile = await env.R2_BUCKET.get(config.LOG_FILE);
|
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
|
||||||
if (!logFile) {
|
if (!logFile) {
|
||||||
const initialLogs = {
|
const initialLogs = {
|
||||||
logs: [{
|
logs: [
|
||||||
|
{
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
type: 'INFO',
|
type: 'INFO',
|
||||||
event: '系统初始化'
|
event: '系统初始化'
|
||||||
}]
|
}
|
||||||
};
|
]
|
||||||
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2));
|
}
|
||||||
console.log('logs.json 初始化成功');
|
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2))
|
||||||
|
console.log('logs.json 初始化成功')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化数据文件失败:', error);
|
console.error('初始化数据文件失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:只获取缓存的版本信息
|
// 新增:只获取缓存的版本信息
|
||||||
async function getCachedRelease(env) {
|
async function getCachedRelease(env) {
|
||||||
try {
|
try {
|
||||||
const cached = await env.R2_BUCKET.get(config.CACHE_KEY);
|
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
// 如果缓存不存在,从版本数据库获取
|
// 如果缓存不存在,从版本数据库获取
|
||||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
if (versionDB) {
|
if (versionDB) {
|
||||||
const versions = JSON.parse(await versionDB.text());
|
const versions = JSON.parse(await versionDB.text())
|
||||||
if (versions.latestVersion) {
|
if (versions.latestVersion) {
|
||||||
const latestVersion = versions.versions[versions.latestVersion];
|
const latestVersion = versions.versions[versions.latestVersion]
|
||||||
const cacheData = {
|
const cacheData = {
|
||||||
version: latestVersion.version,
|
version: latestVersion.version,
|
||||||
publishedAt: latestVersion.publishedAt,
|
publishedAt: latestVersion.publishedAt,
|
||||||
changelog: latestVersion.changelog,
|
changelog: latestVersion.changelog,
|
||||||
downloads: latestVersion.files
|
downloads: latestVersion.files
|
||||||
.filter(file => file.uploaded)
|
.filter((file) => file.uploaded)
|
||||||
.map(file => ({
|
.map((file) => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||||
size: formatFileSize(file.size)
|
size: formatFileSize(file.size)
|
||||||
}))
|
}))
|
||||||
};
|
}
|
||||||
// 重建缓存
|
// 重建缓存
|
||||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||||
return new Response(JSON.stringify(cacheData), {
|
return new Response(JSON.stringify(cacheData), {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果没有任何数据,返回错误
|
// 如果没有任何数据,返回错误
|
||||||
return new Response(JSON.stringify({
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
error: '没有可用的版本信息'
|
error: '没有可用的版本信息'
|
||||||
}), {
|
}),
|
||||||
|
{
|
||||||
status: 404,
|
status: 404,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回缓存数据
|
// 返回缓存数据
|
||||||
@@ -324,10 +338,10 @@ async function getCachedRelease(env) {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message);
|
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,31 +350,31 @@ async function checkNewRelease(env) {
|
|||||||
try {
|
try {
|
||||||
// 获取 GitHub 最新版本
|
// 获取 GitHub 最新版本
|
||||||
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
|
||||||
headers: { 'User-Agent': 'CloudflareWorker' },
|
headers: { 'User-Agent': 'CloudflareWorker' }
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!githubResponse.ok) {
|
if (!githubResponse.ok) {
|
||||||
throw new Error('GitHub API 请求失败');
|
throw new Error('GitHub API 请求失败')
|
||||||
}
|
}
|
||||||
|
|
||||||
const releaseData = await githubResponse.json();
|
const releaseData = await githubResponse.json()
|
||||||
const version = releaseData.tag_name;
|
const version = releaseData.tag_name
|
||||||
|
|
||||||
// 获取版本数据库
|
// 获取版本数据库
|
||||||
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB);
|
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
|
||||||
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() };
|
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() }
|
||||||
|
|
||||||
if (versionDB) {
|
if (versionDB) {
|
||||||
versions = JSON.parse(await versionDB.text());
|
versions = JSON.parse(await versionDB.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除版本检查,改为记录是否有文件更新的标志
|
// 移除版本检查,改为记录是否有文件更新的标志
|
||||||
let hasUpdates = false;
|
let hasUpdates = false
|
||||||
if (versions.latestVersion !== version) {
|
if (versions.latestVersion !== version) {
|
||||||
await addLog(env, 'INFO', `发现新版本: ${version}`);
|
await addLog(env, 'INFO', `发现新版本: ${version}`)
|
||||||
hasUpdates = true;
|
hasUpdates = true
|
||||||
} else {
|
} else {
|
||||||
await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`);
|
await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备新版本记录
|
// 准备新版本记录
|
||||||
@@ -368,60 +382,60 @@ async function checkNewRelease(env) {
|
|||||||
version,
|
version,
|
||||||
publishedAt: releaseData.published_at,
|
publishedAt: releaseData.published_at,
|
||||||
uploadedAt: null,
|
uploadedAt: null,
|
||||||
files: releaseData.assets.map(asset => ({
|
files: releaseData.assets.map((asset) => ({
|
||||||
name: asset.name,
|
name: asset.name,
|
||||||
size: asset.size,
|
size: asset.size,
|
||||||
uploaded: false
|
uploaded: false
|
||||||
})),
|
})),
|
||||||
changelog: releaseData.body
|
changelog: releaseData.body
|
||||||
};
|
}
|
||||||
|
|
||||||
// 检查并上传文件
|
// 检查并上传文件
|
||||||
for (const asset of releaseData.assets) {
|
for (const asset of releaseData.assets) {
|
||||||
try {
|
try {
|
||||||
const existingFile = await env.R2_BUCKET.get(asset.name);
|
const existingFile = await env.R2_BUCKET.get(asset.name)
|
||||||
// 检查文件是否存在且大小是否一致
|
// 检查文件是否存在且大小是否一致
|
||||||
if (!existingFile || existingFile.size !== asset.size) {
|
if (!existingFile || existingFile.size !== asset.size) {
|
||||||
hasUpdates = true;
|
hasUpdates = true
|
||||||
const response = await fetch(asset.browser_download_url);
|
const response = await fetch(asset.browser_download_url)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`下载失败: HTTP ${response.status}`);
|
throw new Error(`下载失败: HTTP ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await response.arrayBuffer();
|
const file = await response.arrayBuffer()
|
||||||
await env.R2_BUCKET.put(asset.name, file, {
|
await env.R2_BUCKET.put(asset.name, file, {
|
||||||
httpMetadata: { contentType: getContentType(asset.name) }
|
httpMetadata: { contentType: getContentType(asset.name) }
|
||||||
});
|
})
|
||||||
|
|
||||||
// 更新文件状态
|
// 更新文件状态
|
||||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
|
||||||
if (fileIndex !== -1) {
|
if (fileIndex !== -1) {
|
||||||
versionRecord.files[fileIndex].uploaded = true;
|
versionRecord.files[fileIndex].uploaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
await addLog(env, 'INFO', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`);
|
await addLog(env, 'INFO', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`)
|
||||||
} else {
|
} else {
|
||||||
// 文件存在且大小相同,标记为已上传
|
// 文件存在且大小相同,标记为已上传
|
||||||
const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name);
|
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
|
||||||
if (fileIndex !== -1) {
|
if (fileIndex !== -1) {
|
||||||
versionRecord.files[fileIndex].uploaded = true;
|
versionRecord.files[fileIndex].uploaded = true
|
||||||
}
|
}
|
||||||
await addLog(env, 'INFO', `文件完整性验证通过: ${asset.name}`);
|
await addLog(env, 'INFO', `文件完整性验证通过: ${asset.name}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message);
|
await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只有在有更新或是新版本时才更新数据库和缓存
|
// 只有在有更新或是新版本时才更新数据库和缓存
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
// 更新版本记录
|
// 更新版本记录
|
||||||
versionRecord.uploadedAt = new Date().toISOString();
|
versionRecord.uploadedAt = new Date().toISOString()
|
||||||
versions.versions[version] = versionRecord;
|
versions.versions[version] = versionRecord
|
||||||
versions.latestVersion = version;
|
versions.latestVersion = version
|
||||||
|
|
||||||
// 保存版本数据库
|
// 保存版本数据库
|
||||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
|
||||||
|
|
||||||
// 更新缓存
|
// 更新缓存
|
||||||
const cacheData = {
|
const cacheData = {
|
||||||
@@ -429,87 +443,87 @@ async function checkNewRelease(env) {
|
|||||||
publishedAt: releaseData.published_at,
|
publishedAt: releaseData.published_at,
|
||||||
changelog: releaseData.body,
|
changelog: releaseData.body,
|
||||||
downloads: versionRecord.files
|
downloads: versionRecord.files
|
||||||
.filter(file => file.uploaded)
|
.filter((file) => file.uploaded)
|
||||||
.map(file => ({
|
.map((file) => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
|
||||||
size: formatFileSize(file.size)
|
size: formatFileSize(file.size)
|
||||||
}))
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData));
|
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
|
||||||
await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成');
|
await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成')
|
||||||
|
|
||||||
// 清理旧版本
|
// 清理旧版本
|
||||||
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a));
|
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a))
|
||||||
if (versionList.length > 2) {
|
if (versionList.length > 2) {
|
||||||
// 获取需要保留的两个最新版本
|
// 获取需要保留的两个最新版本
|
||||||
const keepVersions = versionList.slice(0, 2);
|
const keepVersions = versionList.slice(0, 2)
|
||||||
// 获取所有需要删除的版本
|
// 获取所有需要删除的版本
|
||||||
const oldVersions = versionList.slice(2);
|
const oldVersions = versionList.slice(2)
|
||||||
|
|
||||||
// 先获取 R2 桶中的所有文件列表
|
// 先获取 R2 桶中的所有文件列表
|
||||||
const allFiles = await listAllFiles(env);
|
const allFiles = await listAllFiles(env)
|
||||||
|
|
||||||
// 获取需要保留的文件名列表
|
// 获取需要保留的文件名列表
|
||||||
const keepFiles = new Set();
|
const keepFiles = new Set()
|
||||||
for (const keepVersion of keepVersions) {
|
for (const keepVersion of keepVersions) {
|
||||||
const versionFiles = versions.versions[keepVersion].files;
|
const versionFiles = versions.versions[keepVersion].files
|
||||||
versionFiles.forEach(file => keepFiles.add(file.name));
|
versionFiles.forEach((file) => keepFiles.add(file.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除所有旧版本文件
|
// 删除所有旧版本文件
|
||||||
for (const oldVersion of oldVersions) {
|
for (const oldVersion of oldVersions) {
|
||||||
const oldFiles = versions.versions[oldVersion].files;
|
const oldFiles = versions.versions[oldVersion].files
|
||||||
for (const file of oldFiles) {
|
for (const file of oldFiles) {
|
||||||
try {
|
try {
|
||||||
if (file.uploaded) {
|
if (file.uploaded) {
|
||||||
await env.R2_BUCKET.delete(file.name);
|
await env.R2_BUCKET.delete(file.name)
|
||||||
await addLog(env, 'INFO', `删除旧文件: ${file.name}`);
|
await addLog(env, 'INFO', `删除旧文件: ${file.name}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message);
|
await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete versions.versions[oldVersion];
|
delete versions.versions[oldVersion]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理可能遗留的旧文件
|
// 清理可能遗留的旧文件
|
||||||
for (const file of allFiles) {
|
for (const file of allFiles) {
|
||||||
if (!keepFiles.has(file.name)) {
|
if (!keepFiles.has(file.name)) {
|
||||||
try {
|
try {
|
||||||
await env.R2_BUCKET.delete(file.name);
|
await env.R2_BUCKET.delete(file.name)
|
||||||
await addLog(env, 'INFO', `删除遗留文件: ${file.name}`);
|
await addLog(env, 'INFO', `删除遗留文件: ${file.name}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message);
|
await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存更新后的版本数据库
|
// 保存更新后的版本数据库
|
||||||
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2));
|
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新');
|
await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新')
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasUpdates ? cacheData : null;
|
return hasUpdates ? cacheData : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await addLog(env, 'ERROR', '检查新版本失败', error.message);
|
await addLog(env, 'ERROR', '检查新版本失败', error.message)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增:获取 R2 桶中的所有文件列表
|
// 新增:获取 R2 桶中的所有文件列表
|
||||||
async function listAllFiles(env) {
|
async function listAllFiles(env) {
|
||||||
const files = [];
|
const files = []
|
||||||
let cursor;
|
let cursor
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] });
|
const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] })
|
||||||
files.push(...listed.objects);
|
files.push(...listed.objects)
|
||||||
cursor = listed.cursor;
|
cursor = listed.cursor
|
||||||
} while (cursor);
|
} while (cursor)
|
||||||
|
|
||||||
return files;
|
return files
|
||||||
}
|
}
|
||||||
|
|||||||
58
scripts/remove-locales.js
Normal 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
@@ -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()
|
||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import BackupManager from './services/BackupManager'
|
|||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
import { ExportService } from './services/ExportService'
|
import { ExportService } from './services/ExportService'
|
||||||
import FileStorage from './services/FileStorage'
|
import FileStorage from './services/FileStorage'
|
||||||
|
import KnowledgeService from './services/KnowledgeService'
|
||||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
@@ -100,6 +101,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
|
|
||||||
// file
|
// file
|
||||||
ipcMain.handle('file:open', fileManager.open)
|
ipcMain.handle('file:open', fileManager.open)
|
||||||
|
ipcMain.handle('file:openPath', fileManager.openPath)
|
||||||
ipcMain.handle('file:save', fileManager.save)
|
ipcMain.handle('file:save', fileManager.save)
|
||||||
ipcMain.handle('file:select', fileManager.selectFile)
|
ipcMain.handle('file:select', fileManager.selectFile)
|
||||||
ipcMain.handle('file:upload', fileManager.uploadFile)
|
ipcMain.handle('file:upload', fileManager.uploadFile)
|
||||||
@@ -144,4 +146,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
registerShortcuts(mainWindow)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
OpenDialogOptions,
|
OpenDialogOptions,
|
||||||
OpenDialogReturnValue,
|
OpenDialogReturnValue,
|
||||||
SaveDialogOptions,
|
SaveDialogOptions,
|
||||||
SaveDialogReturnValue
|
SaveDialogReturnValue,
|
||||||
|
shell
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import * as fs from 'fs'
|
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 (
|
public save = async (
|
||||||
_: Electron.IpcMainInvokeEvent,
|
_: Electron.IpcMainInvokeEvent,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
|
|||||||
154
src/main/services/KnowledgeService.ts
Normal 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()
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { is } from '@electron-toolkit/utils'
|
import { is } from '@electron-toolkit/utils'
|
||||||
import { isLinux, isWin } from '@main/constant'
|
import { isLinux, isWin } from '@main/constant'
|
||||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||||
|
import Logger from 'electron-log'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { join } from 'path'
|
import path, { join } from 'path'
|
||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
import icon from '../../../build/icon.png?asset'
|
||||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||||
@@ -32,6 +33,7 @@ export class WindowService {
|
|||||||
|
|
||||||
const theme = configManager.getTheme()
|
const theme = configManager.getTheme()
|
||||||
const isMac = process.platform === 'darwin'
|
const isMac = process.platform === 'darwin'
|
||||||
|
const isLinux = process.platform === 'linux'
|
||||||
|
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
x: mainWindowState.x,
|
x: mainWindowState.x,
|
||||||
@@ -45,7 +47,7 @@ export class WindowService {
|
|||||||
transparent: isMac,
|
transparent: isMac,
|
||||||
vibrancy: 'under-window',
|
vibrancy: 'under-window',
|
||||||
visualEffectState: 'active',
|
visualEffectState: 'active',
|
||||||
titleBarStyle: 'hidden',
|
titleBarStyle: isLinux ? 'default' : 'hidden',
|
||||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||||
trafficLightPosition: { x: 8, y: 12 },
|
trafficLightPosition: { x: 8, y: 12 },
|
||||||
@@ -123,12 +125,26 @@ export class WindowService {
|
|||||||
|
|
||||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||||
|
if (url.includes('localhost:5173')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
shell.openExternal(url)
|
shell.openExternal(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
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)
|
shell.openExternal(details.url)
|
||||||
|
}
|
||||||
|
|
||||||
return { action: 'deny' }
|
return { action: 'deny' }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,11 @@ export function getDataPath() {
|
|||||||
}
|
}
|
||||||
return dataPath
|
return dataPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getInstanceName(baseURL: string) {
|
||||||
|
try {
|
||||||
|
return new URL(baseURL).host.split('.')[0]
|
||||||
|
} catch (error) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
|
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 RuRu from '../../renderer/src/i18n/locales/ru-ru.json'
|
||||||
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
|
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
|
||||||
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
|
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
|
||||||
@@ -7,6 +8,7 @@ const locales = {
|
|||||||
'en-US': EnUs,
|
'en-US': EnUs,
|
||||||
'zh-CN': ZhCn,
|
'zh-CN': ZhCn,
|
||||||
'zh-TW': ZhTw,
|
'zh-TW': ZhTw,
|
||||||
|
'ja-JP': JaJP,
|
||||||
'ru-RU': RuRu
|
'ru-RU': RuRu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/preload/index.d.ts
vendored
@@ -1,7 +1,8 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
import { FileType } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
import { WebDavConfig } 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 { OpenDialogOptions } from 'electron'
|
||||||
import type { UpdateInfo } from 'electron-updater'
|
import type { UpdateInfo } from 'electron-updater'
|
||||||
import { Readable } from 'stream'
|
import { Readable } from 'stream'
|
||||||
@@ -41,6 +42,7 @@ declare global {
|
|||||||
create: (fileName: string) => Promise<string>
|
create: (fileName: string) => Promise<string>
|
||||||
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||||
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
|
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
|
||||||
|
openPath: (path: string) => Promise<void>
|
||||||
save: (
|
save: (
|
||||||
path: string,
|
path: string,
|
||||||
content: string | NodeJS.ArrayBufferView,
|
content: string | NodeJS.ArrayBufferView,
|
||||||
@@ -58,6 +60,22 @@ declare global {
|
|||||||
shortcuts: {
|
shortcuts: {
|
||||||
update: (shortcuts: Shortcut[]) => Promise<void>
|
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[]>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
import { Shortcut, WebDavConfig } from '@types'
|
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
@@ -36,6 +36,7 @@ const api = {
|
|||||||
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
|
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
|
||||||
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
|
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
|
||||||
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
|
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 }) =>
|
save: (path: string, content: string, options?: { compress: boolean }) =>
|
||||||
ipcRenderer.invoke('file:save', path, content, options),
|
ipcRenderer.invoke('file:save', path, content, options),
|
||||||
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
|
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
|
||||||
@@ -50,6 +51,25 @@ const api = {
|
|||||||
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
|
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', 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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import AgentsPage from './pages/agents/AgentsPage'
|
|||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
import FilesPage from './pages/files/FilesPage'
|
import FilesPage from './pages/files/FilesPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
|
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
@@ -30,10 +31,11 @@ function App(): JSX.Element {
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/files" element={<FilesPage />} />
|
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
<Route path="/paintings" element={<PaintingsPage />} />
|
<Route path="/paintings" element={<PaintingsPage />} />
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
|
<Route path="/files" element={<FilesPage />} />
|
||||||
|
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
src/renderer/src/assets/images/apps/github-copilot.webp
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/renderer/src/assets/images/apps/nm.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/renderer/src/assets/images/apps/thinkany.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 44 KiB |
@@ -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 |
@@ -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 |
BIN
src/renderer/src/assets/images/models/bigcode.webp
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/renderer/src/assets/images/models/bigcode_dark.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
@@ -42,6 +42,7 @@
|
|||||||
--color-active: rgba(55, 55, 55, 1);
|
--color-active: rgba(55, 55, 55, 1);
|
||||||
--color-frame-border: #333;
|
--color-frame-border: #333;
|
||||||
--color-group-background: var(--color-background-soft);
|
--color-group-background: var(--color-background-soft);
|
||||||
|
--color-reference-background: #0b0e12;
|
||||||
|
|
||||||
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
||||||
--navbar-background: rgba(30, 30, 30);
|
--navbar-background: rgba(30, 30, 30);
|
||||||
@@ -99,6 +100,7 @@ body[theme-mode='light'] {
|
|||||||
--color-active: var(--color-white-soft);
|
--color-active: var(--color-white-soft);
|
||||||
--color-frame-border: #ddd;
|
--color-frame-border: #ddd;
|
||||||
--color-group-background: var(--color-white);
|
--color-group-background: var(--color-white);
|
||||||
|
--color-reference-background: #f1f7ff;
|
||||||
|
|
||||||
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
||||||
--navbar-background: rgba(255, 255, 255);
|
--navbar-background: rgba(255, 255, 255);
|
||||||
@@ -169,19 +171,11 @@ body,
|
|||||||
border-top: 0.5px solid var(--color-border);
|
border-top: 0.5px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
body[os='mac'] {
|
|
||||||
#content-container {
|
#content-container {
|
||||||
border-top-left-radius: 12px;
|
border-top-left-radius: 12px;
|
||||||
border-left: 0.5px solid var(--color-border);
|
border-left: 0.5px solid var(--color-border);
|
||||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08);
|
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
body[os='windows'] {
|
|
||||||
#app-sidebar {
|
|
||||||
border-right: 0.5px solid var(--color-border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|||||||
@@ -66,6 +66,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: initial;
|
||||||
|
}
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
@@ -225,11 +229,24 @@
|
|||||||
|
|
||||||
.footnotes {
|
.footnotes {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
padding-top: 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 {
|
ol {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
|
margin: 0;
|
||||||
|
li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
|||||||
15
src/renderer/src/components/Icons/WebSearchIcon.tsx
Normal 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
|
||||||
84
src/renderer/src/components/ListItem/index.tsx
Normal 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
|
||||||
65
src/renderer/src/components/Popups/AppStorePopover.tsx
Normal 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
|
||||||
@@ -14,7 +14,7 @@ interface PromptPopupShowParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends PromptPopupShowParams {
|
interface Props extends PromptPopupShowParams {
|
||||||
resolve: (value: string) => void
|
resolve: (value: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PromptPopupContainer: React.FC<Props> = ({
|
const PromptPopupContainer: React.FC<Props> = ({
|
||||||
@@ -30,18 +30,21 @@ const PromptPopupContainer: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
resolve(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const onCancel = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
resolve(value)
|
resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PromptPopup.hide = onCancel
|
||||||
|
|
||||||
return (
|
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>
|
<Box mb={8}>{message}</Box>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder={inputPlaceholder}
|
placeholder={inputPlaceholder}
|
||||||
@@ -57,10 +60,12 @@ const PromptPopupContainer: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TopViewKey = 'PromptPopup'
|
||||||
|
|
||||||
export default class PromptPopup {
|
export default class PromptPopup {
|
||||||
static topviewId = 0
|
static topviewId = 0
|
||||||
static hide() {
|
static hide() {
|
||||||
TopView.hide('PromptPopup')
|
TopView.hide(TopViewKey)
|
||||||
}
|
}
|
||||||
static show(props: PromptPopupShowParams) {
|
static show(props: PromptPopupShowParams) {
|
||||||
return new Promise<string>((resolve) => {
|
return new Promise<string>((resolve) => {
|
||||||
@@ -69,7 +74,7 @@ export default class PromptPopup {
|
|||||||
{...props}
|
{...props}
|
||||||
resolve={(v) => {
|
resolve={(v) => {
|
||||||
resolve(v)
|
resolve(v)
|
||||||
this.hide()
|
TopView.hide(TopViewKey)
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
'PromptPopup'
|
'PromptPopup'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
|
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
|
||||||
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
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 db from '@renderer/databases'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
@@ -12,6 +12,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import WebSearchIcon from '../Icons/WebSearchIcon'
|
||||||
import { HStack } from '../Layout'
|
import { HStack } from '../Layout'
|
||||||
import Scrollbar from '../Scrollbar'
|
import Scrollbar from '../Scrollbar'
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
.filter((p) => p.models && p.models.length > 0)
|
.filter((p) => p.models && p.models.length > 0)
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const filteredModels = sortBy(p.models, ['group', 'name'])
|
const filteredModels = sortBy(p.models, ['group', 'name'])
|
||||||
|
.filter((m) => !isEmbeddingModel(m))
|
||||||
.filter((m) =>
|
.filter((m) =>
|
||||||
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
|
[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: (
|
label: (
|
||||||
<ModelItem>
|
<ModelItem>
|
||||||
<span>
|
<span>
|
||||||
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />}
|
||||||
</span>
|
</span>
|
||||||
<PinIcon
|
<PinIcon
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -113,7 +115,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
.flatMap((p) => p.models || [])
|
.flatMap((p) => p.models || [])
|
||||||
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
|
||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
key: getModelUniqId(m),
|
key: getModelUniqId(m) + '_pinned',
|
||||||
label: (
|
label: (
|
||||||
<ModelItem>
|
<ModelItem>
|
||||||
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
||||||
@@ -141,7 +143,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
if (pinnedItems.length > 0) {
|
if (pinnedItems.length > 0) {
|
||||||
filteredItems.unshift({
|
filteredItems.unshift({
|
||||||
key: 'pinned',
|
key: 'pinned',
|
||||||
label: t('model.pinned'),
|
label: t('models.pinned'),
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: pinnedItems
|
children: pinnedItems
|
||||||
} as MenuItem)
|
} as MenuItem)
|
||||||
@@ -187,7 +189,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
|||||||
</SearchIcon>
|
</SearchIcon>
|
||||||
}
|
}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={t('model.search')}
|
placeholder={t('models.search')}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
|
|||||||
@@ -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 { isMac } from '@renderer/config/constant'
|
||||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
@@ -22,7 +22,7 @@ const Sidebar: FC = () => {
|
|||||||
const { generating } = useRuntime()
|
const { generating } = useRuntime()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { windowStyle } = useSettings()
|
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
|
||||||
const { theme, toggleTheme } = useTheme()
|
const { theme, toggleTheme } = useTheme()
|
||||||
|
|
||||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||||
@@ -79,6 +79,7 @@ const Sidebar: FC = () => {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{showMinappIcon && (
|
||||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink onClick={() => to('/apps')}>
|
<StyledLink onClick={() => to('/apps')}>
|
||||||
<Icon className={isRoute('/apps')}>
|
<Icon className={isRoute('/apps')}>
|
||||||
@@ -86,6 +87,15 @@ const Sidebar: FC = () => {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Tooltip>
|
</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">
|
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink onClick={() => to('/files')}>
|
<StyledLink onClick={() => to('/files')}>
|
||||||
<Icon className={isRoute('/files')}>
|
<Icon className={isRoute('/files')}>
|
||||||
@@ -93,6 +103,7 @@ const Sidebar: FC = () => {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Menus>
|
</Menus>
|
||||||
</MainMenus>
|
</MainMenus>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus onClick={MinApp.onClose}>
|
||||||
|
|||||||
@@ -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 BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
||||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
||||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
|
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
|
||||||
@@ -8,14 +6,17 @@ import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
|
|||||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
|
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
|
||||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
|
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
|
||||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
|
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
|
||||||
|
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
|
||||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
|
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
|
||||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
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 PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
|
||||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
|
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
|
||||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
|
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
|
||||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
||||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.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 TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
|
||||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg'
|
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg'
|
||||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
||||||
@@ -119,19 +120,6 @@ const _apps: MinAppType[] = [
|
|||||||
url: 'https://claude.ai/',
|
url: 'https://claude.ai/',
|
||||||
logo: ClaudeAppLogo
|
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',
|
id: 'baidu-ai-chat',
|
||||||
name: '文心一言',
|
name: '文心一言',
|
||||||
@@ -210,6 +198,12 @@ const _apps: MinAppType[] = [
|
|||||||
url: 'https://felo.ai/',
|
url: 'https://felo.ai/',
|
||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'duckduckgo',
|
||||||
|
name: 'DuckDuckGo',
|
||||||
|
logo: DuckDuckGoAppLogo,
|
||||||
|
url: 'https://duck.ai'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'bolt',
|
id: 'bolt',
|
||||||
name: 'bolt',
|
name: 'bolt',
|
||||||
@@ -218,10 +212,24 @@ const _apps: MinAppType[] = [
|
|||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'duckduckgo',
|
id: 'nm',
|
||||||
name: 'DuckDuckGo',
|
name: '纳米AI搜索',
|
||||||
logo: DuckDuckGoAppLogo,
|
logo: NamiAiSearchLogo,
|
||||||
url: 'https://duck.ai'
|
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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import AisingaporeModelLogo from '@renderer/assets/images/models/aisingapore.png
|
|||||||
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
|
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
|
||||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||||
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
|
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
|
||||||
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.png'
|
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.webp'
|
||||||
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.png'
|
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.webp'
|
||||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
|
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
|
||||||
import ChatGLMModelLogoDark from '@renderer/assets/images/models/chatglm_dark.png'
|
import ChatGLMModelLogoDark from '@renderer/assets/images/models/chatglm_dark.png'
|
||||||
import ChatGptModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
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 WenxinModelLogoDark from '@renderer/assets/images/models/wenxin_dark.png'
|
||||||
import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
||||||
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
|
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
|
||||||
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import OpenAI from 'openai'
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
@@ -150,9 +151,9 @@ export const VISION_REGEX = new RegExp(
|
|||||||
'i'
|
'i'
|
||||||
)
|
)
|
||||||
|
|
||||||
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
|
export 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
|
export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina)/i
|
||||||
const NOT_SUPPORTED_REGEX = /(?:^text-|embed|tts|rerank|whisper|speech|davinci|babbage|bge-|base|retrieval|uae-)/i
|
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
|
||||||
|
|
||||||
export function getModelLogo(modelId: string) {
|
export function getModelLogo(modelId: string) {
|
||||||
const isLight = true
|
const isLight = true
|
||||||
@@ -264,22 +265,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
ollama: [],
|
ollama: [],
|
||||||
silicon: [
|
silicon: [
|
||||||
{
|
{
|
||||||
id: 'Qwen/Qwen2.5-72B-Instruct',
|
id: 'deepseek-ai/DeepSeek-V2.5',
|
||||||
|
name: 'deepseek-ai/DeepSeek-V2.5',
|
||||||
provider: 'silicon',
|
provider: 'silicon',
|
||||||
name: 'Qwen2.5-72B-Instruct',
|
group: 'deepseek-ai'
|
||||||
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'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Qwen/Qwen2.5-7B-Instruct',
|
id: 'Qwen/Qwen2.5-7B-Instruct',
|
||||||
@@ -288,79 +277,17 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
group: 'Qwen2.5'
|
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',
|
provider: 'silicon',
|
||||||
name: 'Qwen2-7B-Instruct',
|
group: 'meta-llama'
|
||||||
group: 'Qwen2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-72B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-72B-Instruct',
|
|
||||||
group: 'Qwen2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'THUDM/glm-4-9b-chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'GLM-4-9B-Chat',
|
|
||||||
group: 'GLM'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'DeepSeek-V2-Chat',
|
|
||||||
group: 'DeepSeek'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'DeepSeek-Coder-V2-Instruct',
|
|
||||||
group: 'DeepSeek'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
openai: [
|
openai: [
|
||||||
{
|
{ id: 'gpt-4o', provider: 'openai', name: ' GPT-4o', group: 'GPT 4o' },
|
||||||
id: 'gpt-4o',
|
{ id: 'gpt-4o-mini', provider: 'openai', name: ' GPT-4o-mini', group: 'GPT 4o' },
|
||||||
provider: 'openai',
|
{ id: 'o1-mini', provider: 'openai', name: ' o1-mini', group: 'o1' },
|
||||||
name: ' GPT-4o',
|
{ id: 'o1-preview', provider: 'openai', name: ' o1-preview', group: 'o1' }
|
||||||
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'
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
'azure-openai': [
|
'azure-openai': [
|
||||||
{
|
{
|
||||||
@@ -384,10 +311,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
group: 'Gemini 1.5'
|
group: 'Gemini 1.5'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemini-1.5-pro-exp-0801',
|
id: 'gemini-1.5-pro',
|
||||||
|
name: 'Gemini 1.5 Pro',
|
||||||
provider: 'gemini',
|
provider: 'gemini',
|
||||||
name: 'Gemini 1.5 Pro Experimental 0801',
|
group: 'gemini-1.5'
|
||||||
group: 'Gemini 1.5'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
anthropic: [
|
anthropic: [
|
||||||
@@ -587,42 +514,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
yi: [
|
yi: [
|
||||||
{
|
{ id: 'yi-lightning', name: 'yi-lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
|
||||||
id: 'yi-large',
|
{ id: 'yi-medium', name: 'yi-medium', provider: 'yi', group: 'yi-medium', owned_by: '01.ai' },
|
||||||
provider: 'yi',
|
{ id: 'yi-large', name: 'yi-large', provider: 'yi', group: 'yi-large', owned_by: '01.ai' },
|
||||||
name: 'Yi-Large',
|
{ id: 'yi-vision', name: 'yi-vision', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
|
||||||
group: 'Yi'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-large-turbo',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Large-Turbo',
|
|
||||||
group: 'Yi'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-large-rag',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Large-Rag',
|
|
||||||
group: 'Yi'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-medium',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Medium',
|
|
||||||
group: 'Yi'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-medium-200k',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Medium-200k',
|
|
||||||
group: 'Yi'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'yi-spark',
|
|
||||||
provider: 'yi',
|
|
||||||
name: 'Yi-Spark',
|
|
||||||
group: 'Yi'
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
zhipu: [
|
zhipu: [
|
||||||
{
|
{
|
||||||
@@ -676,22 +571,11 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
],
|
],
|
||||||
moonshot: [
|
moonshot: [
|
||||||
{
|
{
|
||||||
id: 'moonshot-v1-8k',
|
id: 'moonshot-v1-auto',
|
||||||
|
name: 'moonshot-v1-auto',
|
||||||
provider: 'moonshot',
|
provider: 'moonshot',
|
||||||
name: 'Moonshot V1 8k',
|
group: 'moonshot-v1',
|
||||||
group: 'Moonshot V1'
|
owned_by: 'moonshot'
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'moonshot-v1-32k',
|
|
||||||
provider: 'moonshot',
|
|
||||||
name: 'Moonshot V1 32k',
|
|
||||||
group: 'Moonshot V1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'moonshot-v1-128k',
|
|
||||||
provider: 'moonshot',
|
|
||||||
name: 'Moonshot V1 128k',
|
|
||||||
group: 'Moonshot V1'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
baichuan: [
|
baichuan: [
|
||||||
@@ -715,24 +599,11 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
bailian: [
|
bailian: [
|
||||||
{
|
{ id: 'qwen-vl-plus', name: 'qwen-vl-plus', provider: 'dashscope', group: 'qwen-vl', owned_by: 'system' },
|
||||||
id: 'qwen-turbo',
|
{ id: 'qwen-coder-plus', name: 'qwen-coder-plus', provider: 'dashscope', group: 'qwen-coder', owned_by: 'system' },
|
||||||
provider: 'dashscope',
|
{ id: 'qwen-turbo', name: 'qwen-turbo', provider: 'dashscope', group: 'qwen-turbo', owned_by: 'system' },
|
||||||
name: 'Qwen Turbo',
|
{ id: 'qwen-plus', name: 'qwen-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
|
||||||
group: 'Qwen'
|
{ id: 'qwen-max', name: 'qwen-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' }
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'qwen-plus',
|
|
||||||
provider: 'dashscope',
|
|
||||||
name: 'Qwen Plus',
|
|
||||||
group: 'Qwen'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'qwen-max',
|
|
||||||
provider: 'dashscope',
|
|
||||||
name: 'Qwen Max',
|
|
||||||
group: 'Qwen'
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
stepfun: [
|
stepfun: [
|
||||||
{
|
{
|
||||||
@@ -829,7 +700,56 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
group: 'Mistral'
|
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: [
|
aihubmix: [
|
||||||
{
|
{
|
||||||
id: 'gpt-4o-mini',
|
id: 'gpt-4o-mini',
|
||||||
@@ -1046,13 +966,43 @@ export function isTextToImageModel(model: Model): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isEmbeddingModel(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 {
|
export function isVisionModel(model: Model): boolean {
|
||||||
|
if (!model) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportedModel(model: OpenAI.Models.Model): boolean {
|
export function isSupportedModel(model: OpenAI.Models.Model): boolean {
|
||||||
|
if (!model) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return !NOT_SUPPORTED_REGEX.test(model.id)
|
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'
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,3 +49,20 @@ export const SUMMARIZE_PROMPT =
|
|||||||
|
|
||||||
export const TRANSLATE_PROMPT =
|
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.'
|
'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}
|
||||||
|
`
|
||||||
|
|||||||
@@ -355,11 +355,11 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
aihubmix: {
|
aihubmix: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://aihubmix.com'
|
url: 'https://aihubmix.com?aff=SJyh'
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://aihubmix.com/',
|
official: 'https://aihubmix.com?aff=SJyh',
|
||||||
apiKey: 'https://aihubmix.com/token',
|
apiKey: 'https://aihubmix.com?aff=SJyh',
|
||||||
docs: 'https://doc.aihubmix.com/',
|
docs: 'https://doc.aihubmix.com/',
|
||||||
models: 'https://aihubmix.com/models'
|
models: 'https://aihubmix.com/models'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
|||||||
import { LanguageVarious } from '@renderer/types'
|
import { LanguageVarious } from '@renderer/types'
|
||||||
import { ConfigProvider, theme } from 'antd'
|
import { ConfigProvider, theme } from 'antd'
|
||||||
import enUS from 'antd/locale/en_US'
|
import enUS from 'antd/locale/en_US'
|
||||||
|
import jaJP from 'antd/locale/ja_JP'
|
||||||
import ruRU from 'antd/locale/ru_RU'
|
import ruRU from 'antd/locale/ru_RU'
|
||||||
import zhCN from 'antd/locale/zh_CN'
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
import zhTW from 'antd/locale/zh_TW'
|
import zhTW from 'antd/locale/zh_TW'
|
||||||
@@ -31,6 +32,13 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
Menu: {
|
Menu: {
|
||||||
activeBarBorderWidth: 0,
|
activeBarBorderWidth: 0,
|
||||||
darkItemBg: 'transparent'
|
darkItemBg: 'transparent'
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
boxShadow: 'none',
|
||||||
|
boxShadowSecondary: 'none',
|
||||||
|
defaultShadow: 'none',
|
||||||
|
dangerShadow: 'none',
|
||||||
|
primaryShadow: 'none'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
@@ -52,6 +60,8 @@ function getAntdLocale(language: LanguageVarious) {
|
|||||||
return enUS
|
return enUS
|
||||||
case 'ru-RU':
|
case 'ru-RU':
|
||||||
return ruRU
|
return ruRU
|
||||||
|
case 'ja-JP':
|
||||||
|
return jaJP
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return zhCN
|
return zhCN
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileType, Topic } from '@renderer/types'
|
import { FileType, KnowledgeItem, Topic } from '@renderer/types'
|
||||||
import { Dexie, type EntityTable } from 'dexie'
|
import { Dexie, type EntityTable } from 'dexie'
|
||||||
|
|
||||||
// Database declaration (move this to its own module also)
|
// 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'>
|
files: EntityTable<FileType, 'id'>
|
||||||
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
||||||
settings: EntityTable<{ id: string; value: any }, 'id'>
|
settings: EntityTable<{ id: string; value: any }, 'id'>
|
||||||
|
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
|
||||||
}
|
}
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
@@ -18,4 +19,11 @@ db.version(2).stores({
|
|||||||
settings: '&id, value'
|
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
|
export default db
|
||||||
|
|||||||
1
src/renderer/src/env.d.ts
vendored
@@ -19,5 +19,6 @@ declare global {
|
|||||||
modal: HookAPI
|
modal: HookAPI
|
||||||
keyv: KeyvStorage
|
keyv: KeyvStorage
|
||||||
mermaid: any
|
mermaid: any
|
||||||
|
store: any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { isMac } from '@renderer/config/constant'
|
|||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
|
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
|
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
|
||||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||||
@@ -15,7 +16,16 @@ import useUpdateHandler from './useUpdateHandler'
|
|||||||
|
|
||||||
export function useAppInit() {
|
export function useAppInit() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode } = useSettings()
|
const {
|
||||||
|
proxyUrl,
|
||||||
|
language,
|
||||||
|
windowStyle,
|
||||||
|
manualUpdateCheck,
|
||||||
|
proxyMode,
|
||||||
|
webdavAutoSync,
|
||||||
|
webdavSyncInterval,
|
||||||
|
customCss
|
||||||
|
} = useSettings()
|
||||||
const { minappShow } = useRuntime()
|
const { minappShow } = useRuntime()
|
||||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||||
@@ -73,4 +83,26 @@ export function useAppInit() {
|
|||||||
dispatch(setFilesPath(info.filesPath))
|
dispatch(setFilesPath(info.filesPath))
|
||||||
})
|
})
|
||||||
}, [dispatch])
|
}, [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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { db } from '@renderer/databases'
|
||||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
@@ -50,8 +51,20 @@ export function useAssistant(id: string) {
|
|||||||
dispatch(removeTopic({ assistantId: assistant.id, topic }))
|
dispatch(removeTopic({ assistantId: assistant.id, topic }))
|
||||||
},
|
},
|
||||||
moveTopic: (topic: Topic, toAssistant: Assistant) => {
|
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 }))
|
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 })),
|
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
||||||
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
||||||
|
|||||||
279
src/renderer/src/hooks/useKnowledge.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@@ -38,7 +39,7 @@ export const useShortcut = (
|
|||||||
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey)
|
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : '',
|
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : 'none',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (options.preventDefault) {
|
if (options.preventDefault) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -49,7 +50,8 @@ export const useShortcut = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enableOnFormTags: options.enableOnFormTags,
|
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)
|
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||||
return { 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) : ''
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
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() {
|
export function useShowAssistants() {
|
||||||
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
||||||
@@ -7,6 +7,7 @@ export function useShowAssistants() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
showAssistants,
|
showAssistants,
|
||||||
|
setShowAssistants: (show: boolean) => dispatch(setShowAssistants(show)),
|
||||||
toggleShowAssistants: () => dispatch(toggleShowAssistants())
|
toggleShowAssistants: () => dispatch(toggleShowAssistants())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,11 +54,13 @@ export default function useUpdateHandler() {
|
|||||||
downloadProgress: 0
|
downloadProgress: 0
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
if (window.location.hash.includes('settings/about')) {
|
||||||
window.modal.info({
|
window.modal.info({
|
||||||
title: t('settings.about.updateError'),
|
title: t('settings.about.updateError'),
|
||||||
content: error?.message || t('settings.about.updateError'),
|
content: error?.message || t('settings.about.updateError'),
|
||||||
icon: null
|
icon: null
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
return () => removers.forEach((remover) => remover())
|
return () => removers.forEach((remover) => remover())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import i18n from 'i18next'
|
|||||||
import { initReactI18next } from 'react-i18next'
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
import enUS from './locales/en-us.json'
|
import enUS from './locales/en-us.json'
|
||||||
|
import jaJP from './locales/ja-jp.json'
|
||||||
import ruRU from './locales/ru-ru.json'
|
import ruRU from './locales/ru-ru.json'
|
||||||
import zhCN from './locales/zh-cn.json'
|
import zhCN from './locales/zh-cn.json'
|
||||||
import zhTW from './locales/zh-tw.json'
|
import zhTW from './locales/zh-tw.json'
|
||||||
@@ -10,6 +11,7 @@ const resources = {
|
|||||||
'en-US': enUS,
|
'en-US': enUS,
|
||||||
'zh-CN': zhCN,
|
'zh-CN': zhCN,
|
||||||
'zh-TW': zhTW,
|
'zh-TW': zhTW,
|
||||||
|
'ja-JP': jaJP,
|
||||||
'ru-RU': ruRU
|
'ru-RU': ruRU
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"input.estimated_tokens.tip": "Estimated tokens",
|
"input.estimated_tokens.tip": "Estimated tokens",
|
||||||
"input.expand": "Expand",
|
"input.expand": "Expand",
|
||||||
"input.new.context": "Clear Context",
|
"input.new.context": "Clear Context",
|
||||||
"input.new_topic": "New Topic {{Command}}+N",
|
"input.new_topic": "New Topic {{Command}}",
|
||||||
"input.pause": "Pause",
|
"input.pause": "Pause",
|
||||||
"input.placeholder": "Type your message here...",
|
"input.placeholder": "Type your message here...",
|
||||||
"input.send": "Send",
|
"input.send": "Send",
|
||||||
@@ -80,8 +80,11 @@
|
|||||||
"input.topics": " Topics ",
|
"input.topics": " Topics ",
|
||||||
"input.translate": "Translate to English",
|
"input.translate": "Translate to English",
|
||||||
"input.upload": "Upload image or document file",
|
"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": "New Branch",
|
||||||
"message.new.branch.created": "New Branch Created",
|
"message.new.branch.created": "New Branch Created",
|
||||||
|
"message.regenerate.model": "Switch Model",
|
||||||
"message.new.context": "New Context",
|
"message.new.context": "New Context",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"settings.code_collapsible": "Code block collapsible",
|
"settings.code_collapsible": "Code block collapsible",
|
||||||
@@ -220,11 +223,13 @@
|
|||||||
"assistant.added.content": "Assistant added successfully",
|
"assistant.added.content": "Assistant added successfully",
|
||||||
"backup.failed": "Backup failed",
|
"backup.failed": "Backup failed",
|
||||||
"backup.success": "Backup successful",
|
"backup.success": "Backup successful",
|
||||||
|
"backup.start.success": "Backup started",
|
||||||
"chat.completion.paused": "Chat completion paused",
|
"chat.completion.paused": "Chat completion paused",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"error.enter.api.host": "Please enter your API host first",
|
"error.enter.api.host": "Please enter your API host first",
|
||||||
"error.enter.api.key": "Please enter your API key first",
|
"error.enter.api.key": "Please enter your API key first",
|
||||||
"error.enter.model": "Please select a model 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.proxy.url": "Invalid proxy URL",
|
||||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||||
"message.code_style": "Code style",
|
"message.code_style": "Code style",
|
||||||
@@ -243,21 +248,13 @@
|
|||||||
"upgrade.success.button": "Restart",
|
"upgrade.success.button": "Restart",
|
||||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||||
"upgrade.success.title": "Upgrade successfully",
|
"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": {
|
"minapp": {
|
||||||
"title": "MinApp"
|
"title": "MinApp"
|
||||||
},
|
},
|
||||||
"model": {
|
|
||||||
"pinned": "Pinned",
|
|
||||||
"search": "Search models...",
|
|
||||||
"stream_output": "Stream output",
|
|
||||||
"type": {
|
|
||||||
"select": "Select Model Types",
|
|
||||||
"text": "Text",
|
|
||||||
"vision": "Vision"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||||
"keep_alive_time.placeholder": "Minutes",
|
"keep_alive_time.placeholder": "Minutes",
|
||||||
@@ -360,6 +357,8 @@
|
|||||||
"webdav.password": "WebDAV Password",
|
"webdav.password": "WebDAV Password",
|
||||||
"webdav.path": "WebDAV Path",
|
"webdav.path": "WebDAV Path",
|
||||||
"webdav.path.placeholder": "/backup",
|
"webdav.path.placeholder": "/backup",
|
||||||
|
"webdav.autoSync": "Auto Sync",
|
||||||
|
"webdav.minutes": "Minutes",
|
||||||
"webdav.restore.button": "Restore from WebDAV",
|
"webdav.restore.button": "Restore from WebDAV",
|
||||||
"webdav.title": "WebDAV",
|
"webdav.title": "WebDAV",
|
||||||
"webdav.user": "WebDAV User"
|
"webdav.user": "WebDAV User"
|
||||||
@@ -377,17 +376,26 @@
|
|||||||
"general.user_name": "User Name",
|
"general.user_name": "User Name",
|
||||||
"general.user_name.placeholder": "Enter your name",
|
"general.user_name.placeholder": "Enter your name",
|
||||||
"general.view_webdav_settings": "View WebDAV settings",
|
"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",
|
"input.auto_translate_with_space": "Quickly translate with 3 spaces",
|
||||||
"messages.divider": "Show divider between messages",
|
"messages.divider": "Show divider between messages",
|
||||||
"messages.input.paste_long_text_as_file": "Paste long text as file",
|
"messages.input.paste_long_text_as_file": "Paste long text as file",
|
||||||
"messages.input.send_shortcuts": "Send shortcuts",
|
"messages.input.send_shortcuts": "Send shortcuts",
|
||||||
"messages.input.show_estimated_tokens": "Show estimated tokens",
|
"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.input.title": "Input Settings",
|
||||||
"messages.markdown_rendering_input_message": "Markdown render input msg",
|
"messages.markdown_rendering_input_message": "Markdown render input msg",
|
||||||
"messages.math_engine": "Math render engine",
|
"messages.math_engine": "Math render engine",
|
||||||
"messages.model.title": "Model Settings",
|
"messages.model.title": "Model Settings",
|
||||||
"messages.title": "Message Settings",
|
"messages.title": "Message Settings",
|
||||||
"messages.use_serif_font": "Use serif font",
|
"messages.use_serif_font": "Use serif font",
|
||||||
|
"messages.input.paste_long_text_threshold": "Paste long text length",
|
||||||
"model": "Default Model",
|
"model": "Default Model",
|
||||||
"models.add.add_model": "Add Model",
|
"models.add.add_model": "Add Model",
|
||||||
"models.add.group_name": "Group Name",
|
"models.add.group_name": "Group Name",
|
||||||
@@ -474,7 +482,8 @@
|
|||||||
"reset_to_default": "Reset to Default",
|
"reset_to_default": "Reset to Default",
|
||||||
"clear_shortcut": "Clear Shortcut",
|
"clear_shortcut": "Clear Shortcut",
|
||||||
"toggle_show_assistants": "Toggle Assistants",
|
"toggle_show_assistants": "Toggle Assistants",
|
||||||
"toggle_show_topics": "Toggle Topics"
|
"toggle_show_topics": "Toggle Topics",
|
||||||
|
"copy_last_message": "Copy Last Message"
|
||||||
},
|
},
|
||||||
"theme.auto": "Auto",
|
"theme.auto": "Auto",
|
||||||
"theme.dark": "Dark",
|
"theme.dark": "Dark",
|
||||||
@@ -511,7 +520,72 @@
|
|||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "Knowledge Graph",
|
"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}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
573
src/renderer/src/i18n/locales/ja-jp.json
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
{
|
||||||
|
"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": "翻訳"
|
||||||
|
},
|
||||||
|
"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": "トピックの自動命名",
|
||||||
|
"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": "設定->モデルサービス->管理で追加"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"input.estimated_tokens.tip": "Затраты токенов",
|
"input.estimated_tokens.tip": "Затраты токенов",
|
||||||
"input.expand": "Развернуть",
|
"input.expand": "Развернуть",
|
||||||
"input.new.context": "Очистить контекст",
|
"input.new.context": "Очистить контекст",
|
||||||
"input.new_topic": "Новый топик {{Command}}+N",
|
"input.new_topic": "Новый топик {{Command}}",
|
||||||
"input.pause": "Остановить",
|
"input.pause": "Остановить",
|
||||||
"input.placeholder": "Введите ваше сообщение здесь...",
|
"input.placeholder": "Введите ваше сообщение здесь...",
|
||||||
"input.send": "Отправить",
|
"input.send": "Отправить",
|
||||||
@@ -80,8 +80,11 @@
|
|||||||
"input.topics": " Топики ",
|
"input.topics": " Топики ",
|
||||||
"input.translate": "Перевести на английский",
|
"input.translate": "Перевести на английский",
|
||||||
"input.upload": "Загрузить изображение или документ",
|
"input.upload": "Загрузить изображение или документ",
|
||||||
|
"input.web_search": "Включить веб-поиск",
|
||||||
|
"input.knowledge_base": "База знаний",
|
||||||
"message.new.branch": "Новая ветка",
|
"message.new.branch": "Новая ветка",
|
||||||
"message.new.branch.created": "Новая ветка создана",
|
"message.new.branch.created": "Новая ветка создана",
|
||||||
|
"message.regenerate.model": "Переключить модель",
|
||||||
"message.new.context": "Новый контекст",
|
"message.new.context": "Новый контекст",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"settings.code_collapsible": "Блок кода свернут",
|
"settings.code_collapsible": "Блок кода свернут",
|
||||||
@@ -220,11 +223,13 @@
|
|||||||
"assistant.added.content": "Ассистент успешно добавлен",
|
"assistant.added.content": "Ассистент успешно добавлен",
|
||||||
"backup.failed": "Создание резервной копии не удалось",
|
"backup.failed": "Создание резервной копии не удалось",
|
||||||
"backup.success": "Резервная копия успешно создана",
|
"backup.success": "Резервная копия успешно создана",
|
||||||
|
"backup.start.success": "Создание резервной копии начато",
|
||||||
"chat.completion.paused": "Завершение чата приостановлено",
|
"chat.completion.paused": "Завершение чата приостановлено",
|
||||||
"copied": "Скопировано!",
|
"copied": "Скопировано!",
|
||||||
"error.enter.api.host": "Пожалуйста, введите ваш API хост",
|
"error.enter.api.host": "Пожалуйста, введите ваш API хост",
|
||||||
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
|
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
|
||||||
"error.enter.model": "Пожалуйста, выберите модель",
|
"error.enter.model": "Пожалуйста, выберите модель",
|
||||||
|
"error.enter.name": "Пожалуйста, введите название базы знаний",
|
||||||
"error.invalid.proxy.url": "Неверный URL прокси",
|
"error.invalid.proxy.url": "Неверный URL прокси",
|
||||||
"error.invalid.webdav": "Неверные настройки WebDAV",
|
"error.invalid.webdav": "Неверные настройки WebDAV",
|
||||||
"message.code_style": "Стиль кода",
|
"message.code_style": "Стиль кода",
|
||||||
@@ -243,21 +248,13 @@
|
|||||||
"upgrade.success.button": "Перезапустить",
|
"upgrade.success.button": "Перезапустить",
|
||||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||||
"upgrade.success.title": "Обновление успешно",
|
"upgrade.success.title": "Обновление успешно",
|
||||||
"regenerate.confirm": "Перегенерация заменит текущее сообщение"
|
"regenerate.confirm": "Перегенерация заменит текущее сообщение",
|
||||||
|
"copy.success": "Скопировано!",
|
||||||
|
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"title": "Встроенные приложения"
|
"title": "Встроенные приложения"
|
||||||
},
|
},
|
||||||
"model": {
|
|
||||||
"pinned": "Закреплено",
|
|
||||||
"search": "Поиск моделей...",
|
|
||||||
"stream_output": "Потоковый вывод",
|
|
||||||
"type": {
|
|
||||||
"select": "Выберите тип модели",
|
|
||||||
"text": "Текст",
|
|
||||||
"vision": "Изображение"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||||
"keep_alive_time.placeholder": "Минуты",
|
"keep_alive_time.placeholder": "Минуты",
|
||||||
@@ -360,6 +357,8 @@
|
|||||||
"webdav.password": "Пароль WebDAV",
|
"webdav.password": "Пароль WebDAV",
|
||||||
"webdav.path": "Путь WebDAV",
|
"webdav.path": "Путь WebDAV",
|
||||||
"webdav.path.placeholder": "/backup",
|
"webdav.path.placeholder": "/backup",
|
||||||
|
"webdav.autoSync": "Автоматическая синхронизация",
|
||||||
|
"webdav.minutes": "минут",
|
||||||
"webdav.restore.button": "Восстановление с WebDAV",
|
"webdav.restore.button": "Восстановление с WebDAV",
|
||||||
"webdav.title": "WebDAV",
|
"webdav.title": "WebDAV",
|
||||||
"webdav.user": "Пользователь WebDAV"
|
"webdav.user": "Пользователь WebDAV"
|
||||||
@@ -377,17 +376,26 @@
|
|||||||
"general.user_name": "Имя пользователя",
|
"general.user_name": "Имя пользователя",
|
||||||
"general.user_name.placeholder": "Введите ваше имя",
|
"general.user_name.placeholder": "Введите ваше имя",
|
||||||
"general.view_webdav_settings": "Просмотр настроек WebDAV",
|
"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-х пробелов",
|
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
|
||||||
"messages.divider": "Показывать разделитель между сообщениями",
|
"messages.divider": "Показывать разделитель между сообщениями",
|
||||||
"messages.input.paste_long_text_as_file": "Вставлять длинный текст как файл",
|
"messages.input.paste_long_text_as_file": "Вставлять длинный текст как файл",
|
||||||
"messages.input.send_shortcuts": "Горячие клавиши для отправки",
|
"messages.input.send_shortcuts": "Горячие клавиши для отправки",
|
||||||
"messages.input.show_estimated_tokens": "Показывать затраты токенов",
|
"messages.input.show_estimated_tokens": "Показывать затраты токенов",
|
||||||
|
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
||||||
"messages.input.title": "Настройки ввода",
|
"messages.input.title": "Настройки ввода",
|
||||||
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
|
"messages.markdown_rendering_input_message": "Отображение ввода в формате Markdown",
|
||||||
"messages.math_engine": "Математический движок",
|
"messages.math_engine": "Математический движок",
|
||||||
"messages.model.title": "Настройки модели",
|
"messages.model.title": "Настройки модели",
|
||||||
"messages.title": "Настройки сообщений",
|
"messages.title": "Настройки сообщений",
|
||||||
"messages.use_serif_font": "Использовать serif шрифт",
|
"messages.use_serif_font": "Использовать serif шрифт",
|
||||||
|
"messages.input.paste_long_text_threshold": "Длина вставки длинного текста",
|
||||||
"model": "Модель по умолчанию",
|
"model": "Модель по умолчанию",
|
||||||
"models.add.add_model": "Добавить модель",
|
"models.add.add_model": "Добавить модель",
|
||||||
"models.add.group_name": "Имя группы",
|
"models.add.group_name": "Имя группы",
|
||||||
@@ -474,7 +482,8 @@
|
|||||||
"reset_to_default": "Сбросить настройки по умолчанию",
|
"reset_to_default": "Сбросить настройки по умолчанию",
|
||||||
"clear_shortcut": "Очистить сочетание клавиш",
|
"clear_shortcut": "Очистить сочетание клавиш",
|
||||||
"toggle_show_assistants": "Переключить отображение ассистентов",
|
"toggle_show_assistants": "Переключить отображение ассистентов",
|
||||||
"toggle_show_topics": "Переключить отображение топиков"
|
"toggle_show_topics": "Переключить отображение топиков",
|
||||||
|
"copy_last_message": "Копировать последнее сообщение"
|
||||||
},
|
},
|
||||||
"theme.auto": "Автоматически",
|
"theme.auto": "Автоматически",
|
||||||
"theme.dark": "Темная",
|
"theme.dark": "Темная",
|
||||||
@@ -511,7 +520,72 @@
|
|||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "Граф знаний",
|
"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}} мер"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"input.estimated_tokens.tip": "预估 token 数",
|
"input.estimated_tokens.tip": "预估 token 数",
|
||||||
"input.expand": "展开",
|
"input.expand": "展开",
|
||||||
"input.new.context": "清除上下文",
|
"input.new.context": "清除上下文",
|
||||||
"input.new_topic": "新话题 {{Command}}+N",
|
"input.new_topic": "新话题 {{Command}}",
|
||||||
"input.pause": "暂停",
|
"input.pause": "暂停",
|
||||||
"input.placeholder": "在这里输入消息...",
|
"input.placeholder": "在这里输入消息...",
|
||||||
"input.send": "发送",
|
"input.send": "发送",
|
||||||
@@ -80,8 +80,11 @@
|
|||||||
"input.topics": " 话题 ",
|
"input.topics": " 话题 ",
|
||||||
"input.translate": "翻译成英文",
|
"input.translate": "翻译成英文",
|
||||||
"input.upload": "上传图片或文档",
|
"input.upload": "上传图片或文档",
|
||||||
|
"input.web_search": "开启网络搜索",
|
||||||
|
"input.knowledge_base": "知识库",
|
||||||
"message.new.branch": "新分支",
|
"message.new.branch": "新分支",
|
||||||
"message.new.branch.created": "新分支已创建",
|
"message.new.branch.created": "新分支已创建",
|
||||||
|
"message.regenerate.model": "切换模型",
|
||||||
"message.new.context": "清除上下文",
|
"message.new.context": "清除上下文",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "代码块可折叠",
|
"settings.code_collapsible": "代码块可折叠",
|
||||||
@@ -146,7 +149,8 @@
|
|||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "用户",
|
"you": "用户",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"add": "添加"
|
"add": "添加",
|
||||||
|
"footnotes": "引用内容"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"backup.file_format": "备份文件格式错误",
|
"backup.file_format": "备份文件格式错误",
|
||||||
@@ -220,11 +224,13 @@
|
|||||||
"assistant.added.content": "智能体添加成功",
|
"assistant.added.content": "智能体添加成功",
|
||||||
"backup.failed": "备份失败",
|
"backup.failed": "备份失败",
|
||||||
"backup.success": "备份成功",
|
"backup.success": "备份成功",
|
||||||
|
"backup.start.success": "开始备份",
|
||||||
"chat.completion.paused": "会话已停止",
|
"chat.completion.paused": "会话已停止",
|
||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"error.enter.api.host": "请输入您的 API 地址",
|
"error.enter.api.host": "请输入您的 API 地址",
|
||||||
"error.enter.api.key": "请输入您的 API 密钥",
|
"error.enter.api.key": "请输入您的 API 密钥",
|
||||||
"error.enter.model": "请选择一个模型",
|
"error.enter.model": "请选择一个模型",
|
||||||
|
"error.enter.name": "请输入知识库名称",
|
||||||
"error.invalid.proxy.url": "无效的代理地址",
|
"error.invalid.proxy.url": "无效的代理地址",
|
||||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||||
"message.code_style": "代码风格",
|
"message.code_style": "代码风格",
|
||||||
@@ -243,21 +249,13 @@
|
|||||||
"upgrade.success.button": "重启",
|
"upgrade.success.button": "重启",
|
||||||
"upgrade.success.content": "重启用以完成升级",
|
"upgrade.success.content": "重启用以完成升级",
|
||||||
"upgrade.success.title": "升级成功",
|
"upgrade.success.title": "升级成功",
|
||||||
"regenerate.confirm": "重新生成会覆盖当前消息"
|
"regenerate.confirm": "重新生成会覆盖当前消息",
|
||||||
|
"copy.success": "复制成功",
|
||||||
|
"error.get_embedding_dimensions": "获取嵌入维度失败"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"title": "小程序"
|
"title": "小程序"
|
||||||
},
|
},
|
||||||
"model": {
|
|
||||||
"pinned": "已固定",
|
|
||||||
"search": "搜索模型...",
|
|
||||||
"stream_output": "流式输出",
|
|
||||||
"type": {
|
|
||||||
"select": "选择模型类型",
|
|
||||||
"text": "文本",
|
|
||||||
"vision": "图像"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
||||||
"keep_alive_time.placeholder": "分钟",
|
"keep_alive_time.placeholder": "分钟",
|
||||||
@@ -360,6 +358,8 @@
|
|||||||
"webdav.password": "WebDAV 密码",
|
"webdav.password": "WebDAV 密码",
|
||||||
"webdav.path": "WebDAV 路径",
|
"webdav.path": "WebDAV 路径",
|
||||||
"webdav.path.placeholder": "/backup",
|
"webdav.path.placeholder": "/backup",
|
||||||
|
"webdav.autoSync": "自动同步",
|
||||||
|
"webdav.minutes": "分钟",
|
||||||
"webdav.restore.button": "从 WebDAV 恢复",
|
"webdav.restore.button": "从 WebDAV 恢复",
|
||||||
"webdav.title": "WebDAV",
|
"webdav.title": "WebDAV",
|
||||||
"webdav.user": "WebDAV 用户名"
|
"webdav.user": "WebDAV 用户名"
|
||||||
@@ -377,17 +377,26 @@
|
|||||||
"general.user_name": "用户名",
|
"general.user_name": "用户名",
|
||||||
"general.user_name.placeholder": "请输入用户名",
|
"general.user_name.placeholder": "请输入用户名",
|
||||||
"general.view_webdav_settings": "查看 WebDAV 设置",
|
"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次空格翻译",
|
"input.auto_translate_with_space": "快速敲击3次空格翻译",
|
||||||
"messages.divider": "消息分割线",
|
"messages.divider": "消息分割线",
|
||||||
"messages.input.paste_long_text_as_file": "长文本粘贴为文件",
|
"messages.input.paste_long_text_as_file": "长文本粘贴为文件",
|
||||||
"messages.input.send_shortcuts": "发送快捷键",
|
"messages.input.send_shortcuts": "发送快捷键",
|
||||||
"messages.input.show_estimated_tokens": "显示预估 Token 数",
|
"messages.input.show_estimated_tokens": "显示预估 Token 数",
|
||||||
|
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||||
"messages.input.title": "输入设置",
|
"messages.input.title": "输入设置",
|
||||||
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||||
"messages.math_engine": "数学公式引擎",
|
"messages.math_engine": "数学公式引擎",
|
||||||
"messages.model.title": "模型设置",
|
"messages.model.title": "模型设置",
|
||||||
"messages.title": "消息设置",
|
"messages.title": "消息设置",
|
||||||
"messages.use_serif_font": "使用衬线字体",
|
"messages.use_serif_font": "使用衬线字体",
|
||||||
|
"messages.input.paste_long_text_threshold": "长文本长度",
|
||||||
"model": "默认模型",
|
"model": "默认模型",
|
||||||
"models.add.add_model": "添加模型",
|
"models.add.add_model": "添加模型",
|
||||||
"models.add.group_name": "分组名称",
|
"models.add.group_name": "分组名称",
|
||||||
@@ -462,7 +471,8 @@
|
|||||||
"reset_to_default": "重置为默认",
|
"reset_to_default": "重置为默认",
|
||||||
"clear_shortcut": "清除快捷键",
|
"clear_shortcut": "清除快捷键",
|
||||||
"toggle_show_assistants": "切换助手显示",
|
"toggle_show_assistants": "切换助手显示",
|
||||||
"toggle_show_topics": "切换话题显示"
|
"toggle_show_topics": "切换话题显示",
|
||||||
|
"copy_last_message": "复制上一条消息"
|
||||||
},
|
},
|
||||||
"theme.auto": "跟随系统",
|
"theme.auto": "跟随系统",
|
||||||
"theme.dark": "深色主题",
|
"theme.dark": "深色主题",
|
||||||
@@ -499,7 +509,72 @@
|
|||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "知识图谱",
|
"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}} 维"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
"input.estimated_tokens.tip": "預估 Token 數",
|
"input.estimated_tokens.tip": "預估 Token 數",
|
||||||
"input.expand": "展開",
|
"input.expand": "展開",
|
||||||
"input.new.context": "清除上下文",
|
"input.new.context": "清除上下文",
|
||||||
"input.new_topic": "新話題 {{Command}}+N",
|
"input.new_topic": "新話題 {{Command}}",
|
||||||
"input.pause": "暫停",
|
"input.pause": "暫停",
|
||||||
"input.placeholder": "在此輸入您的訊息...",
|
"input.placeholder": "在此輸入您的訊息...",
|
||||||
"input.send": "發送",
|
"input.send": "發送",
|
||||||
@@ -80,8 +80,11 @@
|
|||||||
"input.topics": " 話題 ",
|
"input.topics": " 話題 ",
|
||||||
"input.translate": "翻譯成英文",
|
"input.translate": "翻譯成英文",
|
||||||
"input.upload": "上傳圖片或文檔",
|
"input.upload": "上傳圖片或文檔",
|
||||||
|
"input.web_search": "開啟網路搜索",
|
||||||
|
"input.knowledge_base": "知識庫",
|
||||||
"message.new.branch": "新分支",
|
"message.new.branch": "新分支",
|
||||||
"message.new.branch.created": "新分支已建立",
|
"message.new.branch.created": "新分支已建立",
|
||||||
|
"message.regenerate.model": "切換模型",
|
||||||
"message.new.context": "新上下文",
|
"message.new.context": "新上下文",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "代码块可折叠",
|
"settings.code_collapsible": "代码块可折叠",
|
||||||
@@ -128,7 +131,6 @@
|
|||||||
"download": "下載",
|
"download": "下載",
|
||||||
"duplicate": "複製",
|
"duplicate": "複製",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"footnotes": "引用",
|
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
"models": "模型",
|
"models": "模型",
|
||||||
@@ -146,7 +148,8 @@
|
|||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "您",
|
"you": "您",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"add": "添加"
|
"add": "添加",
|
||||||
|
"footnotes": "引用"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"backup.file_format": "備份文件格式錯誤",
|
"backup.file_format": "備份文件格式錯誤",
|
||||||
@@ -220,11 +223,13 @@
|
|||||||
"assistant.added.content": "智能體添加成功",
|
"assistant.added.content": "智能體添加成功",
|
||||||
"backup.failed": "備份失敗",
|
"backup.failed": "備份失敗",
|
||||||
"backup.success": "備份成功",
|
"backup.success": "備份成功",
|
||||||
|
"backup.start.success": "開始備份",
|
||||||
"chat.completion.paused": "聊天完成已暫停",
|
"chat.completion.paused": "聊天完成已暫停",
|
||||||
"copied": "已複製",
|
"copied": "已複製",
|
||||||
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
||||||
"error.enter.api.key": "請先輸入您的 API 密鑰",
|
"error.enter.api.key": "請先輸入您的 API 密鑰",
|
||||||
"error.enter.model": "請先選擇一個模型",
|
"error.enter.model": "請先選擇一個模型",
|
||||||
|
"error.enter.name": "請先輸入知識庫名稱",
|
||||||
"error.invalid.proxy.url": "無效的代理 URL",
|
"error.invalid.proxy.url": "無效的代理 URL",
|
||||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||||
"message.code_style": "程式碼風格",
|
"message.code_style": "程式碼風格",
|
||||||
@@ -243,21 +248,13 @@
|
|||||||
"upgrade.success.button": "重新啟動",
|
"upgrade.success.button": "重新啟動",
|
||||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||||
"upgrade.success.title": "升級成功",
|
"upgrade.success.title": "升級成功",
|
||||||
"regenerate.confirm": "重新生成會覆蓋當前訊息"
|
"regenerate.confirm": "重新生成會覆蓋當前訊息",
|
||||||
|
"copy.success": "複製成功",
|
||||||
|
"error.get_embedding_dimensions": "獲取嵌入維度失敗"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"title": "小程序"
|
"title": "小程序"
|
||||||
},
|
},
|
||||||
"model": {
|
|
||||||
"pinned": "已固定",
|
|
||||||
"search": "搜尋模型...",
|
|
||||||
"stream_output": "串流輸出",
|
|
||||||
"type": {
|
|
||||||
"select": "選擇模型類型",
|
|
||||||
"text": "文字",
|
|
||||||
"vision": "圖像"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||||
"keep_alive_time.placeholder": "分鐘",
|
"keep_alive_time.placeholder": "分鐘",
|
||||||
@@ -360,6 +357,8 @@
|
|||||||
"webdav.password": "WebDAV 密碼",
|
"webdav.password": "WebDAV 密碼",
|
||||||
"webdav.path": "WebDAV Path",
|
"webdav.path": "WebDAV Path",
|
||||||
"webdav.path.placeholder": "/backup",
|
"webdav.path.placeholder": "/backup",
|
||||||
|
"webdav.autoSync": "自動同步",
|
||||||
|
"webdav.minutes": "分鐘",
|
||||||
"webdav.restore.button": "從 WebDAV 恢復",
|
"webdav.restore.button": "從 WebDAV 恢復",
|
||||||
"webdav.title": "WebDAV",
|
"webdav.title": "WebDAV",
|
||||||
"webdav.user": "WebDAV 使用者名稱"
|
"webdav.user": "WebDAV 使用者名稱"
|
||||||
@@ -377,17 +376,26 @@
|
|||||||
"general.user_name": "使用者名稱",
|
"general.user_name": "使用者名稱",
|
||||||
"general.user_name.placeholder": "輸入您的名稱",
|
"general.user_name.placeholder": "輸入您的名稱",
|
||||||
"general.view_webdav_settings": "查看 WebDAV 設定",
|
"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次空格翻譯",
|
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
||||||
"messages.divider": "訊息間顯示分隔線",
|
"messages.divider": "訊息間顯示分隔線",
|
||||||
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
|
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
|
||||||
"messages.input.send_shortcuts": "發送快捷鍵",
|
"messages.input.send_shortcuts": "發送快捷鍵",
|
||||||
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
|
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
|
||||||
|
"messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||||
"messages.input.title": "輸入設定",
|
"messages.input.title": "輸入設定",
|
||||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||||
"messages.math_render_engine": "數學公式引擎",
|
"messages.math_render_engine": "數學公式引擎",
|
||||||
"messages.model.title": "模型設定",
|
"messages.model.title": "模型設定",
|
||||||
"messages.title": "訊息設定",
|
"messages.title": "訊息設定",
|
||||||
"messages.use_serif_font": "使用襯線字體",
|
"messages.use_serif_font": "使用襯線字體",
|
||||||
|
"messages.input.paste_long_text_threshold": "長文本長度",
|
||||||
"model": "預設模型",
|
"model": "預設模型",
|
||||||
"models.add.add_model": "添加模型",
|
"models.add.add_model": "添加模型",
|
||||||
"models.add.group_name": "群組名稱",
|
"models.add.group_name": "群組名稱",
|
||||||
@@ -462,7 +470,8 @@
|
|||||||
"reset_to_default": "重置為預設",
|
"reset_to_default": "重置為預設",
|
||||||
"clear_shortcut": "清除快捷鍵",
|
"clear_shortcut": "清除快捷鍵",
|
||||||
"toggle_show_assistants": "切換助手顯示",
|
"toggle_show_assistants": "切換助手顯示",
|
||||||
"toggle_show_topics": "切換話題顯示"
|
"toggle_show_topics": "切換話題顯示",
|
||||||
|
"copy_last_message": "複製上一条消息"
|
||||||
},
|
},
|
||||||
"theme.auto": "自動",
|
"theme.auto": "自動",
|
||||||
"theme.dark": "深色主題",
|
"theme.dark": "深色主題",
|
||||||
@@ -499,7 +508,72 @@
|
|||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "知識圖譜",
|
"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}} 維"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
|||||||
padding: 7px 15px !important;
|
padding: 7px 15px !important;
|
||||||
border: 0.5px solid transparent;
|
border: 0.5px solid transparent;
|
||||||
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
|
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
|
||||||
|
user-select: none;
|
||||||
.ant-tabs-tab-btn {
|
.ant-tabs-tab-btn {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type GroupTranslations = {
|
|||||||
'zh-CN': string
|
'zh-CN': string
|
||||||
'zh-TW': string
|
'zh-TW': string
|
||||||
'ru-RU': string
|
'ru-RU': string
|
||||||
|
'ja-JP': string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,204 +13,238 @@ export const groupTranslations: GroupTranslations = {
|
|||||||
'en-US': 'My Agents',
|
'en-US': 'My Agents',
|
||||||
'zh-CN': '我的',
|
'zh-CN': '我的',
|
||||||
'zh-TW': '我的',
|
'zh-TW': '我的',
|
||||||
'ru-RU': 'Мои агенты'
|
'ru-RU': 'Мои агенты',
|
||||||
|
'ja-JP': '私のエージェント'
|
||||||
},
|
},
|
||||||
职业: {
|
职业: {
|
||||||
'en-US': 'Career',
|
'en-US': 'Career',
|
||||||
'zh-CN': '职业',
|
'zh-CN': '职业',
|
||||||
'zh-TW': '職業',
|
'zh-TW': '職業',
|
||||||
'ru-RU': 'Карьера'
|
'ru-RU': 'Карьера',
|
||||||
|
'ja-JP': 'キャリア'
|
||||||
},
|
},
|
||||||
商业: {
|
商业: {
|
||||||
'en-US': 'Business',
|
'en-US': 'Business',
|
||||||
'zh-CN': '商业',
|
'zh-CN': '商业',
|
||||||
'zh-TW': '商業',
|
'zh-TW': '商業',
|
||||||
'ru-RU': 'Бизнес'
|
'ru-RU': 'Бизнес',
|
||||||
|
'ja-JP': 'ビジネス'
|
||||||
},
|
},
|
||||||
工具: {
|
工具: {
|
||||||
'en-US': 'Tools',
|
'en-US': 'Tools',
|
||||||
'zh-CN': '工具',
|
'zh-CN': '工具',
|
||||||
'zh-TW': '工具',
|
'zh-TW': '工具',
|
||||||
'ru-RU': 'Инструменты'
|
'ru-RU': 'Инструменты',
|
||||||
|
'ja-JP': 'ツール'
|
||||||
},
|
},
|
||||||
语言: {
|
语言: {
|
||||||
'en-US': 'Language',
|
'en-US': 'Language',
|
||||||
'zh-CN': '语言',
|
'zh-CN': '语言',
|
||||||
'zh-TW': '語言',
|
'zh-TW': '語言',
|
||||||
'ru-RU': 'Язык'
|
'ru-RU': 'Язык',
|
||||||
|
'ja-JP': '言語'
|
||||||
},
|
},
|
||||||
办公: {
|
办公: {
|
||||||
'en-US': 'Office',
|
'en-US': 'Office',
|
||||||
'zh-CN': '办公',
|
'zh-CN': '办公',
|
||||||
'zh-TW': '辦公',
|
'zh-TW': '辦公',
|
||||||
'ru-RU': 'Офис'
|
'ru-RU': 'Офис',
|
||||||
|
'ja-JP': 'オフィス'
|
||||||
},
|
},
|
||||||
通用: {
|
通用: {
|
||||||
'en-US': 'General',
|
'en-US': 'General',
|
||||||
'zh-CN': '通用',
|
'zh-CN': '通用',
|
||||||
'zh-TW': '通用',
|
'zh-TW': '通用',
|
||||||
'ru-RU': 'Общее'
|
'ru-RU': 'Общее',
|
||||||
|
'ja-JP': '一般'
|
||||||
},
|
},
|
||||||
写作: {
|
写作: {
|
||||||
'en-US': 'Writing',
|
'en-US': 'Writing',
|
||||||
'zh-CN': '写作',
|
'zh-CN': '写作',
|
||||||
'zh-TW': '寫作',
|
'zh-TW': '寫作',
|
||||||
'ru-RU': 'Письмо'
|
'ru-RU': 'Письмо',
|
||||||
|
'ja-JP': '書き込み'
|
||||||
},
|
},
|
||||||
精选: {
|
精选: {
|
||||||
'en-US': 'Featured',
|
'en-US': 'Featured',
|
||||||
'zh-CN': '精选',
|
'zh-CN': '精选',
|
||||||
'zh-TW': '精選',
|
'zh-TW': '精選',
|
||||||
'ru-RU': 'Избранное'
|
'ru-RU': 'Избранное',
|
||||||
|
'ja-JP': '特集'
|
||||||
},
|
},
|
||||||
编程: {
|
编程: {
|
||||||
'en-US': 'Programming',
|
'en-US': 'Programming',
|
||||||
'zh-CN': '编程',
|
'zh-CN': '编程',
|
||||||
'zh-TW': '編程',
|
'zh-TW': '編程',
|
||||||
'ru-RU': 'Программирование'
|
'ru-RU': 'Программирование',
|
||||||
|
'ja-JP': 'プログラミング'
|
||||||
},
|
},
|
||||||
情感: {
|
情感: {
|
||||||
'en-US': 'Emotion',
|
'en-US': 'Emotion',
|
||||||
'zh-CN': '情感',
|
'zh-CN': '情感',
|
||||||
'zh-TW': '情感',
|
'zh-TW': '情感',
|
||||||
'ru-RU': 'Эмоции'
|
'ru-RU': 'Эмоции',
|
||||||
|
'ja-JP': '感情'
|
||||||
},
|
},
|
||||||
教育: {
|
教育: {
|
||||||
'en-US': 'Education',
|
'en-US': 'Education',
|
||||||
'zh-CN': '教育',
|
'zh-CN': '教育',
|
||||||
'zh-TW': '教育',
|
'zh-TW': '教育',
|
||||||
'ru-RU': 'Образование'
|
'ru-RU': 'Образование',
|
||||||
|
'ja-JP': '教育'
|
||||||
},
|
},
|
||||||
创意: {
|
创意: {
|
||||||
'en-US': 'Creative',
|
'en-US': 'Creative',
|
||||||
'zh-CN': '创意',
|
'zh-CN': '创意',
|
||||||
'zh-TW': '創意',
|
'zh-TW': '創意',
|
||||||
'ru-RU': 'Креатив'
|
'ru-RU': 'Креатив',
|
||||||
|
'ja-JP': 'クリエイティブ'
|
||||||
},
|
},
|
||||||
学术: {
|
学术: {
|
||||||
'en-US': 'Academic',
|
'en-US': 'Academic',
|
||||||
'zh-CN': '学术',
|
'zh-CN': '学术',
|
||||||
'zh-TW': '學術',
|
'zh-TW': '學術',
|
||||||
'ru-RU': 'Академический'
|
'ru-RU': 'Академический',
|
||||||
|
'ja-JP': 'アカデミック'
|
||||||
},
|
},
|
||||||
设计: {
|
设计: {
|
||||||
'en-US': 'Design',
|
'en-US': 'Design',
|
||||||
'zh-CN': '设计',
|
'zh-CN': '设计',
|
||||||
'zh-TW': '設計',
|
'zh-TW': '設計',
|
||||||
'ru-RU': 'Дизайн'
|
'ru-RU': 'Дизайн',
|
||||||
|
'ja-JP': 'デザイン'
|
||||||
},
|
},
|
||||||
艺术: {
|
艺术: {
|
||||||
'en-US': 'Art',
|
'en-US': 'Art',
|
||||||
'zh-CN': '艺术',
|
'zh-CN': '艺术',
|
||||||
'zh-TW': '藝術',
|
'zh-TW': '藝術',
|
||||||
'ru-RU': 'Искусство'
|
'ru-RU': 'Искусство',
|
||||||
|
'ja-JP': 'アート'
|
||||||
},
|
},
|
||||||
娱乐: {
|
娱乐: {
|
||||||
'en-US': 'Entertainment',
|
'en-US': 'Entertainment',
|
||||||
'zh-CN': '娱乐',
|
'zh-CN': '娱乐',
|
||||||
'zh-TW': '娛樂',
|
'zh-TW': '娛樂',
|
||||||
'ru-RU': 'Развлечения'
|
'ru-RU': 'Развлечения',
|
||||||
|
'ja-JP': 'エンターテイメント'
|
||||||
},
|
},
|
||||||
生活: {
|
生活: {
|
||||||
'en-US': 'Life',
|
'en-US': 'Life',
|
||||||
'zh-CN': '生活',
|
'zh-CN': '生活',
|
||||||
'zh-TW': '生活',
|
'zh-TW': '生活',
|
||||||
'ru-RU': 'Жизнь'
|
'ru-RU': 'Жизнь',
|
||||||
|
'ja-JP': '生活'
|
||||||
},
|
},
|
||||||
医疗: {
|
医疗: {
|
||||||
'en-US': 'Medical',
|
'en-US': 'Medical',
|
||||||
'zh-CN': '医疗',
|
'zh-CN': '医疗',
|
||||||
'zh-TW': '醫療',
|
'zh-TW': '醫療',
|
||||||
'ru-RU': 'Медицина'
|
'ru-RU': 'Медицина',
|
||||||
|
'ja-JP': '医療'
|
||||||
},
|
},
|
||||||
游戏: {
|
游戏: {
|
||||||
'en-US': 'Games',
|
'en-US': 'Games',
|
||||||
'zh-CN': '游戏',
|
'zh-CN': '游戏',
|
||||||
'zh-TW': '遊戲',
|
'zh-TW': '遊戲',
|
||||||
'ru-RU': 'Игры'
|
'ru-RU': 'Игры',
|
||||||
|
'ja-JP': 'ゲーム'
|
||||||
},
|
},
|
||||||
翻译: {
|
翻译: {
|
||||||
'en-US': 'Translation',
|
'en-US': 'Translation',
|
||||||
'zh-CN': '翻译',
|
'zh-CN': '翻译',
|
||||||
'zh-TW': '翻譯',
|
'zh-TW': '翻譯',
|
||||||
'ru-RU': 'Перевод'
|
'ru-RU': 'Перевод',
|
||||||
|
'ja-JP': '翻訳'
|
||||||
},
|
},
|
||||||
音乐: {
|
音乐: {
|
||||||
'en-US': 'Music',
|
'en-US': 'Music',
|
||||||
'zh-CN': '音乐',
|
'zh-CN': '音乐',
|
||||||
'zh-TW': '音樂',
|
'zh-TW': '音樂',
|
||||||
'ru-RU': 'Музыка'
|
'ru-RU': 'Музыка',
|
||||||
|
'ja-JP': '音楽'
|
||||||
},
|
},
|
||||||
点评: {
|
点评: {
|
||||||
'en-US': 'Review',
|
'en-US': 'Review',
|
||||||
'zh-CN': '点评',
|
'zh-CN': '点评',
|
||||||
'zh-TW': '點評',
|
'zh-TW': '點評',
|
||||||
'ru-RU': 'Обзор'
|
'ru-RU': 'Обзор',
|
||||||
|
'ja-JP': 'レビュー'
|
||||||
},
|
},
|
||||||
文案: {
|
文案: {
|
||||||
'en-US': 'Copywriting',
|
'en-US': 'Copywriting',
|
||||||
'zh-CN': '文案',
|
'zh-CN': '文案',
|
||||||
'zh-TW': '文案',
|
'zh-TW': '文案',
|
||||||
'ru-RU': 'Копирайтинг'
|
'ru-RU': 'Копирайтинг',
|
||||||
|
'ja-JP': 'コピーライティング'
|
||||||
},
|
},
|
||||||
百科: {
|
百科: {
|
||||||
'en-US': 'Encyclopedia',
|
'en-US': 'Encyclopedia',
|
||||||
'zh-CN': '百科',
|
'zh-CN': '百科',
|
||||||
'zh-TW': '百科',
|
'zh-TW': '百科',
|
||||||
'ru-RU': 'Энциклопедия'
|
'ru-RU': 'Энциклопедия',
|
||||||
|
'ja-JP': '百科事典'
|
||||||
},
|
},
|
||||||
健康: {
|
健康: {
|
||||||
'en-US': 'Health',
|
'en-US': 'Health',
|
||||||
'zh-CN': '健康',
|
'zh-CN': '健康',
|
||||||
'zh-TW': '健康',
|
'zh-TW': '健康',
|
||||||
'ru-RU': 'Здоровье'
|
'ru-RU': 'Здоровье',
|
||||||
|
'ja-JP': '健康'
|
||||||
},
|
},
|
||||||
营销: {
|
营销: {
|
||||||
'en-US': 'Marketing',
|
'en-US': 'Marketing',
|
||||||
'zh-CN': '营销',
|
'zh-CN': '营销',
|
||||||
'zh-TW': '營銷',
|
'zh-TW': '營銷',
|
||||||
'ru-RU': 'Маркетинг'
|
'ru-RU': 'Маркетинг',
|
||||||
|
'ja-JP': 'マーケティング'
|
||||||
},
|
},
|
||||||
科学: {
|
科学: {
|
||||||
'en-US': 'Science',
|
'en-US': 'Science',
|
||||||
'zh-CN': '科学',
|
'zh-CN': '科学',
|
||||||
'zh-TW': '科學',
|
'zh-TW': '科學',
|
||||||
'ru-RU': 'Наука'
|
'ru-RU': 'Наука',
|
||||||
|
'ja-JP': '科学'
|
||||||
},
|
},
|
||||||
分析: {
|
分析: {
|
||||||
'en-US': 'Analysis',
|
'en-US': 'Analysis',
|
||||||
'zh-CN': '分析',
|
'zh-CN': '分析',
|
||||||
'zh-TW': '分析',
|
'zh-TW': '分析',
|
||||||
'ru-RU': 'Анализ'
|
'ru-RU': 'Анализ',
|
||||||
|
'ja-JP': '分析'
|
||||||
},
|
},
|
||||||
法律: {
|
法律: {
|
||||||
'en-US': 'Legal',
|
'en-US': 'Legal',
|
||||||
'zh-CN': '法律',
|
'zh-CN': '法律',
|
||||||
'zh-TW': '法律',
|
'zh-TW': '法律',
|
||||||
'ru-RU': 'Право'
|
'ru-RU': 'Право',
|
||||||
|
'ja-JP': '法律'
|
||||||
},
|
},
|
||||||
咨询: {
|
咨询: {
|
||||||
'en-US': 'Consulting',
|
'en-US': 'Consulting',
|
||||||
'zh-CN': '咨询',
|
'zh-CN': '咨询',
|
||||||
'zh-TW': '諮詢',
|
'zh-TW': '諮詢',
|
||||||
'ru-RU': 'Консалтинг'
|
'ru-RU': 'Консалтинг',
|
||||||
|
'ja-JP': 'コンサルティング'
|
||||||
},
|
},
|
||||||
金融: {
|
金融: {
|
||||||
'en-US': 'Finance',
|
'en-US': 'Finance',
|
||||||
'zh-CN': '金融',
|
'zh-CN': '金融',
|
||||||
'zh-TW': '金融',
|
'zh-TW': '金融',
|
||||||
'ru-RU': 'Финансы'
|
'ru-RU': 'Финансы',
|
||||||
|
'ja-JP': '金融'
|
||||||
},
|
},
|
||||||
旅游: {
|
旅游: {
|
||||||
'en-US': 'Travel',
|
'en-US': 'Travel',
|
||||||
'zh-CN': '旅游',
|
'zh-CN': '旅游',
|
||||||
'zh-TW': '旅遊',
|
'zh-TW': '旅遊',
|
||||||
'ru-RU': 'Путешествия'
|
'ru-RU': 'Путешествия',
|
||||||
|
'ja-JP': '旅行'
|
||||||
},
|
},
|
||||||
管理: {
|
管理: {
|
||||||
'en-US': 'Management',
|
'en-US': 'Management',
|
||||||
'zh-CN': '管理',
|
'zh-CN': '管理',
|
||||||
'zh-TW': '管理',
|
'zh-TW': '管理',
|
||||||
'ru-RU': 'Управление'
|
'ru-RU': 'Управление',
|
||||||
|
'ja-JP': '管理'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons'
|
||||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAgents } from '@renderer/hooks/useAgents'
|
||||||
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
import { Col } from 'antd'
|
import { Col } from 'antd'
|
||||||
|
|||||||
@@ -5,16 +5,26 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
app: MinAppType
|
app: MinAppType
|
||||||
|
onClick?: () => void
|
||||||
|
size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const App: FC<Props> = ({ app }) => {
|
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
|
||||||
const onClick = () => {
|
const handleClick = () => {
|
||||||
MinApp.start(app)
|
MinApp.start(app)
|
||||||
|
onClick?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onClick={onClick}>
|
<Container onClick={handleClick}>
|
||||||
<AppIcon src={app.logo} style={{ border: app.bodered ? '0.5px solid var(--color-border)' : 'none' }} />
|
<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>
|
<AppTitle>{app.name}</AppTitle>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@@ -26,12 +36,10 @@ const Container = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 65px;
|
overflow: hidden;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AppIcon = styled.img`
|
const AppIcon = styled.img`
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
@@ -43,6 +51,7 @@ const AppTitle = styled.div`
|
|||||||
color: var(--color-text-soft);
|
color: var(--color-text-soft);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ const ContentContainer = styled.div`
|
|||||||
|
|
||||||
const AppsContainer = styled.div`
|
const AppsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 900px;
|
min-width: 930px;
|
||||||
max-width: 900px;
|
max-width: 930px;
|
||||||
flex-direction: row;
|
max-height: 500px;
|
||||||
flex-wrap: wrap;
|
display: grid;
|
||||||
align-content: flex-start;
|
grid-template-columns: repeat(8, minmax(90px, 1fr));
|
||||||
gap: 50px;
|
gap: 25px 25px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default AppsPage
|
export default AppsPage
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ const SideNav = styled.div`
|
|||||||
width: var(--assistants-width);
|
width: var(--assistants-width);
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
.ant-menu {
|
.ant-menu {
|
||||||
border-inline-end: none !important;
|
border-inline-end: none !important;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ let _message: Message | undefined
|
|||||||
const TopicsPage: FC = () => {
|
const TopicsPage: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [search, setSearch] = useState(_search)
|
const [search, setSearch] = useState(_search)
|
||||||
|
const [searchKeywords, setSearchKeywords] = useState(_search)
|
||||||
const [stack, setStack] = useState<Route[]>(_stack)
|
const [stack, setStack] = useState<Route[]>(_stack)
|
||||||
const [topic, setTopic] = useState<Topic | undefined>(_topic)
|
const [topic, setTopic] = useState<Topic | undefined>(_topic)
|
||||||
const [message, setMessage] = useState<Message | undefined>(_message)
|
const [message, setMessage] = useState<Message | undefined>(_message)
|
||||||
@@ -40,6 +41,7 @@ const TopicsPage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onSearch = () => {
|
const onSearch = () => {
|
||||||
|
setSearchKeywords(search)
|
||||||
setStack(['topics', 'search'])
|
setStack(['topics', 'search'])
|
||||||
setTopic(undefined)
|
setTopic(undefined)
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ const TopicsPage: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
||||||
<SearchResults
|
<SearchResults
|
||||||
keywords={isShow('search') ? search : ''}
|
keywords={isShow('search') ? searchKeywords : ''}
|
||||||
onMessageClick={onMessageClick}
|
onMessageClick={onMessageClick}
|
||||||
onTopicClick={onTopicClick}
|
onTopicClick={onTopicClick}
|
||||||
style={{ display: isShow('search') }}
|
style={{ display: isShow('search') }}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ArrowRightOutlined } from '@ant-design/icons'
|
import { ArrowRightOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
||||||
import { locateToMessage } from '@renderer/services/MessagesService'
|
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||||
import NavigationService from '@renderer/services/NavigationService'
|
import NavigationService from '@renderer/services/NavigationService'
|
||||||
@@ -15,6 +16,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
|
|
||||||
const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
||||||
const navigate = NavigationService.navigate!
|
const navigate = NavigationService.navigate!
|
||||||
|
const { messageStyle } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@@ -22,7 +24,7 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagesContainer {...props}>
|
<MessagesContainer {...props} className={messageStyle}>
|
||||||
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
|
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
|
||||||
<MessageItem message={message} />
|
<MessageItem message={message} />
|
||||||
<Button
|
<Button
|
||||||
@@ -45,6 +47,7 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
|||||||
const MessagesContainer = styled.div`
|
const MessagesContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|||||||
@@ -11,19 +11,16 @@ interface Props {
|
|||||||
files: FileType[]
|
files: FileType[]
|
||||||
setFiles: (files: FileType[]) => void
|
setFiles: (files: FileType[]) => void
|
||||||
ToolbarButton: any
|
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 { t } = useTranslation()
|
||||||
const extensions = isVisionModel(model)
|
const extensions = isVisionModel(model)
|
||||||
? [...imageExts, ...documentExts, ...textExts]
|
? [...imageExts, ...documentExts, ...textExts]
|
||||||
: [...documentExts, ...textExts]
|
: [...documentExts, ...textExts]
|
||||||
|
|
||||||
const onSelectFile = async () => {
|
const onSelectFile = async () => {
|
||||||
if (files.length > 0) {
|
|
||||||
return setFiles([])
|
|
||||||
}
|
|
||||||
|
|
||||||
const _files = await window.api.file.select({
|
const _files = await window.api.file.select({
|
||||||
properties: ['openFile', 'multiSelections'],
|
properties: ['openFile', 'multiSelections'],
|
||||||
filters: [
|
filters: [
|
||||||
@@ -34,12 +31,14 @@ const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton })
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
_files && setFiles(_files)
|
if (_files) {
|
||||||
|
setFiles([...files, ..._files])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
|
<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' }} />
|
<PaperClipOutlined style={{ rotate: '135deg' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import {
|
|||||||
FormOutlined,
|
FormOutlined,
|
||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
QuestionCircleOutlined
|
QuestionCircleOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { PicCenterOutlined } from '@ant-design/icons'
|
import { PicCenterOutlined } from '@ant-design/icons'
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
import TranslateButton from '@renderer/components/TranslateButton'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||||
import { isVisionModel } from '@renderer/config/models'
|
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
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 { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
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 { translateText } from '@renderer/services/TranslateService'
|
||||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
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 { delay, getFileExtension, uuid } from '@renderer/utils'
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||||
@@ -37,6 +37,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import AttachmentButton from './AttachmentButton'
|
import AttachmentButton from './AttachmentButton'
|
||||||
import AttachmentPreview from './AttachmentPreview'
|
import AttachmentPreview from './AttachmentPreview'
|
||||||
|
import KnowledgeBaseButton from './KnowledgeBaseButton'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
import TokenCount from './TokenCount'
|
import TokenCount from './TokenCount'
|
||||||
|
|
||||||
@@ -47,15 +48,17 @@ interface Props {
|
|||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
let _files: FileType[] = []
|
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 [text, setText] = useState(_text)
|
||||||
const [inputFocus, setInputFocus] = useState(false)
|
const [inputFocus, setInputFocus] = useState(false)
|
||||||
const { addTopic, model, setModel } = useAssistant(assistant.id)
|
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id)
|
||||||
const {
|
const {
|
||||||
sendMessageShortcut,
|
sendMessageShortcut,
|
||||||
fontSize,
|
fontSize,
|
||||||
pasteLongTextAsFile,
|
pasteLongTextAsFile,
|
||||||
|
pasteLongTextThreshold,
|
||||||
showInputEstimatedTokens,
|
showInputEstimatedTokens,
|
||||||
clickAssistantToShowTopic,
|
clickAssistantToShowTopic,
|
||||||
language,
|
language,
|
||||||
@@ -76,6 +79,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
|
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
||||||
|
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
@@ -85,16 +89,19 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
|
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
|
||||||
[estimateTextTokens, showInputEstimatedTokens, text]
|
[estimateTextTokens, showInputEstimatedTokens, text]
|
||||||
)
|
)
|
||||||
|
const newTopicShortcut = useShortcutDisplay('new_topic')
|
||||||
|
const inputEmpty = isEmpty(text.trim()) && files.length === 0
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
_files = files
|
_files = files
|
||||||
|
_base = selectedKnowledgeBase
|
||||||
|
|
||||||
const sendMessage = useCallback(async () => {
|
const sendMessage = useCallback(async () => {
|
||||||
if (generating) {
|
if (generating) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEmpty(text.trim())) {
|
if (inputEmpty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +116,10 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedKnowledgeBase) {
|
||||||
|
message.knowledgeBaseIds = [selectedKnowledgeBase.id]
|
||||||
|
}
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
message.files = await FileManager.uploadFiles(files)
|
message.files = await FileManager.uploadFiles(files)
|
||||||
}
|
}
|
||||||
@@ -121,7 +132,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
setTimeout(() => resizeTextArea(), 0)
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
|
||||||
setExpend(false)
|
setExpend(false)
|
||||||
}, [assistant.id, assistant.topics, generating, files, text])
|
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
|
||||||
|
|
||||||
const translate = async () => {
|
const translate = async () => {
|
||||||
if (isTranslating) {
|
if (isTranslating) {
|
||||||
@@ -188,6 +199,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
||||||
|
sendMessage()
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNewTopic = useCallback(async () => {
|
const addNewTopic = useCallback(async () => {
|
||||||
@@ -285,7 +301,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const item = event.clipboardData?.items[0]
|
const item = event.clipboardData?.items[0]
|
||||||
if (item && item.kind === 'string' && item.type === 'text/plain') {
|
if (item && item.kind === 'string' && item.type === 'text/plain') {
|
||||||
item.getAsString(async (pasteText) => {
|
item.getAsString(async (pasteText) => {
|
||||||
if (pasteText.length > 1500) {
|
if (pasteText.length > pasteLongTextThreshold) {
|
||||||
const tempFilePath = await window.api.file.create('pasted_text.txt')
|
const tempFilePath = await window.api.file.create('pasted_text.txt')
|
||||||
await window.api.file.write(tempFilePath, pasteText)
|
await window.api.file.write(tempFilePath, pasteText)
|
||||||
const selectedFile = await window.api.file.get(tempFilePath)
|
const selectedFile = await window.api.file.get(tempFilePath)
|
||||||
@@ -297,7 +313,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[pasteLongTextAsFile, supportExts, text]
|
[pasteLongTextAsFile, pasteLongTextThreshold, supportExts, text]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
@@ -365,6 +381,12 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
||||||
|
|
||||||
|
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
||||||
|
setSelectedKnowledgeBase(base)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||||
@@ -377,7 +399,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
autoFocus
|
autoFocus
|
||||||
contextMenu="true"
|
contextMenu="true"
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
rows={isBubbleStyle ? 2 : 1}
|
rows={textareaRows}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
style={{ fontSize }}
|
style={{ fontSize }}
|
||||||
styles={{ textarea: TextareaStyle }}
|
styles={{ textarea: TextareaStyle }}
|
||||||
@@ -390,11 +412,22 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
/>
|
/>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarMenu>
|
<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}>
|
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||||
<FormOutlined />
|
<FormOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</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>
|
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('chat.input.clear.content')}
|
title={t('chat.input.clear.content')}
|
||||||
@@ -418,7 +451,19 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
<ControlOutlined />
|
<ControlOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</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}>
|
<ToolbarButton type="text" onClick={onNewContext}>
|
||||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||||
<PicCenterOutlined />
|
<PicCenterOutlined />
|
||||||
@@ -448,7 +493,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
||||||
</ToolbarMenu>
|
</ToolbarMenu>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</InputBarContainer>
|
</InputBarContainer>
|
||||||
@@ -462,7 +507,7 @@ const Container = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const InputBarContainer = styled.div`
|
const InputBarContainer = styled.div`
|
||||||
border: 1px solid var(--color-border-soft);
|
border: 1px solid var(--color-border);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 20px 15px 20px;
|
margin: 0 20px 15px 20px;
|
||||||
|
|||||||
83
src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx
Normal 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
|
||||||
@@ -8,6 +8,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import Artifacts from './Artifacts'
|
import Artifacts from './Artifacts'
|
||||||
import Mermaid from './Mermaid'
|
import Mermaid from './Mermaid'
|
||||||
|
import SvgPreview from './SvgPreview'
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
children: string
|
children: string
|
||||||
@@ -79,8 +80,20 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
return <Mermaid chart={children} />
|
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 ? (
|
return match ? (
|
||||||
<div className="code-block">
|
<CodeBlockWrapper className="code-block">
|
||||||
<CodeHeader>
|
<CodeHeader>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
{codeCollapsible && shouldShowExpandButton && (
|
{codeCollapsible && shouldShowExpandButton && (
|
||||||
@@ -118,7 +131,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
</CodeFooter>
|
</CodeFooter>
|
||||||
)}
|
)}
|
||||||
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
|
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
|
||||||
</div>
|
</CodeBlockWrapper>
|
||||||
) : (
|
) : (
|
||||||
<code className={className}>{children}</code>
|
<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 }>`
|
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
|
||||||
.shiki {
|
.shiki {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
|
|||||||
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { escapeBrackets } from '@renderer/utils/formula'
|
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -19,7 +19,7 @@ import ImagePreview from './ImagePreview'
|
|||||||
import Link from './Link'
|
import Link from './Link'
|
||||||
|
|
||||||
const ALLOWED_ELEMENTS =
|
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 {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@@ -35,7 +35,7 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
const empty = isEmpty(message.content)
|
const empty = isEmpty(message.content)
|
||||||
const paused = message.status === 'paused'
|
const paused = message.status === 'paused'
|
||||||
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
||||||
return escapeBrackets(content)
|
return removeSvgEmptyLines(escapeBrackets(content))
|
||||||
}, [message.content, message.status, t])
|
}, [message.content, message.status, t])
|
||||||
|
|
||||||
const rehypePlugins = useMemo(() => {
|
const rehypePlugins = useMemo(() => {
|
||||||
@@ -52,7 +52,6 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
className="markdown"
|
className="markdown"
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
remarkPlugins={[remarkMath, remarkGfm]}
|
remarkPlugins={[remarkMath, remarkGfm]}
|
||||||
disallowedElements={mathEngine === 'KaTeX' ? ['style'] : []}
|
|
||||||
components={
|
components={
|
||||||
{
|
{
|
||||||
a: Link,
|
a: Link,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
|||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const mermaidId = `mermaid-popup-${Date.now()}`
|
const mermaidId = `mermaid-popup-${Date.now()}`
|
||||||
|
const [activeTab, setActiveTab] = useState('preview')
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
setOpen(false)
|
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(() => {
|
useEffect(() => {
|
||||||
window?.mermaid?.contentLoaded()
|
window?.mermaid?.contentLoaded()
|
||||||
}, [])
|
}, [])
|
||||||
@@ -101,11 +107,18 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
|||||||
centered
|
centered
|
||||||
footer={[
|
footer={[
|
||||||
<Space key="download-buttons">
|
<Space key="download-buttons">
|
||||||
|
{activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>}
|
||||||
|
{activeTab === 'preview' && (
|
||||||
|
<>
|
||||||
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
|
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
|
||||||
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
|
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
]}>
|
]}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={(key) => setActiveTab(key)}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'preview',
|
key: 'preview',
|
||||||
|
|||||||
16
src/renderer/src/pages/home/Markdown/SvgPreview.tsx
Normal 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
|
||||||
@@ -35,9 +35,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
|||||||
|
|
||||||
const getUserName = useCallback(() => {
|
const getUserName = useCallback(() => {
|
||||||
if (isLocalAi && message.role !== 'user') return APP_NAME
|
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')
|
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'
|
const isAssistantMessage = message.role === 'assistant'
|
||||||
|
|
||||||
|
|||||||
@@ -205,13 +205,15 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||||
onConfirm={onDeleteAndRegenerate}>
|
onConfirm={onDeleteAndRegenerate}>
|
||||||
|
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||||
<ActionButton className="message-action-button">
|
<ActionButton className="message-action-button">
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
{canRegenerate && (
|
{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}>
|
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
|
||||||
<i className="iconfont icon-at1"></i>
|
<i className="iconfont icon-at1"></i>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
|
import { t } from 'i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ message, isLastMessage }) => {
|
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') {
|
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 (
|
return (
|
||||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
<MessageMetadata className={`message-tokens ${hasMetrics ? 'has-metrics' : ''}`} onClick={locateMessage}>
|
||||||
Tokens: {message?.usage?.total_tokens} | ↑{message?.usage?.prompt_tokens} | ↓{message?.usage?.completion_tokens}
|
<span className="metrics">{metrixs}</span>
|
||||||
|
<span className="tokens">
|
||||||
|
Tokens: {message?.usage?.total_tokens} ↑{message?.usage?.prompt_tokens} ↓{message?.usage?.completion_tokens}
|
||||||
|
</span>
|
||||||
</MessageMetadata>
|
</MessageMetadata>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -38,11 +55,30 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MessageMetadata = styled.div`
|
const MessageMetadata = styled.div`
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
user-select: text;
|
user-select: text;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.metrics {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-metrics:hover {
|
||||||
|
.metrics {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default MessgeTokens
|
export default MessgeTokens
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { getTopic, TopicManager } from '@renderer/hooks/useTopic'
|
import { getTopic, TopicManager } from '@renderer/hooks/useTopic'
|
||||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||||
@@ -266,6 +267,14 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
}, 300)
|
}, 300)
|
||||||
}, [displayMessages, hasMore, isLoadingMore, messages])
|
}, [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 (
|
return (
|
||||||
<Container
|
<Container
|
||||||
id="messages"
|
id="messages"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { SearchOutlined } from '@ant-design/icons'
|
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import AppStorePopover from '@renderer/components/Popups/AppStorePopover'
|
||||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||||
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@@ -43,22 +44,22 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
|||||||
<Navbar>
|
<Navbar>
|
||||||
{showAssistants && (
|
{showAssistants && (
|
||||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
<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" />
|
<i className="iconfont icon-hide-sidebar" />
|
||||||
</NewButton>
|
</NavbarIcon>
|
||||||
<NewButton onClick={() => SearchPopup.show()}>
|
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||||
<SearchOutlined />
|
<FormOutlined />
|
||||||
</NewButton>
|
</NavbarIcon>
|
||||||
</NavbarLeft>
|
</NavbarLeft>
|
||||||
)}
|
)}
|
||||||
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
|
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
|
||||||
<HStack alignItems="center">
|
<HStack alignItems="center">
|
||||||
{!showAssistants && (
|
{!showAssistants && (
|
||||||
<NewButton
|
<NavbarIcon
|
||||||
onClick={() => toggleShowAssistants()}
|
onClick={() => toggleShowAssistants()}
|
||||||
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
|
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
|
||||||
<i className="iconfont icon-show-sidebar" />
|
<i className="iconfont icon-show-sidebar" />
|
||||||
</NewButton>
|
</NavbarIcon>
|
||||||
)}
|
)}
|
||||||
<TitleText
|
<TitleText
|
||||||
style={{ marginRight: 10, cursor: 'pointer' }}
|
style={{ marginRight: 10, cursor: 'pointer' }}
|
||||||
@@ -69,10 +70,18 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
|||||||
<SelectModelButton assistant={assistant} />
|
<SelectModelButton assistant={assistant} />
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack alignItems="center">
|
<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' && (
|
{topicPosition === 'right' && (
|
||||||
<NewButton onClick={toggleShowTopics}>
|
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}>
|
||||||
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
||||||
</NewButton>
|
</NavbarIcon>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</NavbarRight>
|
</NavbarRight>
|
||||||
@@ -80,7 +89,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NewButton = styled.div`
|
export const NavbarIcon = styled.div`
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
@@ -100,6 +109,9 @@ export const NewButton = styled.div`
|
|||||||
&.icon-a-darkmode {
|
&.icon-a-darkmode {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
&.icon-appstore {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.anticon {
|
.anticon {
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
@@ -115,6 +127,7 @@ const TitleText = styled.span`
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
user-select: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default HeaderNavbar
|
export default HeaderNavbar
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
|
||||||
import DragableList from '@renderer/components/DragableList'
|
import DragableList from '@renderer/components/DragableList'
|
||||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAgents } from '@renderer/hooks/useAgents'
|
||||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
@@ -175,6 +175,7 @@ const Container = styled(Scrollbar)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 11px;
|
padding-top: 11px;
|
||||||
|
user-select: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AssistantItem = styled.div`
|
const AssistantItem = styled.div`
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
import {
|
||||||
|
DEFAULT_CONTEXTCOUNT,
|
||||||
|
DEFAULT_MAX_TOKENS,
|
||||||
|
DEFAULT_TEMPERATURE,
|
||||||
|
isMac,
|
||||||
|
isWindows
|
||||||
|
} from '@renderer/config/constant'
|
||||||
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@@ -9,7 +15,6 @@ import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@r
|
|||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setAutoTranslateWithSpace,
|
setAutoTranslateWithSpace,
|
||||||
setClickAssistantToShowTopic,
|
|
||||||
setCodeCollapsible,
|
setCodeCollapsible,
|
||||||
setCodeShowLineNumbers,
|
setCodeShowLineNumbers,
|
||||||
setCodeStyle,
|
setCodeStyle,
|
||||||
@@ -18,13 +23,13 @@ import {
|
|||||||
setMessageFont,
|
setMessageFont,
|
||||||
setMessageStyle,
|
setMessageStyle,
|
||||||
setPasteLongTextAsFile,
|
setPasteLongTextAsFile,
|
||||||
|
setPasteLongTextThreshold,
|
||||||
setRenderInputMessageAsMarkdown,
|
setRenderInputMessageAsMarkdown,
|
||||||
setShowInputEstimatedTokens,
|
setShowInputEstimatedTokens,
|
||||||
setShowMessageDivider,
|
setShowMessageDivider
|
||||||
setShowTopicTime
|
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { Assistant, AssistantSettings, ThemeMode } from '@renderer/types'
|
import { Assistant, AssistantSettings, ThemeMode } from '@renderer/types'
|
||||||
import { Col, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
import { Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@@ -58,11 +63,8 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
codeShowLineNumbers,
|
codeShowLineNumbers,
|
||||||
codeCollapsible,
|
codeCollapsible,
|
||||||
mathEngine,
|
mathEngine,
|
||||||
topicPosition,
|
|
||||||
showTopicTime,
|
|
||||||
clickAssistantToShowTopic,
|
|
||||||
autoTranslateWithSpace,
|
autoTranslateWithSpace,
|
||||||
setTopicPosition
|
pasteLongTextThreshold
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
|
|
||||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
@@ -160,7 +162,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('model.stream_output')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('models.stream_output')}</SettingRowTitleSmall>
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={streamOutput}
|
checked={streamOutput}
|
||||||
@@ -320,6 +322,23 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
|
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
{pasteLongTextAsFile && (
|
||||||
|
<>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_threshold')}</SettingRowTitleSmall>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={500}
|
||||||
|
max={10000}
|
||||||
|
step={100}
|
||||||
|
value={pasteLongTextThreshold}
|
||||||
|
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('settings.messages.markdown_rendering_input_message')}</SettingRowTitleSmall>
|
||||||
@@ -351,49 +370,15 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
menuItemSelectedIcon={<CheckOutlined />}
|
menuItemSelectedIcon={<CheckOutlined />}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'Enter', label: 'Enter' },
|
{ value: 'Enter', label: 'Enter' },
|
||||||
{ value: 'Shift+Enter', label: `Shift + Enter` },
|
{ value: 'Shift+Enter', label: 'Shift + Enter' },
|
||||||
{ value: 'Ctrl+Enter', label: `Ctrl + Enter` }
|
{ value: 'Ctrl+Enter', label: 'Ctrl + Enter' },
|
||||||
|
{ value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` }
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setSendMessageShortcut(value)}
|
onChange={(value) => setSendMessageShortcut(value)}
|
||||||
style={{ width: 135 }}
|
style={{ width: 135 }}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
<SettingGroup>
|
|
||||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.display.title')}</SettingSubtitle>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
|
||||||
<Select
|
|
||||||
defaultValue={topicPosition || 'right'}
|
|
||||||
style={{ width: 135 }}
|
|
||||||
onChange={setTopicPosition}
|
|
||||||
size="small"
|
|
||||||
options={[
|
|
||||||
{ value: 'left', label: t('settings.topic.position.left') },
|
|
||||||
{ value: 'right', label: t('settings.topic.position.right') }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
{topicPosition === 'left' && (
|
|
||||||
<>
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.advanced.auto_switch_to_topics')}</SettingRowTitle>
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
checked={clickAssistantToShowTopic}
|
|
||||||
onChange={(checked) => dispatch(setClickAssistantToShowTopic(checked))}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.topic.show.time')}</SettingRowTitle>
|
|
||||||
<Switch size="small" checked={showTopicTime} onChange={(checked) => dispatch(setShowTopicTime(checked))} />
|
|
||||||
</SettingRow>
|
|
||||||
</SettingGroup>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ const Container = styled(Scrollbar)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 11px;
|
padding-top: 11px;
|
||||||
|
user-select: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopicListItem = styled.div`
|
const TopicListItem = styled.div`
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ const Segmented = styled(AntSegmented)`
|
|||||||
height: 34px;
|
height: 34px;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.ant-segmented-item-selected {
|
.ant-segmented-item-selected {
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
|||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import { isVisionModel } from '@renderer/config/models'
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@@ -32,9 +33,14 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownButton size="small" type="default" onClick={onSelectModel}>
|
<DropdownButton size="small" type="default" onClick={onSelectModel}>
|
||||||
|
<ButtonContent>
|
||||||
<ModelAvatar model={model} size={20} />
|
<ModelAvatar model={model} size={20} />
|
||||||
<ModelName>{model ? model.name : t('button.select_model')}</ModelName>
|
<ModelName>
|
||||||
|
{model ? model.name : t('button.select_model')} |{' '}
|
||||||
|
{t(`provider.${model?.provider}`, { defaultValue: getProviderByModel(model)?.name })}
|
||||||
|
</ModelName>
|
||||||
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
|
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
|
||||||
|
</ButtonContent>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -49,6 +55,12 @@ const DropdownButton = styled(Button)`
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const ButtonContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
const ModelName = styled.span`
|
const ModelName = styled.span`
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
443
src/renderer/src/pages/knowledge/KnowledgeContent.tsx
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
GlobalOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
SearchOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
|
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
|
||||||
|
import { Alert, Button, Card, Divider, message, Tag, Typography, Upload } from 'antd'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
|
||||||
|
import StatusIcon from './components/StatusIcon'
|
||||||
|
|
||||||
|
const { Dragger } = Upload
|
||||||
|
const { Title } = Typography
|
||||||
|
|
||||||
|
interface KnowledgeContentProps {
|
||||||
|
selectedBase: KnowledgeBase
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md', '.mdx']
|
||||||
|
|
||||||
|
const FlexColumn = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FlexAlignCenter = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ClickableSpan = styled.span`
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileIcon = styled(FileTextOutlined)`
|
||||||
|
font-size: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const BottomSpacer = styled.div`
|
||||||
|
min-height: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const {
|
||||||
|
base,
|
||||||
|
noteItems,
|
||||||
|
fileItems,
|
||||||
|
urlItems,
|
||||||
|
sitemapItems,
|
||||||
|
directoryItems,
|
||||||
|
addFiles,
|
||||||
|
updateNoteContent,
|
||||||
|
addUrl,
|
||||||
|
addSitemap,
|
||||||
|
removeItem,
|
||||||
|
getProcessingStatus,
|
||||||
|
addNote,
|
||||||
|
addDirectory
|
||||||
|
} = useKnowledge(selectedBase.id || '')
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddFile = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.multiple = true
|
||||||
|
input.accept = fileTypes.join(',')
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const files = (e.target as HTMLInputElement).files
|
||||||
|
files && handleDrop(Array.from(files))
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = async (files: File[]) => {
|
||||||
|
if (files) {
|
||||||
|
const _files: FileType[] = files.map((file) => ({
|
||||||
|
id: file.name,
|
||||||
|
name: file.name,
|
||||||
|
path: file.path,
|
||||||
|
size: file.size,
|
||||||
|
ext: `.${file.name.split('.').pop()}`,
|
||||||
|
count: 1,
|
||||||
|
origin_name: file.name,
|
||||||
|
type: file.type as FileTypes,
|
||||||
|
created_at: new Date()
|
||||||
|
}))
|
||||||
|
console.debug('[KnowledgeContent] Uploading files:', _files, files)
|
||||||
|
const uploadedFiles = await FileManager.uploadFiles(_files)
|
||||||
|
addFiles(uploadedFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddUrl = async () => {
|
||||||
|
const url = await PromptPopup.show({
|
||||||
|
title: t('knowledge_base.add_url'),
|
||||||
|
message: '',
|
||||||
|
inputPlaceholder: t('knowledge_base.url_placeholder'),
|
||||||
|
inputProps: {
|
||||||
|
maxLength: 1000,
|
||||||
|
rows: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
if (urlItems.find((item) => item.content === url)) {
|
||||||
|
message.success(t('knowledge_base.url_added'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addUrl(url)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid URL:', url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddSitemap = async () => {
|
||||||
|
const url = await PromptPopup.show({
|
||||||
|
title: t('knowledge_base.add_sitemap'),
|
||||||
|
message: '',
|
||||||
|
inputPlaceholder: t('knowledge_base.sitemap_placeholder'),
|
||||||
|
inputProps: {
|
||||||
|
maxLength: 1000,
|
||||||
|
rows: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
if (sitemapItems.find((item) => item.content === url)) {
|
||||||
|
message.success(t('knowledge_base.sitemap_added'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addSitemap(url)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid Sitemap URL:', url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddNote = async () => {
|
||||||
|
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
|
||||||
|
note && addNote(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditNote = async (note: any) => {
|
||||||
|
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
|
||||||
|
editedText && updateNoteContent(note.id, editedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddDirectory = async () => {
|
||||||
|
const path = await window.api.file.selectFolder()
|
||||||
|
console.log('[KnowledgeContent] Selected directory:', path)
|
||||||
|
path && addDirectory(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainContent>
|
||||||
|
{!base?.version && (
|
||||||
|
<Alert message={t('knowledge_base.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||||
|
)}
|
||||||
|
<FileSection>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Title level={5}>{t('files.title')}</Title>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddFile}>
|
||||||
|
{t('knowledge_base.add_file')}
|
||||||
|
</Button>
|
||||||
|
</TitleWrapper>
|
||||||
|
<Dragger
|
||||||
|
showUploadList={false}
|
||||||
|
customRequest={({ file }) => handleDrop([file as File])}
|
||||||
|
multiple={true}
|
||||||
|
accept={fileTypes.join(',')}
|
||||||
|
style={{ marginTop: 10, background: 'transparent' }}>
|
||||||
|
<p className="ant-upload-text">{t('knowledge_base.drag_file')}</p>
|
||||||
|
<p className="ant-upload-hint">
|
||||||
|
{t('knowledge_base.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })}
|
||||||
|
</p>
|
||||||
|
</Dragger>
|
||||||
|
</FileSection>
|
||||||
|
|
||||||
|
<FileListSection>
|
||||||
|
{fileItems.map((item) => {
|
||||||
|
const file = item.content as FileType
|
||||||
|
return (
|
||||||
|
<ItemCard key={item.id}>
|
||||||
|
<ItemContent>
|
||||||
|
<ItemInfo>
|
||||||
|
<FileIcon />
|
||||||
|
<ClickableSpan onClick={() => window.api.file.openPath(file.path)}>{file.origin_name}</ClickableSpan>
|
||||||
|
</ItemInfo>
|
||||||
|
<FlexAlignCenter>
|
||||||
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
||||||
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
|
</FlexAlignCenter>
|
||||||
|
</ItemContent>
|
||||||
|
</ItemCard>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</FileListSection>
|
||||||
|
|
||||||
|
<ContentSection>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Title level={5}>{t('knowledge_base.directories')}</Title>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddDirectory}>
|
||||||
|
{t('knowledge_base.add_directory')}
|
||||||
|
</Button>
|
||||||
|
</TitleWrapper>
|
||||||
|
<FlexColumn>
|
||||||
|
{directoryItems.map((item) => (
|
||||||
|
<ItemCard key={item.id}>
|
||||||
|
<ItemContent>
|
||||||
|
<ItemInfo>
|
||||||
|
<FolderOutlined />
|
||||||
|
<ClickableSpan onClick={() => window.api.file.openPath(item.content as string)}>
|
||||||
|
{item.content as string}
|
||||||
|
</ClickableSpan>
|
||||||
|
</ItemInfo>
|
||||||
|
<FlexAlignCenter>
|
||||||
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
||||||
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
|
</FlexAlignCenter>
|
||||||
|
</ItemContent>
|
||||||
|
</ItemCard>
|
||||||
|
))}
|
||||||
|
</FlexColumn>
|
||||||
|
</ContentSection>
|
||||||
|
|
||||||
|
<ContentSection>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Title level={5}>{t('knowledge_base.urls')}</Title>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddUrl}>
|
||||||
|
{t('knowledge_base.add_url')}
|
||||||
|
</Button>
|
||||||
|
</TitleWrapper>
|
||||||
|
<FlexColumn>
|
||||||
|
{urlItems.map((item) => (
|
||||||
|
<ItemCard key={item.id}>
|
||||||
|
<ItemContent>
|
||||||
|
<ItemInfo>
|
||||||
|
<LinkOutlined />
|
||||||
|
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||||
|
{item.content as string}
|
||||||
|
</a>
|
||||||
|
</ItemInfo>
|
||||||
|
<FlexAlignCenter>
|
||||||
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
||||||
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
|
</FlexAlignCenter>
|
||||||
|
</ItemContent>
|
||||||
|
</ItemCard>
|
||||||
|
))}
|
||||||
|
</FlexColumn>
|
||||||
|
</ContentSection>
|
||||||
|
|
||||||
|
<ContentSection>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Title level={5}>{t('knowledge_base.sitemaps')}</Title>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddSitemap}>
|
||||||
|
{t('knowledge_base.add_sitemap')}
|
||||||
|
</Button>
|
||||||
|
</TitleWrapper>
|
||||||
|
<FlexColumn>
|
||||||
|
{sitemapItems.map((item) => (
|
||||||
|
<ItemCard key={item.id}>
|
||||||
|
<ItemContent>
|
||||||
|
<ItemInfo>
|
||||||
|
<GlobalOutlined />
|
||||||
|
<a href={item.content as string} target="_blank" rel="noopener noreferrer">
|
||||||
|
{item.content as string}
|
||||||
|
</a>
|
||||||
|
</ItemInfo>
|
||||||
|
<FlexAlignCenter>
|
||||||
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
||||||
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
|
</FlexAlignCenter>
|
||||||
|
</ItemContent>
|
||||||
|
</ItemCard>
|
||||||
|
))}
|
||||||
|
</FlexColumn>
|
||||||
|
</ContentSection>
|
||||||
|
|
||||||
|
<ContentSection>
|
||||||
|
<TitleWrapper>
|
||||||
|
<Title level={5}>{t('knowledge_base.notes')}</Title>
|
||||||
|
<Button icon={<PlusOutlined />} onClick={handleAddNote}>
|
||||||
|
{t('knowledge_base.add_note')}
|
||||||
|
</Button>
|
||||||
|
</TitleWrapper>
|
||||||
|
<FlexColumn>
|
||||||
|
{noteItems.map((note) => (
|
||||||
|
<ItemCard key={note.id}>
|
||||||
|
<ItemContent>
|
||||||
|
<ItemInfo onClick={() => handleEditNote(note)} style={{ cursor: 'pointer' }}>
|
||||||
|
<span>{(note.content as string).slice(0, 50)}...</span>
|
||||||
|
</ItemInfo>
|
||||||
|
<FlexAlignCenter>
|
||||||
|
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
||||||
|
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} />
|
||||||
|
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
||||||
|
</FlexAlignCenter>
|
||||||
|
</ItemContent>
|
||||||
|
</ItemCard>
|
||||||
|
))}
|
||||||
|
</FlexColumn>
|
||||||
|
</ContentSection>
|
||||||
|
|
||||||
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
|
|
||||||
|
<ModelInfo>
|
||||||
|
<label htmlFor="model-info">{t('knowledge_base.model_info')}</label>
|
||||||
|
<Tag color="blue">{base.model.name}</Tag>
|
||||||
|
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
|
||||||
|
<Tag color="purple">{base.model.provider}</Tag>
|
||||||
|
</ModelInfo>
|
||||||
|
|
||||||
|
<IndexSection>
|
||||||
|
<Button type="primary" onClick={() => KnowledgeSearchPopup.show({ base })} icon={<SearchOutlined />}>
|
||||||
|
{t('knowledge_base.search')}
|
||||||
|
</Button>
|
||||||
|
</IndexSection>
|
||||||
|
|
||||||
|
<BottomSpacer />
|
||||||
|
</MainContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MainContent = styled(Scrollbar)`
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContentSection = styled.div`
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.ant-input-textarea {
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitleWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
padding: 5px 20px;
|
||||||
|
min-height: 45px;
|
||||||
|
border-radius: 6px;
|
||||||
|
.ant-typography {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileListSection = styled.div`
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ItemCard = styled(Card)`
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ItemContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ItemInfo = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const IndexSection = styled.div`
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ModelInfo = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
label {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default KnowledgeContent
|
||||||
228
src/renderer/src/pages/knowledge/KnowledgePage.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { DeleteOutlined, EditOutlined, FileTextOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
|
import DragableList from '@renderer/components/DragableList'
|
||||||
|
import ListItem from '@renderer/components/ListItem'
|
||||||
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||||
|
import { KnowledgeBase } from '@renderer/types'
|
||||||
|
import { Dropdown, Empty, MenuProps } from 'antd'
|
||||||
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import AddKnowledgePopup from './components/AddKnowledgePopup'
|
||||||
|
import KnowledgeContent from './KnowledgeContent'
|
||||||
|
|
||||||
|
const KnowledgePage: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { bases, renameKnowledgeBase, deleteKnowledgeBase, updateKnowledgeBases } = useKnowledgeBases()
|
||||||
|
const [selectedBase, setSelectedBase] = useState<KnowledgeBase>()
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const prevLength = useRef(0)
|
||||||
|
|
||||||
|
const handleAddKnowledge = async () => {
|
||||||
|
await AddKnowledgePopup.show({ title: t('knowledge_base.add.title') })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bases.length > 0) {
|
||||||
|
if (!selectedBase) {
|
||||||
|
return setSelectedBase(bases[0])
|
||||||
|
}
|
||||||
|
if (selectedBase && !bases.find((base) => base.id === selectedBase.id)) {
|
||||||
|
return setSelectedBase(bases[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [bases, selectedBase])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentLength = bases.length
|
||||||
|
if (currentLength > 0 && currentLength > prevLength.current) {
|
||||||
|
setSelectedBase(bases[currentLength - 1])
|
||||||
|
}
|
||||||
|
prevLength.current = currentLength
|
||||||
|
}, [bases])
|
||||||
|
|
||||||
|
const getMenuItems = useCallback(
|
||||||
|
(base: KnowledgeBase) => {
|
||||||
|
const menus: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
label: t('knowledge_base.rename'),
|
||||||
|
key: 'rename',
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
async onClick() {
|
||||||
|
const name = await PromptPopup.show({
|
||||||
|
title: t('knowledge_base.rename'),
|
||||||
|
message: '',
|
||||||
|
defaultValue: base.name || ''
|
||||||
|
})
|
||||||
|
if (name && base.name !== name) {
|
||||||
|
renameKnowledgeBase(base.id, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
label: t('common.delete'),
|
||||||
|
danger: true,
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
window.modal.confirm({
|
||||||
|
title: t('knowledge_base.delete_confirm'),
|
||||||
|
centered: true,
|
||||||
|
onOk: () => {
|
||||||
|
deleteKnowledgeBase(base.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return menus
|
||||||
|
},
|
||||||
|
[deleteKnowledgeBase, renameKnowledgeBase, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Navbar>
|
||||||
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge_base.title')}</NavbarCenter>
|
||||||
|
</Navbar>
|
||||||
|
<ContentContainer id="content-container">
|
||||||
|
<SideNav>
|
||||||
|
<ScrollContainer>
|
||||||
|
<DragableList
|
||||||
|
list={bases}
|
||||||
|
onUpdate={updateKnowledgeBases}
|
||||||
|
style={{ marginBottom: 0, paddingBottom: isDragging ? 50 : 0 }}
|
||||||
|
onDragStart={() => setIsDragging(true)}
|
||||||
|
onDragEnd={() => setIsDragging(false)}>
|
||||||
|
{(base: KnowledgeBase) => (
|
||||||
|
<Dropdown menu={{ items: getMenuItems(base) }} trigger={['contextMenu']} key={base.id}>
|
||||||
|
<div>
|
||||||
|
<ListItem
|
||||||
|
active={selectedBase?.id === base.id}
|
||||||
|
icon={<FileTextOutlined />}
|
||||||
|
title={base.name}
|
||||||
|
onClick={() => setSelectedBase(base)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</DragableList>
|
||||||
|
{!isDragging && (
|
||||||
|
<AddKnowledgeItem onClick={handleAddKnowledge}>
|
||||||
|
<AddKnowledgeName>
|
||||||
|
<PlusOutlined style={{ color: 'var(--color-text-2)', marginRight: 4 }} />
|
||||||
|
{t('button.add')}
|
||||||
|
</AddKnowledgeName>
|
||||||
|
</AddKnowledgeItem>
|
||||||
|
)}
|
||||||
|
<div style={{ minHeight: '10px' }}></div>
|
||||||
|
</ScrollContainer>
|
||||||
|
</SideNav>
|
||||||
|
{bases.length === 0 ? (
|
||||||
|
<MainContent>
|
||||||
|
<Empty description={t('knowledge_base.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
</MainContent>
|
||||||
|
) : selectedBase ? (
|
||||||
|
<KnowledgeContent selectedBase={selectedBase} />
|
||||||
|
) : null}
|
||||||
|
</ContentContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
`
|
||||||
|
|
||||||
|
const ContentContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
min-height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MainContent = styled(Scrollbar)`
|
||||||
|
padding: 15px 20px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SideNav = styled.div`
|
||||||
|
width: var(--assistants-width);
|
||||||
|
border-right: 0.5px solid var(--color-border);
|
||||||
|
padding: 12px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ant-menu {
|
||||||
|
border-inline-end: none !important;
|
||||||
|
background: transparent;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
margin: 4px 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-menu-item-selected {
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ScrollContainer = styled(Scrollbar)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AddKnowledgeItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 7px 12px;
|
||||||
|
position: relative;
|
||||||
|
font-family: Ubuntu;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 0.5px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AddKnowledgeName = styled.div`
|
||||||
|
color: var(--color-text);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default KnowledgePage
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { TopView } from '@renderer/components/TopView'
|
||||||
|
import { isEmbeddingModel } from '@renderer/config/models'
|
||||||
|
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||||
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
|
import AiProvider from '@renderer/providers/AiProvider'
|
||||||
|
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||||
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
|
import { Model } from '@renderer/types'
|
||||||
|
import { Form, Input, Modal, Select } from 'antd'
|
||||||
|
import { find, sortBy } from 'lodash'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface ShowParams {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
name: string
|
||||||
|
model: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends ShowParams {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [form] = Form.useForm<FormData>()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { providers } = useProviders()
|
||||||
|
const { addKnowledgeBase } = useKnowledgeBases()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const allModels = providers
|
||||||
|
.map((p) => p.models)
|
||||||
|
.flat()
|
||||||
|
.filter((model) => isEmbeddingModel(model))
|
||||||
|
|
||||||
|
const selectOptions = providers
|
||||||
|
.filter((p) => p.models.length > 0)
|
||||||
|
.map((p) => ({
|
||||||
|
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||||
|
title: p.name,
|
||||||
|
options: sortBy(p.models, 'name')
|
||||||
|
.filter((model) => isEmbeddingModel(model))
|
||||||
|
.map((m) => ({
|
||||||
|
label: m.name,
|
||||||
|
value: getModelUniqId(m)
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
.filter((group) => group.options.length > 0)
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields()
|
||||||
|
const selectedModel = find(allModels, JSON.parse(values.model)) as Model
|
||||||
|
|
||||||
|
if (selectedModel) {
|
||||||
|
setLoading(true)
|
||||||
|
const provider = providers.find((p) => p.id === selectedModel.provider)
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiProvider = new AiProvider(provider)
|
||||||
|
let dimensions = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
dimensions = await aiProvider.getEmbeddingDimensions(selectedModel)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting embedding dimensions:', error)
|
||||||
|
window.message.error(t('message.error.get_embedding_dimensions'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBase = {
|
||||||
|
id: nanoid(),
|
||||||
|
name: values.name,
|
||||||
|
model: selectedModel,
|
||||||
|
dimensions,
|
||||||
|
items: [],
|
||||||
|
created_at: Date.now(),
|
||||||
|
updated_at: Date.now(),
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.api.knowledgeBase.create(getKnowledgeBaseParams(newBase))
|
||||||
|
|
||||||
|
addKnowledgeBase(newBase as any)
|
||||||
|
setOpen(false)
|
||||||
|
resolve(newBase)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Validation failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={title}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
destroyOnClose
|
||||||
|
centered
|
||||||
|
okButtonProps={{ loading }}>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label={t('common.name')}
|
||||||
|
rules={[{ required: true, message: t('message.error.enter.name') }]}>
|
||||||
|
<Input placeholder={t('common.name')} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="model"
|
||||||
|
label={t('models.embedding_model')}
|
||||||
|
tooltip={{ title: t('models.embedding_model_tooltip'), placement: 'right' }}
|
||||||
|
rules={[{ required: true, message: t('message.error.enter.model') }]}>
|
||||||
|
<Select style={{ width: '100%' }} options={selectOptions} placeholder={t('settings.models.empty')} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AddKnowledgePopup {
|
||||||
|
static hide() {
|
||||||
|
TopView.hide('AddKnowledgePopup')
|
||||||
|
}
|
||||||
|
|
||||||
|
static show(props: ShowParams) {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
{...props}
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
this.hide()
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
'AddKnowledgePopup'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||||
|
import { TopView } from '@renderer/components/TopView'
|
||||||
|
import { getFileFromUrl, getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||||
|
import { FileType, KnowledgeBase } from '@renderer/types'
|
||||||
|
import { Input, List, Modal, Spin, Typography } from 'antd'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const { Search } = Input
|
||||||
|
const { Text, Paragraph } = Typography
|
||||||
|
|
||||||
|
interface ShowParams {
|
||||||
|
base: KnowledgeBase
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends ShowParams {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [results, setResults] = useState<Array<ExtractChunkData & { file: FileType | null }>>([])
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState('')
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleSearch = async (value: string) => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
setResults([])
|
||||||
|
setSearchKeyword('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchKeyword(value.trim())
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const searchResults = await window.api.knowledgeBase.search({
|
||||||
|
search: value,
|
||||||
|
base: getKnowledgeBaseParams(base)
|
||||||
|
})
|
||||||
|
const results = await Promise.all(
|
||||||
|
searchResults.map(async (item) => {
|
||||||
|
const file = await getFileFromUrl(item.metadata.source)
|
||||||
|
return { ...item, file }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
setResults(results)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search failed:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOk = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
|
KnowledgeSearchPopup.hide = onCancel
|
||||||
|
|
||||||
|
const highlightText = (text: string) => {
|
||||||
|
if (!searchKeyword) return text
|
||||||
|
const parts = text.split(new RegExp(`(${searchKeyword})`, 'gi'))
|
||||||
|
return parts.map((part, i) =>
|
||||||
|
part.toLowerCase() === searchKeyword.toLowerCase() ? <mark key={i}>{part}</mark> : part
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('knowledge_base.search')}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
transitionName="ant-move-down">
|
||||||
|
<SearchContainer>
|
||||||
|
<Search
|
||||||
|
placeholder={t('knowledge_base.search_placeholder')}
|
||||||
|
allowClear
|
||||||
|
enterButton
|
||||||
|
size="large"
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>
|
||||||
|
<ResultsContainer>
|
||||||
|
{loading ? (
|
||||||
|
<LoadingContainer>
|
||||||
|
<Spin size="large" />
|
||||||
|
</LoadingContainer>
|
||||||
|
) : (
|
||||||
|
<List
|
||||||
|
dataSource={results}
|
||||||
|
renderItem={(item) => (
|
||||||
|
<List.Item>
|
||||||
|
<ResultItem>
|
||||||
|
<ScoreTag>Score: {(item.score * 100).toFixed(1)}%</ScoreTag>
|
||||||
|
<Paragraph>{highlightText(item.pageContent)}</Paragraph>
|
||||||
|
<MetadataContainer>
|
||||||
|
<Text type="secondary">
|
||||||
|
{t('knowledge_base.source')}:{' '}
|
||||||
|
{item.file ? (
|
||||||
|
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
|
||||||
|
{item.file.origin_name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
item.metadata.source
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</MetadataContainer>
|
||||||
|
</ResultItem>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ResultsContainer>
|
||||||
|
</SearchContainer>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ResultsContainer = styled.div`
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ResultItem = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ScoreTag = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MetadataContainer = styled.div`
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
`
|
||||||
|
|
||||||
|
const TopViewKey = 'KnowledgeSearchPopup'
|
||||||
|
|
||||||
|
export default class KnowledgeSearchPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
static show(props: ShowParams) {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
{...props}
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
TopViewKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/renderer/src/pages/knowledge/components/StatusIcon.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
||||||
|
import { Center } from '@renderer/components/Layout'
|
||||||
|
import { KnowledgeBase, ProcessingStatus } from '@renderer/types'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface StatusIconProps {
|
||||||
|
sourceId: string
|
||||||
|
base: KnowledgeBase
|
||||||
|
getProcessingStatus: (sourceId: string) => ProcessingStatus | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const status = getProcessingStatus(sourceId)
|
||||||
|
const item = base.items.find((item) => item.id === sourceId)
|
||||||
|
const errorText = item?.processingError
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
if (item?.uniqueId) {
|
||||||
|
return (
|
||||||
|
<Tooltip title={t('knowledge_base.status_completed')} placement="left">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip title={t('knowledge_base.status_new')} placement="left">
|
||||||
|
<Center style={{ width: '16px', height: '16px' }}>
|
||||||
|
<StatusDot $status="new" />
|
||||||
|
</Center>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return (
|
||||||
|
<Tooltip title={t('knowledge_base.status_pending')} placement="left">
|
||||||
|
<StatusDot $status="pending" />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
case 'processing':
|
||||||
|
return (
|
||||||
|
<Tooltip title={t('knowledge_base.status_processing')} placement="left">
|
||||||
|
<StatusDot $status="processing" />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
case 'completed':
|
||||||
|
return (
|
||||||
|
<Tooltip title={t('knowledge_base.status_completed')} placement="left">
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
case 'failed':
|
||||||
|
return (
|
||||||
|
<Tooltip title={errorText || t('knowledge_base.status_failed')} placement="left">
|
||||||
|
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusDot = styled.div<{ $status: 'pending' | 'processing' | 'new' }>`
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.$status === 'pending' ? '#faad14' : props.$status === 'new' ? '#918999' : '#1890ff'};
|
||||||
|
animation: ${(props) => (props.$status === 'processing' ? 'pulse 2s infinite' : 'none')};
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default StatusIcon
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GithubOutlined, TwitterOutlined } from '@ant-design/icons'
|
import { GithubOutlined, XOutlined } from '@ant-design/icons'
|
||||||
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SendOutlined, SoundOutlined } from '@ant-design/icons'
|
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SendOutlined, SoundOutlined } from '@ant-design/icons'
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
@@ -208,7 +208,7 @@ const AboutSettings: FC = () => {
|
|||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>
|
<SettingRowTitle>
|
||||||
<TwitterOutlined />X
|
<XOutlined />X
|
||||||
</SettingRowTitle>
|
</SettingRowTitle>
|
||||||
<Button onClick={() => onOpenWebsite('https://x.com/kangfenmao')}>
|
<Button onClick={() => onOpenWebsite('https://x.com/kangfenmao')}>
|
||||||
{t('settings.about.website.button')}
|
{t('settings.about.website.button')}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||||
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||||
import { SettingRow } from '@renderer/pages/settings'
|
import { SettingRow } from '@renderer/pages/settings'
|
||||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||||
@@ -8,9 +10,6 @@ import { FC, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import ModelAvatar from '../Avatar/ModelAvatar'
|
|
||||||
import SelectModelPopup from '../Popups/SelectModelPopup'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
updateAssistant: (assistant: Assistant) => void
|
updateAssistant: (assistant: Assistant) => void
|
||||||
@@ -126,14 +125,14 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
onChangeComplete={onTemperatureChange}
|
onChangeComplete={onTemperatureChange}
|
||||||
value={typeof temperature === 'number' ? temperature : 0}
|
value={typeof temperature === 'number' ? temperature : 0}
|
||||||
marks={{ 0: '0', 0.7: '0.7', 2: '2' }}
|
marks={{ 0: '0', 0.7: '0.7', 2: '2' }}
|
||||||
step={0.1}
|
step={0.01}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={0}
|
min={0}
|
||||||
max={2}
|
max={2}
|
||||||
step={0.1}
|
step={0.01}
|
||||||
value={temperature}
|
value={temperature}
|
||||||
onChange={onTemperatureChange}
|
onChange={onTemperatureChange}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
@@ -155,7 +154,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
onChangeComplete={onTopPChange}
|
onChangeComplete={onTopPChange}
|
||||||
value={typeof topP === 'number' ? topP : 1}
|
value={typeof topP === 'number' ? topP : 1}
|
||||||
marks={{ 0: '0', 1: '1' }}
|
marks={{ 0: '0', 1: '1' }}
|
||||||
step={0.1}
|
step={0.01}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
@@ -241,7 +240,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
)}
|
)}
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<SettingRow style={{ minHeight: 30 }}>
|
<SettingRow style={{ minHeight: 30 }}>
|
||||||
<Label>{t('model.stream_output')}</Label>
|
<Label>{t('models.stream_output')}</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={streamOutput}
|
checked={streamOutput}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Box, HStack } from '@renderer/components/Layout'
|
||||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||||
import { Button, Input } from 'antd'
|
import { Button, Input } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
@@ -5,8 +6,6 @@ import { useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { Box, HStack } from '../Layout'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
updateAssistant: (assistant: Assistant) => void
|
updateAssistant: (assistant: Assistant) => void
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAgent } from '@renderer/hooks/useAgents'
|
import { useAgent } from '@renderer/hooks/useAgents'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
@@ -6,8 +8,6 @@ import { useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { HStack } from '../Layout'
|
|
||||||
import { TopView } from '../TopView'
|
|
||||||
import AssistantMessagesSettings from './AssistantMessagesSettings'
|
import AssistantMessagesSettings from './AssistantMessagesSettings'
|
||||||
import AssistantModelSettings from './AssistantModelSettings'
|
import AssistantModelSettings from './AssistantModelSettings'
|
||||||
import AssistantPromptSettings from './AssistantPromptSettings'
|
import AssistantPromptSettings from './AssistantPromptSettings'
|
||||||