Compare commits
119 Commits
v1.4.0
...
feat/sideb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e5cc9be9 | ||
|
|
39f74cab3c | ||
|
|
4a0924ce15 | ||
|
|
b7eef3b753 | ||
|
|
d3f5887980 | ||
|
|
8db2059605 | ||
|
|
d11b98dfbb | ||
|
|
38330c4c81 | ||
|
|
b762cfd60b | ||
|
|
278397f7c8 | ||
|
|
c6d5faff73 | ||
|
|
9cac8fba56 | ||
|
|
b7d9949832 | ||
|
|
b4665509ab | ||
|
|
21e88b02ea | ||
|
|
10caef2c4c | ||
|
|
6ea1bcc7d1 | ||
|
|
06a60c4871 | ||
|
|
684367bf7c | ||
|
|
75b9e2f408 | ||
|
|
e13b136484 | ||
|
|
9c5fa57936 | ||
|
|
7e201522d0 | ||
|
|
df35f25502 | ||
|
|
f9e557763e | ||
|
|
eafd814caf | ||
|
|
b84f7bf596 | ||
|
|
c1d753b7fe | ||
|
|
3350f58422 | ||
|
|
8c617872e0 | ||
|
|
a333c635cb | ||
|
|
a244057b3a | ||
|
|
79d7ffcbad | ||
|
|
2d985c1f91 | ||
|
|
5879ccbeb2 | ||
|
|
7887f4867d | ||
|
|
c38a6cdfbf | ||
|
|
ea7766db44 | ||
|
|
a5012ce49e | ||
|
|
d3da4f4623 | ||
|
|
7f12c2f8b8 | ||
|
|
9ba2dea148 | ||
|
|
653bfa1f17 | ||
|
|
fa00b5b173 | ||
|
|
70fb6393b6 | ||
|
|
5b379666f4 | ||
|
|
3cb34d30a9 | ||
|
|
d47c93b4d8 | ||
|
|
bc5cc4bf02 | ||
|
|
8efa7d25f8 | ||
|
|
59195fec1a | ||
|
|
14e6a80049 | ||
|
|
67ab36e0ea | ||
|
|
dfc32967ed | ||
|
|
aa3c376def | ||
|
|
61c58caf78 | ||
|
|
b402cdf7ff | ||
|
|
d80513d011 | ||
|
|
4bcfbf785f | ||
|
|
b722dab56b | ||
|
|
6165e4a47f | ||
|
|
b829abed2d | ||
|
|
36f56ba9aa | ||
|
|
022b11cf6c | ||
|
|
8d6662cb48 | ||
|
|
a59a45f109 | ||
|
|
6337561f65 | ||
|
|
fbbc94028d | ||
|
|
93d955c4b9 | ||
|
|
1c71e6d474 | ||
|
|
b2d10b7a6b | ||
|
|
1215bcb046 | ||
|
|
9195a0324e | ||
|
|
acbec213e8 | ||
|
|
e2a08e31e8 | ||
|
|
e479ee3dbc | ||
|
|
f6462ef998 | ||
|
|
dcdf49a5ce | ||
|
|
74f72fa5b6 | ||
|
|
36f33fed75 | ||
|
|
eb7c05fd4c | ||
|
|
cb746fd722 | ||
|
|
0449bc359a | ||
|
|
d3e51ffb1c | ||
|
|
77eb70626c | ||
|
|
345c4f096e | ||
|
|
a4aab3fd4e | ||
|
|
ecf770e183 | ||
|
|
d58911ac60 | ||
|
|
bb0a35b920 | ||
|
|
403649f2ea | ||
|
|
958f8387d0 | ||
|
|
9c89676030 | ||
|
|
34ec018840 | ||
|
|
1be103a249 | ||
|
|
f83f8bb789 | ||
|
|
cc2810b117 | ||
|
|
be1dae7ef0 | ||
|
|
446d26d8dc | ||
|
|
7724b49ec4 | ||
|
|
ecbd283779 | ||
|
|
389f750d7b | ||
|
|
23eaae80c8 | ||
|
|
8f8c2f852e | ||
|
|
13f7269e36 | ||
|
|
0cd62a07fb | ||
|
|
20b55693cb | ||
|
|
74cccf2c09 | ||
|
|
54d20aa99b | ||
|
|
2c8086f078 | ||
|
|
475c1e38df | ||
|
|
80289f1dc3 | ||
|
|
ef16558947 | ||
|
|
c799f15fcc | ||
|
|
802402e922 | ||
|
|
37482bca7b | ||
|
|
184713dba8 | ||
|
|
0a0956cfc4 | ||
|
|
0a0bbad77f |
2
.github/workflows/nightly-build.yml
vendored
2
.github/workflows/nightly-build.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
ref: main
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
35
.github/workflows/release.yml
vendored
35
.github/workflows/release.yml
vendored
@@ -115,3 +115,38 @@ jobs:
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/*.blockmap'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
dispatch-docs-update:
|
||||
needs: release
|
||||
if: success() && github.repository == 'CherryHQ/cherry-studio' # 确保所有构建成功且在主仓库中运行
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get release tag
|
||||
id: get-tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if tag is pre-release
|
||||
id: check-tag
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${{ steps.get-tag.outputs.tag }}"
|
||||
if [[ "$TAG" == *"rc"* || "$TAG" == *"pre-release"* ]]; then
|
||||
echo "is_pre_release=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_pre_release=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Dispatch update-download-version workflow to cherry-studio-docs
|
||||
if: steps.check-tag.outputs.is_pre_release == 'false'
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: CherryHQ/cherry-studio-docs
|
||||
event-type: update-download-version
|
||||
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -45,7 +45,7 @@ stats.html
|
||||
local
|
||||
.aider*
|
||||
.cursorrules
|
||||
.cursor/rules
|
||||
.cursor/*
|
||||
|
||||
# vitest
|
||||
coverage
|
||||
|
||||
@@ -4,5 +4,8 @@
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none",
|
||||
"endOfLine": "lf",
|
||||
"bracketSameLine": true
|
||||
"bracketSameLine": true,
|
||||
"tailwindStylesheet": "./src/renderer/src/assets/styles/tailwind.css",
|
||||
"tailwindFunctions": ["clsx"],
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
}
|
||||
|
||||
85
.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch
vendored
85
.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch
vendored
@@ -1,85 +0,0 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644
|
||||
--- a/core.js
|
||||
+++ b/core.js
|
||||
@@ -159,7 +159,7 @@ class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/core.mjs b/core.mjs
|
||||
index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644
|
||||
--- a/core.mjs
|
||||
+++ b/core.mjs
|
||||
@@ -152,7 +152,7 @@ export class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
...this.authHeaders(opts),
|
||||
};
|
||||
}
|
||||
diff --git a/error.mjs b/error.mjs
|
||||
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
|
||||
--- a/error.mjs
|
||||
+++ b/error.mjs
|
||||
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
|
||||
if (!status || !headers) {
|
||||
return new APIConnectionError({ message, cause: castToError(errorResponse) });
|
||||
}
|
||||
- const error = errorResponse?.['error'];
|
||||
+ const error = errorResponse?.['error'] || errorResponse;
|
||||
if (status === 400) {
|
||||
return new BadRequestError(status, error, message, headers);
|
||||
}
|
||||
diff --git a/resources/embeddings.js b/resources/embeddings.js
|
||||
index aae578404cb2d09a39ac33fc416f1c215c45eecd..25c54b05bdae64d5c3b36fbb30dc7c8221b14034 100644
|
||||
--- a/resources/embeddings.js
|
||||
+++ b/resources/embeddings.js
|
||||
@@ -36,6 +36,9 @@ class Embeddings extends resource_1.APIResource {
|
||||
// No encoding_format specified, defaulting to base64 for performance reasons
|
||||
// See https://github.com/openai/openai-node/pull/1312
|
||||
let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
|
||||
+ if (body.model.includes('jina')) {
|
||||
+ encoding_format = undefined;
|
||||
+ }
|
||||
if (hasUserProvidedEncodingFormat) {
|
||||
Core.debug('Request', 'User defined encoding_format:', body.encoding_format);
|
||||
}
|
||||
@@ -47,7 +50,7 @@ class Embeddings extends resource_1.APIResource {
|
||||
...options,
|
||||
});
|
||||
// if the user specified an encoding_format, return the response as-is
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
+ if (hasUserProvidedEncodingFormat || body.model.includes('jina')) {
|
||||
return response;
|
||||
}
|
||||
// in this stage, we are sure the user did not specify an encoding_format
|
||||
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
|
||||
index 0df3c6cc79a520e54acb4c2b5f77c43b774035ff..aa488b8a11b2c413c0a663d9a6059d286d7b5faf 100644
|
||||
--- a/resources/embeddings.mjs
|
||||
+++ b/resources/embeddings.mjs
|
||||
@@ -10,6 +10,9 @@ export class Embeddings extends APIResource {
|
||||
// No encoding_format specified, defaulting to base64 for performance reasons
|
||||
// See https://github.com/openai/openai-node/pull/1312
|
||||
let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
|
||||
+ if (body.model.includes('jina')) {
|
||||
+ encoding_format = undefined;
|
||||
+ }
|
||||
if (hasUserProvidedEncodingFormat) {
|
||||
Core.debug('Request', 'User defined encoding_format:', body.encoding_format);
|
||||
}
|
||||
@@ -21,7 +24,7 @@ export class Embeddings extends APIResource {
|
||||
...options,
|
||||
});
|
||||
// if the user specified an encoding_format, return the response as-is
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
+ if (hasUserProvidedEncodingFormat || body.model.includes('jina')) {
|
||||
return response;
|
||||
}
|
||||
// in this stage, we are sure the user did not specify an encoding_format
|
||||
279
.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch
vendored
Normal file
279
.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch
vendored
Normal file
@@ -0,0 +1,279 @@
|
||||
diff --git a/client.js b/client.js
|
||||
index 33b4ff6309d5f29187dab4e285d07dac20340bab..8f568637ee9e4677585931fb0284c8165a933f69 100644
|
||||
--- a/client.js
|
||||
+++ b/client.js
|
||||
@@ -433,7 +433,7 @@ class OpenAI {
|
||||
'User-Agent': this.getUserAgent(),
|
||||
'X-Stainless-Retry-Count': String(retryCount),
|
||||
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
|
||||
- ...(0, detect_platform_1.getPlatformHeaders)(),
|
||||
+ // ...(0, detect_platform_1.getPlatformHeaders)(),
|
||||
'OpenAI-Organization': this.organization,
|
||||
'OpenAI-Project': this.project,
|
||||
},
|
||||
diff --git a/client.mjs b/client.mjs
|
||||
index c34c18213073540ebb296ea540b1d1ad39527906..1ce1a98256d7e90e26ca963582f235b23e996e73 100644
|
||||
--- a/client.mjs
|
||||
+++ b/client.mjs
|
||||
@@ -430,7 +430,7 @@ export class OpenAI {
|
||||
'User-Agent': this.getUserAgent(),
|
||||
'X-Stainless-Retry-Count': String(retryCount),
|
||||
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
|
||||
- ...getPlatformHeaders(),
|
||||
+ // ...getPlatformHeaders(),
|
||||
'OpenAI-Organization': this.organization,
|
||||
'OpenAI-Project': this.project,
|
||||
},
|
||||
diff --git a/core/error.js b/core/error.js
|
||||
index a12d9d9ccd242050161adeb0f82e1b98d9e78e20..fe3a5462480558bc426deea147f864f12b36f9bd 100644
|
||||
--- a/core/error.js
|
||||
+++ b/core/error.js
|
||||
@@ -40,7 +40,7 @@ class APIError extends OpenAIError {
|
||||
if (!status || !headers) {
|
||||
return new APIConnectionError({ message, cause: (0, errors_1.castToError)(errorResponse) });
|
||||
}
|
||||
- const error = errorResponse?.['error'];
|
||||
+ const error = errorResponse?.['error'] || errorResponse;
|
||||
if (status === 400) {
|
||||
return new BadRequestError(status, error, message, headers);
|
||||
}
|
||||
diff --git a/core/error.mjs b/core/error.mjs
|
||||
index 83cefbaffeb8c657536347322d8de9516af479a2..63334b7972ec04882aa4a0800c1ead5982345045 100644
|
||||
--- a/core/error.mjs
|
||||
+++ b/core/error.mjs
|
||||
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
|
||||
if (!status || !headers) {
|
||||
return new APIConnectionError({ message, cause: castToError(errorResponse) });
|
||||
}
|
||||
- const error = errorResponse?.['error'];
|
||||
+ const error = errorResponse?.['error'] || errorResponse;
|
||||
if (status === 400) {
|
||||
return new BadRequestError(status, error, message, headers);
|
||||
}
|
||||
diff --git a/resources/embeddings.js b/resources/embeddings.js
|
||||
index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b90ae6790 100644
|
||||
--- a/resources/embeddings.js
|
||||
+++ b/resources/embeddings.js
|
||||
@@ -5,52 +5,64 @@ exports.Embeddings = void 0;
|
||||
const resource_1 = require("../core/resource.js");
|
||||
const utils_1 = require("../internal/utils.js");
|
||||
class Embeddings extends resource_1.APIResource {
|
||||
- /**
|
||||
- * Creates an embedding vector representing the input text.
|
||||
- *
|
||||
- * @example
|
||||
- * ```ts
|
||||
- * const createEmbeddingResponse =
|
||||
- * await client.embeddings.create({
|
||||
- * input: 'The quick brown fox jumped over the lazy dog',
|
||||
- * model: 'text-embedding-3-small',
|
||||
- * });
|
||||
- * ```
|
||||
- */
|
||||
- create(body, options) {
|
||||
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
|
||||
- // No encoding_format specified, defaulting to base64 for performance reasons
|
||||
- // See https://github.com/openai/openai-node/pull/1312
|
||||
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
- (0, utils_1.loggerFor)(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
|
||||
- }
|
||||
- const response = this._client.post('/embeddings', {
|
||||
- body: {
|
||||
- ...body,
|
||||
- encoding_format: encoding_format,
|
||||
- },
|
||||
- ...options,
|
||||
- });
|
||||
- // if the user specified an encoding_format, return the response as-is
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
- return response;
|
||||
- }
|
||||
- // in this stage, we are sure the user did not specify an encoding_format
|
||||
- // and we defaulted to base64 for performance reasons
|
||||
- // we are sure then that the response is base64 encoded, let's decode it
|
||||
- // the returned result will be a float32 array since this is OpenAI API's default encoding
|
||||
- (0, utils_1.loggerFor)(this._client).debug('embeddings/decoding base64 embeddings from base64');
|
||||
- return response._thenUnwrap((response) => {
|
||||
- if (response && response.data) {
|
||||
- response.data.forEach((embeddingBase64Obj) => {
|
||||
- const embeddingBase64Str = embeddingBase64Obj.embedding;
|
||||
- embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(embeddingBase64Str);
|
||||
- });
|
||||
- }
|
||||
- return response;
|
||||
- });
|
||||
- }
|
||||
+ /**
|
||||
+ * Creates an embedding vector representing the input text.
|
||||
+ *
|
||||
+ * @example
|
||||
+ * ```ts
|
||||
+ * const createEmbeddingResponse =
|
||||
+ * await client.embeddings.create({
|
||||
+ * input: 'The quick brown fox jumped over the lazy dog',
|
||||
+ * model: 'text-embedding-3-small',
|
||||
+ * });
|
||||
+ * ```
|
||||
+ */
|
||||
+ create(body, options) {
|
||||
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
|
||||
+ // No encoding_format specified, defaulting to base64 for performance reasons
|
||||
+ // See https://github.com/openai/openai-node/pull/1312
|
||||
+ let encoding_format = hasUserProvidedEncodingFormat
|
||||
+ ? body.encoding_format
|
||||
+ : "base64";
|
||||
+ if (body.model.includes("jina")) {
|
||||
+ encoding_format = undefined;
|
||||
+ }
|
||||
+ if (hasUserProvidedEncodingFormat) {
|
||||
+ (0, utils_1.loggerFor)(this._client).debug(
|
||||
+ "embeddings/user defined encoding_format:",
|
||||
+ body.encoding_format
|
||||
+ );
|
||||
+ }
|
||||
+ const response = this._client.post("/embeddings", {
|
||||
+ body: {
|
||||
+ ...body,
|
||||
+ encoding_format: encoding_format,
|
||||
+ },
|
||||
+ ...options,
|
||||
+ });
|
||||
+ // if the user specified an encoding_format, return the response as-is
|
||||
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
|
||||
+ return response;
|
||||
+ }
|
||||
+ // in this stage, we are sure the user did not specify an encoding_format
|
||||
+ // and we defaulted to base64 for performance reasons
|
||||
+ // we are sure then that the response is base64 encoded, let's decode it
|
||||
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
|
||||
+ (0, utils_1.loggerFor)(this._client).debug(
|
||||
+ "embeddings/decoding base64 embeddings from base64"
|
||||
+ );
|
||||
+ return response._thenUnwrap((response) => {
|
||||
+ if (response && response.data) {
|
||||
+ response.data.forEach((embeddingBase64Obj) => {
|
||||
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
|
||||
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
|
||||
+ embeddingBase64Str
|
||||
+ );
|
||||
+ });
|
||||
+ }
|
||||
+ return response;
|
||||
+ });
|
||||
+ }
|
||||
}
|
||||
exports.Embeddings = Embeddings;
|
||||
//# sourceMappingURL=embeddings.js.map
|
||||
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
|
||||
index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a74bd9200 100644
|
||||
--- a/resources/embeddings.mjs
|
||||
+++ b/resources/embeddings.mjs
|
||||
@@ -2,51 +2,61 @@
|
||||
import { APIResource } from "../core/resource.mjs";
|
||||
import { loggerFor, toFloat32Array } from "../internal/utils.mjs";
|
||||
export class Embeddings extends APIResource {
|
||||
- /**
|
||||
- * Creates an embedding vector representing the input text.
|
||||
- *
|
||||
- * @example
|
||||
- * ```ts
|
||||
- * const createEmbeddingResponse =
|
||||
- * await client.embeddings.create({
|
||||
- * input: 'The quick brown fox jumped over the lazy dog',
|
||||
- * model: 'text-embedding-3-small',
|
||||
- * });
|
||||
- * ```
|
||||
- */
|
||||
- create(body, options) {
|
||||
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
|
||||
- // No encoding_format specified, defaulting to base64 for performance reasons
|
||||
- // See https://github.com/openai/openai-node/pull/1312
|
||||
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
- loggerFor(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
|
||||
- }
|
||||
- const response = this._client.post('/embeddings', {
|
||||
- body: {
|
||||
- ...body,
|
||||
- encoding_format: encoding_format,
|
||||
- },
|
||||
- ...options,
|
||||
- });
|
||||
- // if the user specified an encoding_format, return the response as-is
|
||||
- if (hasUserProvidedEncodingFormat) {
|
||||
- return response;
|
||||
- }
|
||||
- // in this stage, we are sure the user did not specify an encoding_format
|
||||
- // and we defaulted to base64 for performance reasons
|
||||
- // we are sure then that the response is base64 encoded, let's decode it
|
||||
- // the returned result will be a float32 array since this is OpenAI API's default encoding
|
||||
- loggerFor(this._client).debug('embeddings/decoding base64 embeddings from base64');
|
||||
- return response._thenUnwrap((response) => {
|
||||
- if (response && response.data) {
|
||||
- response.data.forEach((embeddingBase64Obj) => {
|
||||
- const embeddingBase64Str = embeddingBase64Obj.embedding;
|
||||
- embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
|
||||
- });
|
||||
- }
|
||||
- return response;
|
||||
- });
|
||||
- }
|
||||
+ /**
|
||||
+ * Creates an embedding vector representing the input text.
|
||||
+ *
|
||||
+ * @example
|
||||
+ * ```ts
|
||||
+ * const createEmbeddingResponse =
|
||||
+ * await client.embeddings.create({
|
||||
+ * input: 'The quick brown fox jumped over the lazy dog',
|
||||
+ * model: 'text-embedding-3-small',
|
||||
+ * });
|
||||
+ * ```
|
||||
+ */
|
||||
+ create(body, options) {
|
||||
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
|
||||
+ // No encoding_format specified, defaulting to base64 for performance reasons
|
||||
+ // See https://github.com/openai/openai-node/pull/1312
|
||||
+ let encoding_format = hasUserProvidedEncodingFormat
|
||||
+ ? body.encoding_format
|
||||
+ : "base64";
|
||||
+ if (body.model.includes("jina")) {
|
||||
+ encoding_format = undefined;
|
||||
+ }
|
||||
+ if (hasUserProvidedEncodingFormat) {
|
||||
+ loggerFor(this._client).debug(
|
||||
+ "embeddings/user defined encoding_format:",
|
||||
+ body.encoding_format
|
||||
+ );
|
||||
+ }
|
||||
+ const response = this._client.post("/embeddings", {
|
||||
+ body: {
|
||||
+ ...body,
|
||||
+ encoding_format: encoding_format,
|
||||
+ },
|
||||
+ ...options,
|
||||
+ });
|
||||
+ // if the user specified an encoding_format, return the response as-is
|
||||
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
|
||||
+ return response;
|
||||
+ }
|
||||
+ // in this stage, we are sure the user did not specify an encoding_format
|
||||
+ // and we defaulted to base64 for performance reasons
|
||||
+ // we are sure then that the response is base64 encoded, let's decode it
|
||||
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
|
||||
+ loggerFor(this._client).debug(
|
||||
+ "embeddings/decoding base64 embeddings from base64"
|
||||
+ );
|
||||
+ return response._thenUnwrap((response) => {
|
||||
+ if (response && response.data) {
|
||||
+ response.data.forEach((embeddingBase64Obj) => {
|
||||
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
|
||||
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
|
||||
+ });
|
||||
+ }
|
||||
+ return response;
|
||||
+ });
|
||||
+ }
|
||||
}
|
||||
//# sourceMappingURL=embeddings.mjs.map
|
||||
96
README.md
96
README.md
@@ -3,10 +3,42 @@
|
||||
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
|
||||
</a>
|
||||
</h1>
|
||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
|
||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
|
||||
|
||||
<!-- 题头徽章组合 -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][deepwiki-shield]][deepwiki-link]
|
||||
[![][twitter-shield]][twitter-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
[![][telegram-shield]][telegram-link]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 项目统计徽章 -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
[![][github-forks-shield]][github-forks-link]
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-contributors-shield]][github-contributors-link]
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][license-shield]][license-link]
|
||||
[![][commercial-shield]][commercial-link]
|
||||
[![][sponsor-shield]][sponsor-link]
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="Featured|HelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
@@ -17,10 +49,6 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
||||
|
||||
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
|
||||
|
||||
# 📖 Guide
|
||||
|
||||
<https://docs.cherry-ai.com>
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||

|
||||
@@ -114,14 +142,6 @@ Want to influence our roadmap? Join our [GitHub Discussions](https://github.com/
|
||||
|
||||
Welcome PR for more themes
|
||||
|
||||
# 🖥️ Develop
|
||||
|
||||
Refer to the [development documentation](docs/dev.md)
|
||||
|
||||
Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
|
||||
|
||||
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
|
||||
|
||||
# 🤝 Contributing
|
||||
|
||||
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
|
||||
@@ -134,6 +154,8 @@ We welcome contributions to Cherry Studio! Here are some ways you can contribute
|
||||
6. **Community Engagement**: Join discussions and help users.
|
||||
7. **Promote Usage**: Spread the word about Cherry Studio.
|
||||
|
||||
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Fork the Repository**: Fork and clone it to your local machine.
|
||||
@@ -158,22 +180,34 @@ Thank you for your support and contributions!
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
# 🌐 Community
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# ☕ Sponsor
|
||||
|
||||
[Buy Me a Coffee](docs/sponsor.md)
|
||||
|
||||
# 📃 License
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
|
||||
# ✉️ Contact
|
||||
|
||||
<yinsenho@cherry-ai.com>
|
||||
|
||||
# ⭐️ Star History
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
[](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
|
||||
|
||||
<!-- Links & Images -->
|
||||
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
|
||||
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
|
||||
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
|
||||
[twitter-link]: https://twitter.com/CherryStudioApp
|
||||
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
|
||||
[discord-link]: https://discord.gg/wez8HtpxqQ
|
||||
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
|
||||
[telegram-link]: https://t.me/CherryStudioAI
|
||||
|
||||
<!-- Links & Images -->
|
||||
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
|
||||
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
|
||||
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
|
||||
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
|
||||
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
|
||||
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
|
||||
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
|
||||
|
||||
<!-- Links & Images -->
|
||||
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
|
||||
[license-link]: https://www.gnu.org/licenses/agpl-3.0
|
||||
[commercial-shield]: https://img.shields.io/badge/License-Contact-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
|
||||
[commercial-link]: mailto:license@cherry-ai.com?subject=Commercial%20License%20Inquiry
|
||||
[sponsor-shield]: https://img.shields.io/badge/Sponsor-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
|
||||
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md
|
||||
|
||||
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/renderer/src/assets/styles/tailwind.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@renderer/ui/third-party",
|
||||
"utils": "@renderer/utils",
|
||||
"ui": "@renderer/ui",
|
||||
"lib": "@renderer/lib",
|
||||
"hooks": "@renderer/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,15 +1,46 @@
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/CherryHQ/cherry-studio/releases">
|
||||
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
|
||||
</a>
|
||||
</h1>
|
||||
<p align="center">
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 <br>
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 | <a href="https://cherry-ai.com">公式サイト</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/ja">ドキュメント</a> | <a href="./dev.md">開発</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">フィードバック</a><br>
|
||||
</p>
|
||||
|
||||
<!-- バッジコレクション -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][deepwiki-shield]][deepwiki-link]
|
||||
[![][twitter-shield]][twitter-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
[![][telegram-shield]][telegram-link]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- プロジェクト統計 -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
[![][github-forks-shield]][github-forks-link]
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-contributors-shield]][github-contributors-link]
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][license-shield]][license-link]
|
||||
[![][commercial-shield]][commercial-link]
|
||||
[![][sponsor-shield]][sponsor-link]
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="Featured|HelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
@@ -20,10 +51,6 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
|
||||
|
||||
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
|
||||
|
||||
# 📖 ガイド
|
||||
|
||||
https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||

|
||||
@@ -117,14 +144,6 @@ https://docs.cherry-ai.com
|
||||
|
||||
より多くのテーマの PR を歓迎します
|
||||
|
||||
# 🖥️ 開発
|
||||
|
||||
[開発ドキュメント](dev.md)を参照してください
|
||||
|
||||
[アーキテクチャ概要ドキュメント](https://deepwiki.com/CherryHQ/cherry-studio)を参照してください
|
||||
|
||||
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
|
||||
|
||||
# 🤝 貢献
|
||||
|
||||
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
|
||||
@@ -137,6 +156,8 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
|
||||
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
|
||||
7. **使用の促進**:Cherry Studio を広めます
|
||||
|
||||
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
|
||||
|
||||
## 始め方
|
||||
|
||||
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
|
||||
@@ -161,22 +182,34 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
# 🌐 コミュニティ
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# ☕ スポンサー
|
||||
|
||||
[開発者を支援する](sponsor.md)
|
||||
|
||||
# 📃 ライセンス
|
||||
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ✉️ お問い合わせ
|
||||
|
||||
yinsenho@cherry-ai.com
|
||||
|
||||
# ⭐️ スター履歴
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
[](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
|
||||
|
||||
<!-- リンクと画像 -->
|
||||
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
|
||||
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
|
||||
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
|
||||
[twitter-link]: https://twitter.com/CherryStudioApp
|
||||
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
|
||||
[discord-link]: https://discord.gg/wez8HtpxqQ
|
||||
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
|
||||
[telegram-link]: https://t.me/CherryStudioAI
|
||||
|
||||
<!-- プロジェクト統計 -->
|
||||
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
|
||||
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
|
||||
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
|
||||
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
|
||||
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
|
||||
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
|
||||
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
|
||||
|
||||
<!-- ライセンスとスポンサー -->
|
||||
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
|
||||
[license-link]: https://www.gnu.org/licenses/agpl-3.0
|
||||
[commercial-shield]: https://img.shields.io/badge/商用ライセンス-お問い合わせ-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
|
||||
[commercial-link]: mailto:license@cherry-ai.com?subject=商業ライセンスについて
|
||||
[sponsor-shield]: https://img.shields.io/badge/スポンサー-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
|
||||
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md
|
||||
|
||||
@@ -1,14 +1,46 @@
|
||||
<h1 align="center">
|
||||
<a href="https://github.com/CherryHQ/cherry-studio/releases">
|
||||
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
|
||||
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
|
||||
</a>
|
||||
</h1>
|
||||
<p align="center">
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br>
|
||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a> | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/zh-cn">文档</a> | <a href="./dev.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
|
||||
</p>
|
||||
|
||||
<!-- 题头徽章组合 -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][deepwiki-shield]][deepwiki-link]
|
||||
[![][twitter-shield]][twitter-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
[![][telegram-shield]][telegram-link]
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 项目统计徽章 -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
[![][github-forks-shield]][github-forks-link]
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-contributors-shield]][github-contributors-link]
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![][license-shield]][license-link]
|
||||
[![][commercial-shield]][commercial-link]
|
||||
[![][sponsor-shield]][sponsor-link]
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="Featured|HelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
|
||||
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry-studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry Studio - AI Chatbots, AI Desktop Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
|
||||
</div>
|
||||
|
||||
# 🍒 Cherry Studio
|
||||
@@ -124,14 +156,6 @@ https://docs.cherry-ai.com
|
||||
|
||||
欢迎 PR 更多主题
|
||||
|
||||
# 🖥️ 开发
|
||||
|
||||
参考[开发文档](dev.md)
|
||||
|
||||
参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
|
||||
|
||||
参考[分支策略](branching-strategy-zh.md)了解贡献指南
|
||||
|
||||
# 🤝 贡献
|
||||
|
||||
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
|
||||
@@ -144,6 +168,8 @@ https://docs.cherry-ai.com
|
||||
6. **社区参与**:加入讨论并帮助用户
|
||||
7. **推广使用**:宣传 Cherry Studio
|
||||
|
||||
参考[分支策略](branching-strategy-zh.md)了解贡献指南
|
||||
|
||||
## 入门
|
||||
|
||||
1. **Fork 仓库**:Fork 并克隆到您的本地机器
|
||||
@@ -168,22 +194,34 @@ https://docs.cherry-ai.com
|
||||
</a>
|
||||
<br /><br />
|
||||
|
||||
# 🌐 社区
|
||||
|
||||
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
|
||||
|
||||
# ☕ 赞助
|
||||
|
||||
[赞助开发者](sponsor.md)
|
||||
|
||||
# 📃 许可证
|
||||
|
||||
[LICENSE](../LICENSE)
|
||||
|
||||
# ✉️ 联系我们
|
||||
|
||||
yinsenho@cherry-ai.com
|
||||
|
||||
# ⭐️ Star 记录
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
[](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
|
||||
|
||||
<!-- Links & Images -->
|
||||
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
|
||||
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
|
||||
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
|
||||
[twitter-link]: https://twitter.com/CherryStudioApp
|
||||
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
|
||||
[discord-link]: https://discord.gg/wez8HtpxqQ
|
||||
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
|
||||
[telegram-link]: https://t.me/CherryStudioAI
|
||||
|
||||
<!-- 项目统计徽章 -->
|
||||
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
|
||||
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
|
||||
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
|
||||
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
|
||||
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
|
||||
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
|
||||
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
|
||||
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
|
||||
|
||||
<!-- 许可和赞助徽章 -->
|
||||
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
|
||||
[license-link]: https://www.gnu.org/licenses/agpl-3.0
|
||||
[commercial-shield]: https://img.shields.io/badge/商用授权-联系-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
|
||||
[commercial-link]: mailto:license@cherry-ai.com?subject=商业授权咨询
|
||||
[sponsor-shield]: https://img.shields.io/badge/赞助支持-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
|
||||
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md
|
||||
|
||||
@@ -40,6 +40,7 @@ export default defineConfig({
|
||||
},
|
||||
renderer: {
|
||||
plugins: [
|
||||
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||
react({
|
||||
plugins: [
|
||||
[
|
||||
|
||||
@@ -62,7 +62,8 @@ export default defineConfig([
|
||||
'.yarn/**',
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**'
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/renderer/src/ui/**'
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
35
package.json
35
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -47,6 +47,7 @@
|
||||
"test": "vitest run --silent",
|
||||
"test:main": "vitest run --project main",
|
||||
"test:renderer": "vitest run --project renderer",
|
||||
"test:update": "yarn test:renderer --update",
|
||||
"test:coverage": "vitest run --coverage --silent",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest",
|
||||
@@ -83,7 +84,7 @@
|
||||
"electron-window-state": "^5.0.3",
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
"franc": "^6.2.0",
|
||||
"franc-min": "^6.2.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
@@ -91,7 +92,7 @@
|
||||
"officeparser": "^4.1.1",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"selection-hook": "^0.9.19",
|
||||
"selection-hook": "^0.9.22",
|
||||
"tar": "^7.4.3",
|
||||
"turndown": "^7.2.0",
|
||||
"webdav": "^5.8.0",
|
||||
@@ -118,9 +119,18 @@
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.4.2",
|
||||
"@swc/plugin-styled-components": "^7.1.5",
|
||||
"@tailwindcss/vite": "^4.1.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -149,6 +159,8 @@
|
||||
"antd": "^5.22.5",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "^5.0.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
@@ -172,15 +184,17 @@
|
||||
"lint-staged": "^15.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-react": "^0.487.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"mermaid": "^11.6.0",
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.10.5",
|
||||
"motion": "^12.12.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"playwright": "^1.52.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"rc-virtual-list": "^3.18.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -204,11 +218,16 @@
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.88.0",
|
||||
"shiki": "^3.4.2",
|
||||
"sonner": "^2.0.3",
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
"tokenx": "^0.4.1",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.6.2",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "6.2.6",
|
||||
"vitest": "^3.1.4"
|
||||
@@ -218,10 +237,10 @@
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"openai@npm:^4.87.3": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
|
||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch"
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ export enum IpcChannel {
|
||||
App_SetTrayOnClose = 'app:set-tray-on-close',
|
||||
App_SetTheme = 'app:set-theme',
|
||||
App_SetAutoUpdate = 'app:set-auto-update',
|
||||
App_SetFeedUrl = 'app:set-feed-url',
|
||||
App_HandleZoomFactor = 'app:handle-zoom-factor',
|
||||
|
||||
App_IsBinaryExist = 'app:is-binary-exist',
|
||||
@@ -20,6 +21,8 @@ export enum IpcChannel {
|
||||
App_InstallUvBinary = 'app:install-uv-binary',
|
||||
App_InstallBunBinary = 'app:install-bun-binary',
|
||||
|
||||
App_QuoteToMain = 'app:quote-to-main',
|
||||
|
||||
Notification_Send = 'notification:send',
|
||||
Notification_OnClick = 'notification:on-click',
|
||||
|
||||
|
||||
@@ -4,135 +4,368 @@ export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||
export const bookExts = ['.epub']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
'.md', // Markdown 文件
|
||||
'.mdx', // Markdown 文件
|
||||
'.html', // HTML 文件
|
||||
'.htm', // HTML 文件的另一种扩展名
|
||||
'.xml', // XML 文件
|
||||
'.json', // JSON 文件
|
||||
'.yaml', // YAML 文件
|
||||
'.yml', // YAML 文件的另一种扩展名
|
||||
'.csv', // 逗号分隔值文件
|
||||
'.tsv', // 制表符分隔值文件
|
||||
'.ini', // 配置文件
|
||||
'.log', // 日志文件
|
||||
'.rtf', // 富文本格式文件
|
||||
'.org', // org-mode 文件
|
||||
'.wiki', // VimWiki 文件
|
||||
'.tex', // LaTeX 文件
|
||||
'.bib', // BibTeX 文件
|
||||
'.srt', // 字幕文件
|
||||
'.xhtml', // XHTML 文件
|
||||
'.nfo', // 信息文件(主要用于场景发布)
|
||||
'.conf', // 配置文件
|
||||
'.config', // 配置文件
|
||||
'.env', // 环境变量文件
|
||||
'.rst', // reStructuredText 文件
|
||||
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||
'.ts', // TypeScript 文件
|
||||
'.jsp', // JavaServer Pages 文件
|
||||
'.aspx', // ASP.NET 文件
|
||||
'.bat', // Windows 批处理文件
|
||||
'.sh', // Unix/Linux Shell 脚本文件
|
||||
'.py', // Python 脚本文件
|
||||
'.ipynb', // Jupyter 笔记本格式
|
||||
'.rb', // Ruby 脚本文件
|
||||
'.pl', // Perl 脚本文件
|
||||
'.sql', // SQL 脚本文件
|
||||
'.css', // Cascading Style Sheets 文件
|
||||
'.less', // Less CSS 预处理器文件
|
||||
'.scss', // Sass CSS 预处理器文件
|
||||
'.sass', // Sass 文件
|
||||
'.styl', // Stylus CSS 预处理器文件
|
||||
'.coffee', // CoffeeScript 文件
|
||||
'.ino', // Arduino 代码文件
|
||||
'.asm', // Assembly 语言文件
|
||||
'.go', // Go 语言文件
|
||||
'.scala', // Scala 语言文件
|
||||
'.swift', // Swift 语言文件
|
||||
'.kt', // Kotlin 语言文件
|
||||
'.rs', // Rust 语言文件
|
||||
'.lua', // Lua 语言文件
|
||||
'.groovy', // Groovy 语言文件
|
||||
'.dart', // Dart 语言文件
|
||||
'.hs', // Haskell 语言文件
|
||||
'.clj', // Clojure 语言文件
|
||||
'.cljs', // ClojureScript 语言文件
|
||||
'.elm', // Elm 语言文件
|
||||
'.erl', // Erlang 语言文件
|
||||
'.ex', // Elixir 语言文件
|
||||
'.exs', // Elixir 脚本文件
|
||||
'.pug', // Pug (formerly Jade) 模板文件
|
||||
'.haml', // Haml 模板文件
|
||||
'.slim', // Slim 模板文件
|
||||
'.tpl', // 模板文件(通用)
|
||||
'.ejs', // Embedded JavaScript 模板文件
|
||||
'.hbs', // Handlebars 模板文件
|
||||
'.mustache', // Mustache 模板文件
|
||||
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||
'.twig', // Twig 模板文件
|
||||
'.blade', // Blade 模板文件 (Laravel)
|
||||
'.vue', // Vue.js 单文件组件
|
||||
'.jsx', // React JSX 文件
|
||||
'.tsx', // React TSX 文件
|
||||
'.graphql', // GraphQL 查询语言文件
|
||||
'.gql', // GraphQL 查询语言文件
|
||||
'.proto', // Protocol Buffers 文件
|
||||
'.thrift', // Thrift 文件
|
||||
'.toml', // TOML 配置文件
|
||||
'.edn', // Clojure 数据表示文件
|
||||
'.cake', // CakePHP 配置文件
|
||||
'.ctp', // CakePHP 视图文件
|
||||
'.cfm', // ColdFusion 标记语言文件
|
||||
'.cfc', // ColdFusion 组件文件
|
||||
'.m', // Objective-C 或 MATLAB 源文件
|
||||
'.mm', // Objective-C++ 源文件
|
||||
'.gradle', // Gradle 构建文件
|
||||
'.groovy', // Gradle 构建文件
|
||||
'.kts', // Kotlin Script 文件
|
||||
'.java', // Java 代码文件
|
||||
'.cs', // C# 代码文件
|
||||
'.cpp', // C++ 代码文件
|
||||
'.c', // C++ 代码文件
|
||||
'.h', // C++ 头文件
|
||||
'.hpp', // C++ 头文件
|
||||
'.cc', // C++ 源文件
|
||||
'.cxx', // C++ 源文件
|
||||
'.cppm', // C++20 模块接口文件
|
||||
'.ipp', // 模板实现文件
|
||||
'.ixx', // C++20 模块实现文件
|
||||
'.f90', // Fortran 90 源文件
|
||||
'.f', // Fortran 固定格式源代码文件
|
||||
'.f03', // Fortran 2003+ 源代码文件
|
||||
'.ahk', // AutoHotKey 语言文件
|
||||
'.tcl', // Tcl 脚本
|
||||
'.do', // Questa 或 Modelsim Tcl 脚本
|
||||
'.v', // Verilog 源文件
|
||||
'.sv', // SystemVerilog 源文件
|
||||
'.svh', // SystemVerilog 头文件
|
||||
'.vhd', // VHDL 源文件
|
||||
'.vhdl', // VHDL 源文件
|
||||
'.lef', // Library Exchange Format
|
||||
'.def', // Design Exchange Format
|
||||
'.edif', // Electronic Design Interchange Format
|
||||
'.sdf', // Standard Delay Format
|
||||
'.sdc', // Synopsys Design Constraints
|
||||
'.xdc', // Xilinx Design Constraints
|
||||
'.rpt', // 报告文件
|
||||
'.lisp', // Lisp 脚本
|
||||
'.il', // Cadence SKILL 脚本
|
||||
'.ils', // Cadence SKILL++ 脚本
|
||||
'.sp', // SPICE netlist 文件
|
||||
'.spi', // SPICE netlist 文件
|
||||
'.cir', // SPICE netlist 文件
|
||||
'.net', // SPICE netlist 文件
|
||||
'.scs', // Spectre netlist 文件
|
||||
'.asc', // LTspice netlist schematic 文件
|
||||
'.tf' // Technology File
|
||||
]
|
||||
const textExtsByCategory = new Map([
|
||||
[
|
||||
'language',
|
||||
[
|
||||
'.js',
|
||||
'.mjs',
|
||||
'.cjs',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx', // JavaScript/TypeScript
|
||||
'.py', // Python
|
||||
'.java', // Java
|
||||
'.cs', // C#
|
||||
'.cpp',
|
||||
'.c',
|
||||
'.h',
|
||||
'.hpp',
|
||||
'.cc',
|
||||
'.cxx',
|
||||
'.cppm',
|
||||
'.ipp',
|
||||
'.ixx', // C/C++
|
||||
'.php', // PHP
|
||||
'.rb', // Ruby
|
||||
'.pl', // Perl
|
||||
'.go', // Go
|
||||
'.rs', // Rust
|
||||
'.swift', // Swift
|
||||
'.kt',
|
||||
'.kts', // Kotlin
|
||||
'.scala', // Scala
|
||||
'.lua', // Lua
|
||||
'.groovy', // Groovy
|
||||
'.dart', // Dart
|
||||
'.hs', // Haskell
|
||||
'.clj',
|
||||
'.cljs', // Clojure
|
||||
'.elm', // Elm
|
||||
'.erl', // Erlang
|
||||
'.ex',
|
||||
'.exs', // Elixir
|
||||
'.ml',
|
||||
'.mli', // OCaml
|
||||
'.fs', // F#
|
||||
'.r',
|
||||
'.R', // R
|
||||
'.sol', // Solidity
|
||||
'.awk', // AWK
|
||||
'.cob', // COBOL
|
||||
'.asm',
|
||||
'.s', // Assembly
|
||||
'.lisp',
|
||||
'.lsp', // Lisp
|
||||
'.coffee', // CoffeeScript
|
||||
'.ino', // Arduino
|
||||
'.jl', // Julia
|
||||
'.nim', // Nim
|
||||
'.zig', // Zig
|
||||
'.d', // D语言
|
||||
'.pas', // Pascal
|
||||
'.vb', // Visual Basic
|
||||
'.rkt', // Racket
|
||||
'.scm', // Scheme
|
||||
'.hx', // Haxe
|
||||
'.as', // ActionScript
|
||||
'.pde', // Processing
|
||||
'.f90',
|
||||
'.f',
|
||||
'.f03',
|
||||
'.for',
|
||||
'.f95', // Fortran
|
||||
'.adb',
|
||||
'.ads', // Ada
|
||||
'.pro', // Prolog
|
||||
'.m',
|
||||
'.mm', // Objective-C/MATLAB
|
||||
'.rpy', // Ren'Py
|
||||
'.ets', // OpenHarmony,
|
||||
'.uniswap', // DeFi
|
||||
'.vy', // Vyper
|
||||
'.shader',
|
||||
'.glsl',
|
||||
'.frag',
|
||||
'.vert',
|
||||
'.gd' // Godot
|
||||
]
|
||||
],
|
||||
[
|
||||
'script',
|
||||
[
|
||||
'.sh', // Shell
|
||||
'.bat',
|
||||
'.cmd', // Windows批处理
|
||||
'.ps1', // PowerShell
|
||||
'.tcl',
|
||||
'.do', // Tcl
|
||||
'.ahk', // AutoHotkey
|
||||
'.zsh', // Zsh
|
||||
'.fish', // Fish shell
|
||||
'.csh', // C shell
|
||||
'.vbs', // VBScript
|
||||
'.applescript', // AppleScript
|
||||
'.au3', // AutoIt
|
||||
'.bash',
|
||||
'.nu'
|
||||
]
|
||||
],
|
||||
[
|
||||
'style',
|
||||
[
|
||||
'.css', // CSS
|
||||
'.less', // Less
|
||||
'.scss',
|
||||
'.sass', // Sass
|
||||
'.styl', // Stylus
|
||||
'.pcss', // PostCSS
|
||||
'.postcss' // PostCSS
|
||||
]
|
||||
],
|
||||
[
|
||||
'template',
|
||||
[
|
||||
'.vue', // Vue.js
|
||||
'.pug',
|
||||
'.jade', // Pug/Jade
|
||||
'.haml', // Haml
|
||||
'.slim', // Slim
|
||||
'.tpl', // 通用模板
|
||||
'.ejs', // EJS
|
||||
'.hbs', // Handlebars
|
||||
'.mustache', // Mustache
|
||||
'.twig', // Twig
|
||||
'.blade', // Blade (Laravel)
|
||||
'.liquid', // Liquid
|
||||
'.jinja',
|
||||
'.jinja2',
|
||||
'.j2', // Jinja
|
||||
'.erb', // ERB
|
||||
'.vm', // Velocity
|
||||
'.ftl', // FreeMarker
|
||||
'.svelte', // Svelte
|
||||
'.astro' // Astro
|
||||
]
|
||||
],
|
||||
[
|
||||
'config',
|
||||
[
|
||||
'.ini', // INI配置
|
||||
'.conf',
|
||||
'.config', // 通用配置
|
||||
'.env', // 环境变量
|
||||
'.toml', // TOML
|
||||
'.cfg', // 通用配置
|
||||
'.properties', // Java属性
|
||||
'.desktop', // Linux桌面文件
|
||||
'.service', // systemd服务
|
||||
'.rc',
|
||||
'.bashrc',
|
||||
'.zshrc', // Shell配置
|
||||
'.fishrc', // Fish shell配置
|
||||
'.vimrc', // Vim配置
|
||||
'.htaccess', // Apache配置
|
||||
'.robots', // robots.txt
|
||||
'.editorconfig', // EditorConfig
|
||||
'.eslintrc', // ESLint
|
||||
'.prettierrc', // Prettier
|
||||
'.babelrc', // Babel
|
||||
'.npmrc', // npm
|
||||
'.dockerignore', // Docker ignore
|
||||
'.npmignore',
|
||||
'.yarnrc',
|
||||
'.prettierignore',
|
||||
'.eslintignore',
|
||||
'.browserslistrc',
|
||||
'.json5',
|
||||
'.tfvars'
|
||||
]
|
||||
],
|
||||
[
|
||||
'document',
|
||||
[
|
||||
'.txt',
|
||||
'.text', // 纯文本
|
||||
'.md',
|
||||
'.mdx', // Markdown
|
||||
'.html',
|
||||
'.htm',
|
||||
'.xhtml', // HTML
|
||||
'.xml', // XML
|
||||
'.org', // Org-mode
|
||||
'.wiki', // Wiki
|
||||
'.tex',
|
||||
'.bib', // LaTeX
|
||||
'.rst', // reStructuredText
|
||||
'.rtf', // 富文本
|
||||
'.nfo', // 信息文件
|
||||
'.adoc',
|
||||
'.asciidoc', // AsciiDoc
|
||||
'.pod', // Perl文档
|
||||
'.1',
|
||||
'.2',
|
||||
'.3',
|
||||
'.4',
|
||||
'.5',
|
||||
'.6',
|
||||
'.7',
|
||||
'.8',
|
||||
'.9', // man页面
|
||||
'.man', // man页面
|
||||
'.texi',
|
||||
'.texinfo', // Texinfo
|
||||
'.readme',
|
||||
'.me', // README
|
||||
'.changelog', // 变更日志
|
||||
'.license', // 许可证
|
||||
'.authors', // 作者文件
|
||||
'.po',
|
||||
'.pot'
|
||||
]
|
||||
],
|
||||
[
|
||||
'data',
|
||||
[
|
||||
'.json', // JSON
|
||||
'.jsonc', // JSON with comments
|
||||
'.yaml',
|
||||
'.yml', // YAML
|
||||
'.csv',
|
||||
'.tsv', // 分隔值文件
|
||||
'.edn', // Clojure数据
|
||||
'.jsonl',
|
||||
'.ndjson', // 换行分隔JSON
|
||||
'.geojson', // GeoJSON
|
||||
'.gpx', // GPS Exchange
|
||||
'.kml', // Keyhole Markup
|
||||
'.rss',
|
||||
'.atom', // Feed格式
|
||||
'.vcf', // vCard
|
||||
'.ics', // iCalendar
|
||||
'.ldif', // LDAP数据交换
|
||||
'.pbtxt',
|
||||
'.map'
|
||||
]
|
||||
],
|
||||
[
|
||||
'build',
|
||||
[
|
||||
'.gradle', // Gradle
|
||||
'.make',
|
||||
'.mk', // Make
|
||||
'.cmake', // CMake
|
||||
'.sbt', // SBT
|
||||
'.rake', // Rake
|
||||
'.spec', // RPM spec
|
||||
'.pom',
|
||||
'.build', // Meson
|
||||
'.bazel' // Bazel
|
||||
]
|
||||
],
|
||||
[
|
||||
'database',
|
||||
[
|
||||
'.sql', // SQL
|
||||
'.ddl',
|
||||
'.dml', // DDL/DML
|
||||
'.plsql', // PL/SQL
|
||||
'.psql', // PostgreSQL
|
||||
'.cypher', // Cypher
|
||||
'.sparql' // SPARQL
|
||||
]
|
||||
],
|
||||
[
|
||||
'web',
|
||||
[
|
||||
'.graphql',
|
||||
'.gql', // GraphQL
|
||||
'.proto', // Protocol Buffers
|
||||
'.thrift', // Thrift
|
||||
'.wsdl', // WSDL
|
||||
'.raml', // RAML
|
||||
'.swagger',
|
||||
'.openapi' // API文档
|
||||
]
|
||||
],
|
||||
[
|
||||
'version',
|
||||
[
|
||||
'.gitignore', // Git ignore
|
||||
'.gitattributes', // Git attributes
|
||||
'.gitconfig', // Git config
|
||||
'.hgignore', // Mercurial ignore
|
||||
'.bzrignore', // Bazaar ignore
|
||||
'.svnignore', // SVN ignore
|
||||
'.githistory' // Git history
|
||||
]
|
||||
],
|
||||
[
|
||||
'subtitle',
|
||||
[
|
||||
'.srt',
|
||||
'.sub',
|
||||
'.ass' // 字幕格式
|
||||
]
|
||||
],
|
||||
[
|
||||
'log',
|
||||
[
|
||||
'.log',
|
||||
'.rpt' // 日志和报告 (移除了.out,因为通常是二进制可执行文件)
|
||||
]
|
||||
],
|
||||
[
|
||||
'eda',
|
||||
[
|
||||
'.v',
|
||||
'.sv',
|
||||
'.svh', // Verilog/SystemVerilog
|
||||
'.vhd',
|
||||
'.vhdl', // VHDL
|
||||
'.lef',
|
||||
'.def', // LEF/DEF
|
||||
'.edif', // EDIF
|
||||
'.sdf', // SDF
|
||||
'.sdc',
|
||||
'.xdc', // 约束文件
|
||||
'.sp',
|
||||
'.spi',
|
||||
'.cir',
|
||||
'.net', // SPICE
|
||||
'.scs', // Spectre
|
||||
'.asc', // LTspice
|
||||
'.tf', // Technology File
|
||||
'.il',
|
||||
'.ils' // SKILL
|
||||
]
|
||||
],
|
||||
[
|
||||
'game',
|
||||
[
|
||||
'.mtl', // Material Template Library
|
||||
'.x3d', // X3D文件
|
||||
'.gltf', // glTF JSON
|
||||
'.prefab', // Unity预制体 (YAML格式)
|
||||
'.meta' // Unity元数据文件 (YAML格式)
|
||||
]
|
||||
],
|
||||
[
|
||||
'other',
|
||||
[
|
||||
'.mcfunction', // Minecraft函数
|
||||
'.jsp', // JSP
|
||||
'.aspx', // ASP.NET
|
||||
'.ipynb', // Jupyter Notebook
|
||||
'.cake',
|
||||
'.ctp', // CakePHP
|
||||
'.cfm',
|
||||
'.cfc' // ColdFusion
|
||||
]
|
||||
]
|
||||
])
|
||||
|
||||
export const textExts = Array.from(textExtsByCategory.values()).flat()
|
||||
|
||||
export const ZOOM_LEVELS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]
|
||||
|
||||
@@ -170,3 +403,8 @@ export const KB = 1024
|
||||
export const MB = 1024 * KB
|
||||
export const GB = 1024 * MB
|
||||
export const defaultLanguage = 'en-US'
|
||||
|
||||
export enum FeedUrl {
|
||||
PRODUCTION = 'https://releases.cherry-ai.com',
|
||||
EARLY_ACCESS = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ if (isDev) {
|
||||
export const DATA_PATH = getDataPath()
|
||||
|
||||
export const titleBarOverlayDark = {
|
||||
height: 40,
|
||||
height: 42,
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#fff'
|
||||
}
|
||||
|
||||
export const titleBarOverlayLight = {
|
||||
height: 40,
|
||||
height: 42,
|
||||
color: 'rgba(255,255,255,0)',
|
||||
symbolColor: '#000'
|
||||
}
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
interface IBlacklist {
|
||||
interface IFilterList {
|
||||
WINDOWS: string[]
|
||||
MAC?: string[]
|
||||
}
|
||||
|
||||
interface IFinetunedList {
|
||||
EXCLUDE_CLIPBOARD_CURSOR_DETECT: IFilterList
|
||||
INCLUDE_CLIPBOARD_DELAY_READ: IFilterList
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
* 注意:请不要修改此配置,除非你非常清楚其含义、影响和行为的目的
|
||||
* Note: Do not modify this configuration unless you fully understand its meaning, implications, and intended behavior.
|
||||
* -----------------------------------------------------------------------
|
||||
* A predefined application filter list to include commonly used software
|
||||
* that does not require text selection but may conflict with it, and disable them in advance.
|
||||
* Only available in the selected mode.
|
||||
*
|
||||
* Specification: must be all lowercase, need to accurately find the actual running program name
|
||||
*************************************************************************/
|
||||
export const SELECTION_PREDEFINED_BLACKLIST: IBlacklist = {
|
||||
export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
|
||||
WINDOWS: [
|
||||
'explorer.exe',
|
||||
// Screenshot
|
||||
'snipaste.exe',
|
||||
'pixpin.exe',
|
||||
'sharex.exe',
|
||||
// Office
|
||||
'excel.exe',
|
||||
'powerpnt.exe',
|
||||
// Image Editor
|
||||
'photoshop.exe',
|
||||
'illustrator.exe',
|
||||
@@ -32,6 +42,17 @@ export const SELECTION_PREDEFINED_BLACKLIST: IBlacklist = {
|
||||
'maya.exe',
|
||||
// CAD
|
||||
'acad.exe',
|
||||
'sldworks.exe'
|
||||
'sldworks.exe',
|
||||
// Remote Desktop
|
||||
'mstsc.exe'
|
||||
]
|
||||
}
|
||||
|
||||
export const SELECTION_FINETUNED_LIST: IFinetunedList = {
|
||||
EXCLUDE_CLIPBOARD_CURSOR_DETECT: {
|
||||
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe']
|
||||
},
|
||||
INCLUDE_CLIPBOARD_DELAY_READ: {
|
||||
WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { app } from 'electron'
|
||||
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { isDev } from './constant'
|
||||
import { isDev, isWin } from './constant'
|
||||
import { registerIpc } from './ipc'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
@@ -24,6 +24,16 @@ import { setUserDataDir } from './utils/file'
|
||||
|
||||
Logger.initialize()
|
||||
|
||||
/**
|
||||
* Disable chromium's window animations
|
||||
* main purpose for this is to avoid the transparent window flashing when it is shown
|
||||
* (especially on Windows for SelectionAssistant Toolbar)
|
||||
* Know Issue: https://github.com/electron/electron/issues/12130#issuecomment-627198990
|
||||
*/
|
||||
if (isWin) {
|
||||
app.commandLine.appendSwitch('wm-window-animations-disabled')
|
||||
}
|
||||
|
||||
// in production mode, handle uncaught exception and unhandled rejection globally
|
||||
if (!isDev) {
|
||||
// handle uncaught exception
|
||||
|
||||
@@ -34,6 +34,7 @@ import { calculateDirectorySize, getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { FeedUrl } from '@shared/config/constant'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
@@ -112,6 +113,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
configManager.setAutoUpdate(isActive)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.App_SetFeedUrl, (_, feedUrl: FeedUrl) => {
|
||||
appUpdater.setFeedUrl(feedUrl)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
|
||||
configManager.set(key, value, isNotify)
|
||||
})
|
||||
@@ -346,4 +351,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// selection assistant
|
||||
SelectionService.registerIpcHandler()
|
||||
|
||||
ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { isWin } from '@main/constant'
|
||||
import { locales } from '@main/utils/locales'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { FeedUrl } from '@shared/config/constant'
|
||||
import { UpdateInfo } from 'builder-util-runtime'
|
||||
import { app, BrowserWindow, dialog } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
@@ -19,6 +21,7 @@ export default class AppUpdater {
|
||||
autoUpdater.forceDevUpdateConfig = !app.isPackaged
|
||||
autoUpdater.autoDownload = configManager.getAutoUpdate()
|
||||
autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate()
|
||||
autoUpdater.setFeedURL(configManager.getFeedUrl())
|
||||
|
||||
// 检测下载错误
|
||||
autoUpdater.on('error', (error) => {
|
||||
@@ -61,6 +64,11 @@ export default class AppUpdater {
|
||||
autoUpdater.autoInstallOnAppQuit = isActive
|
||||
}
|
||||
|
||||
public setFeedUrl(feedUrl: FeedUrl) {
|
||||
autoUpdater.setFeedURL(feedUrl)
|
||||
configManager.setFeedUrl(feedUrl)
|
||||
}
|
||||
|
||||
public async checkForUpdates() {
|
||||
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
|
||||
return {
|
||||
@@ -94,15 +102,22 @@ export default class AppUpdater {
|
||||
if (!this.releaseInfo) {
|
||||
return
|
||||
}
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { update: updateLocale } = locale.translation
|
||||
|
||||
let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes)
|
||||
if (detail === '') {
|
||||
detail = updateLocale.noReleaseNotes
|
||||
}
|
||||
|
||||
dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: '安装更新',
|
||||
title: updateLocale.title,
|
||||
icon,
|
||||
message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
|
||||
detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
|
||||
buttons: ['稍后安装', '立即安装'],
|
||||
message: updateLocale.message.replace('{{version}}', this.releaseInfo.version),
|
||||
detail,
|
||||
buttons: [updateLocale.later, updateLocale.install],
|
||||
defaultId: 1,
|
||||
cancelId: 0
|
||||
})
|
||||
@@ -118,7 +133,7 @@ export default class AppUpdater {
|
||||
|
||||
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
|
||||
if (!releaseNotes) {
|
||||
return '暂无更新说明'
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof releaseNotes === 'string') {
|
||||
|
||||
@@ -7,7 +7,7 @@ import Logger from 'electron-log'
|
||||
import * as fs from 'fs-extra'
|
||||
import StreamZip from 'node-stream-zip'
|
||||
import * as path from 'path'
|
||||
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
|
||||
import { CreateDirectoryOptions, FileStat } from 'webdav'
|
||||
|
||||
import WebDav from './WebDav'
|
||||
import { windowService } from './WindowService'
|
||||
@@ -295,10 +295,12 @@ class BackupManager {
|
||||
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
|
||||
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
|
||||
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
|
||||
const contentLength = (await fs.stat(backupedFilePath)).size
|
||||
const webdavClient = new WebDav(webdavConfig)
|
||||
try {
|
||||
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
|
||||
overwrite: true
|
||||
overwrite: true,
|
||||
contentLength
|
||||
})
|
||||
// 上传成功后删除本地备份文件
|
||||
await fs.remove(backupedFilePath)
|
||||
@@ -340,12 +342,8 @@ class BackupManager {
|
||||
|
||||
listWebdavFiles = async (_: Electron.IpcMainInvokeEvent, config: WebDavConfig) => {
|
||||
try {
|
||||
const client = createClient(config.webdavHost, {
|
||||
username: config.webdavUser,
|
||||
password: config.webdavPass
|
||||
})
|
||||
|
||||
const response = await client.getDirectoryContents(config.webdavPath)
|
||||
const client = new WebDav(config)
|
||||
const response = await client.getDirectoryContents()
|
||||
const files = Array.isArray(response) ? response : response.data
|
||||
|
||||
return files
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant'
|
||||
import { defaultLanguage, FeedUrl, ZOOM_SHORTCUTS } from '@shared/config/constant'
|
||||
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
|
||||
import { app } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
@@ -16,6 +16,7 @@ export enum ConfigKeys {
|
||||
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
|
||||
EnableQuickAssistant = 'enableQuickAssistant',
|
||||
AutoUpdate = 'autoUpdate',
|
||||
FeedUrl = 'feedUrl',
|
||||
EnableDataCollection = 'enableDataCollection',
|
||||
SelectionAssistantEnabled = 'selectionAssistantEnabled',
|
||||
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
|
||||
@@ -141,6 +142,14 @@ export class ConfigManager {
|
||||
this.set(ConfigKeys.AutoUpdate, value)
|
||||
}
|
||||
|
||||
getFeedUrl(): string {
|
||||
return this.get<string>(ConfigKeys.FeedUrl, FeedUrl.PRODUCTION)
|
||||
}
|
||||
|
||||
setFeedUrl(value: FeedUrl) {
|
||||
this.set(ConfigKeys.FeedUrl, value)
|
||||
}
|
||||
|
||||
getEnableDataCollection(): boolean {
|
||||
return this.get<boolean>(ConfigKeys.EnableDataCollection, true)
|
||||
}
|
||||
@@ -151,7 +160,7 @@ export class ConfigManager {
|
||||
|
||||
// Selection Assistant: is enabled the selection assistant
|
||||
getSelectionAssistantEnabled(): boolean {
|
||||
return this.get<boolean>(ConfigKeys.SelectionAssistantEnabled, true)
|
||||
return this.get<boolean>(ConfigKeys.SelectionAssistantEnabled, false)
|
||||
}
|
||||
|
||||
setSelectionAssistantEnabled(value: boolean) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
export default class FileService {
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
|
||||
return fs.readFileSync(path, 'utf8')
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, pathOrUrl: string, encoding?: BufferEncoding) {
|
||||
const path = pathOrUrl.startsWith('file://') ? new URL(pathOrUrl) : pathOrUrl
|
||||
if (encoding) return fs.readFile(path, { encoding })
|
||||
return fs.readFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
|
||||
import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig'
|
||||
import { isDev, isWin } from '@main/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { BrowserWindow, ipcMain, screen } from 'electron'
|
||||
@@ -37,6 +37,11 @@ type RelativeOrientation =
|
||||
| 'middleRight'
|
||||
| 'center'
|
||||
|
||||
enum TriggerMode {
|
||||
Selected = 'selected',
|
||||
Ctrlkey = 'ctrlkey'
|
||||
}
|
||||
|
||||
/** SelectionService is a singleton class that manages the selection hook and the toolbar window
|
||||
*
|
||||
* Features:
|
||||
@@ -59,7 +64,7 @@ export class SelectionService {
|
||||
private initStatus: boolean = false
|
||||
private started: boolean = false
|
||||
|
||||
private triggerMode = 'selected'
|
||||
private triggerMode = TriggerMode.Selected
|
||||
private isFollowToolbar = true
|
||||
private isRemeberWinSize = false
|
||||
private filterMode = 'default'
|
||||
@@ -145,17 +150,25 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
private initConfig() {
|
||||
this.triggerMode = configManager.getSelectionAssistantTriggerMode()
|
||||
this.triggerMode = configManager.getSelectionAssistantTriggerMode() as TriggerMode
|
||||
this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar()
|
||||
this.isRemeberWinSize = configManager.getSelectionAssistantRemeberWinSize()
|
||||
this.filterMode = configManager.getSelectionAssistantFilterMode()
|
||||
this.filterList = configManager.getSelectionAssistantFilterList()
|
||||
|
||||
this.setHookGlobalFilterMode(this.filterMode, this.filterList)
|
||||
this.setHookFineTunedList()
|
||||
|
||||
configManager.subscribe(ConfigKeys.SelectionAssistantTriggerMode, (triggerMode: TriggerMode) => {
|
||||
const oldTriggerMode = this.triggerMode
|
||||
|
||||
configManager.subscribe(ConfigKeys.SelectionAssistantTriggerMode, (triggerMode: string) => {
|
||||
this.triggerMode = triggerMode
|
||||
this.processTriggerMode()
|
||||
|
||||
//trigger mode changed, need to update the filter list
|
||||
if (oldTriggerMode !== triggerMode) {
|
||||
this.setHookGlobalFilterMode(this.filterMode, this.filterList)
|
||||
}
|
||||
})
|
||||
|
||||
configManager.subscribe(ConfigKeys.SelectionAssistantFollowToolbar, (isFollowToolbar: boolean) => {
|
||||
@@ -193,28 +206,31 @@ export class SelectionService {
|
||||
if (!this.selectionHook) return
|
||||
|
||||
const modeMap = {
|
||||
default: 0,
|
||||
whitelist: 1,
|
||||
blacklist: 2
|
||||
default: SelectionHook!.FilterMode.DEFAULT,
|
||||
whitelist: SelectionHook!.FilterMode.INCLUDE_LIST,
|
||||
blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST
|
||||
}
|
||||
|
||||
let combinedList: string[] = []
|
||||
let combinedList: string[] = list
|
||||
let combinedMode = mode
|
||||
|
||||
switch (mode) {
|
||||
case 'blacklist':
|
||||
//combine the predefined blacklist with the user-defined blacklist
|
||||
combinedList = [...new Set([...list, ...SELECTION_PREDEFINED_BLACKLIST.WINDOWS])]
|
||||
break
|
||||
case 'whitelist':
|
||||
combinedList = [...list]
|
||||
break
|
||||
case 'default':
|
||||
default:
|
||||
//use the predefined blacklist as the default filter list
|
||||
combinedList = [...SELECTION_PREDEFINED_BLACKLIST.WINDOWS]
|
||||
combinedMode = 'blacklist'
|
||||
break
|
||||
//only the selected mode need to combine the predefined blacklist with the user-defined blacklist
|
||||
if (this.triggerMode === TriggerMode.Selected) {
|
||||
switch (mode) {
|
||||
case 'blacklist':
|
||||
//combine the predefined blacklist with the user-defined blacklist
|
||||
combinedList = [...new Set([...list, ...SELECTION_PREDEFINED_BLACKLIST.WINDOWS])]
|
||||
break
|
||||
case 'whitelist':
|
||||
combinedList = [...list]
|
||||
break
|
||||
case 'default':
|
||||
default:
|
||||
//use the predefined blacklist as the default filter list
|
||||
combinedList = [...SELECTION_PREDEFINED_BLACKLIST.WINDOWS]
|
||||
combinedMode = 'blacklist'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selectionHook.setGlobalFilterMode(modeMap[combinedMode], combinedList)) {
|
||||
@@ -222,6 +238,20 @@ export class SelectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private setHookFineTunedList() {
|
||||
if (!this.selectionHook) return
|
||||
|
||||
this.selectionHook.setFineTunedList(
|
||||
SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT,
|
||||
SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS
|
||||
)
|
||||
|
||||
this.selectionHook.setFineTunedList(
|
||||
SelectionHook!.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ,
|
||||
SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the selection service and initialize required windows
|
||||
* @returns {boolean} Success status of service start
|
||||
@@ -274,7 +304,12 @@ export class SelectionService {
|
||||
if (!this.selectionHook) return false
|
||||
|
||||
this.selectionHook.stop()
|
||||
this.selectionHook.cleanup()
|
||||
this.selectionHook.cleanup() //already remove all listeners
|
||||
|
||||
//reset the listener states
|
||||
this.isCtrlkeyListenerActive = false
|
||||
this.isHideByMouseKeyListenerActive = false
|
||||
|
||||
if (this.toolbarWindow) {
|
||||
this.toolbarWindow.close()
|
||||
this.toolbarWindow = null
|
||||
@@ -774,11 +809,11 @@ export class SelectionService {
|
||||
*/
|
||||
private handleKeyDownHide = (data: KeyboardEventData) => {
|
||||
//dont hide toolbar when ctrlkey is pressed
|
||||
if (this.triggerMode === 'ctrlkey' && this.isCtrlkey(data.vkCode)) {
|
||||
if (this.triggerMode === TriggerMode.Ctrlkey && this.isCtrlkey(data.vkCode)) {
|
||||
return
|
||||
}
|
||||
//dont hide toolbar when shiftkey is pressed, because it's used for selection
|
||||
if (this.isShiftkey(data.vkCode)) {
|
||||
//dont hide toolbar when shiftkey or altkey is pressed, because it's used for selection
|
||||
if (this.isShiftkey(data.vkCode) || this.isAltkey(data.vkCode)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -806,6 +841,9 @@ export class SelectionService {
|
||||
//ctrlkey pressed
|
||||
if (this.lastCtrlkeyDownTime === 0) {
|
||||
this.lastCtrlkeyDownTime = Date.now()
|
||||
//add the mouse-wheel&mouse-down listener, detect if user is zooming in/out or multi-selecting
|
||||
this.selectionHook!.on('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
|
||||
this.selectionHook!.on('mouse-down', this.handleMouseDownCtrlkeyMode)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -829,9 +867,30 @@ export class SelectionService {
|
||||
*/
|
||||
private handleKeyUpCtrlkeyMode = (data: KeyboardEventData) => {
|
||||
if (!this.isCtrlkey(data.vkCode)) return
|
||||
//remove the mouse-wheel&mouse-down listener
|
||||
this.selectionHook!.off('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
|
||||
this.selectionHook!.off('mouse-down', this.handleMouseDownCtrlkeyMode)
|
||||
this.lastCtrlkeyDownTime = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse wheel events in ctrlkey trigger mode
|
||||
* ignore CtrlKey pressing when mouse wheel is used
|
||||
* because user is zooming in/out
|
||||
*/
|
||||
private handleMouseWheelCtrlkeyMode = () => {
|
||||
this.lastCtrlkeyDownTime = -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse down events in ctrlkey trigger mode
|
||||
* ignore CtrlKey pressing when mouse down is used
|
||||
* because user is multi-selecting
|
||||
*/
|
||||
private handleMouseDownCtrlkeyMode = () => {
|
||||
this.lastCtrlkeyDownTime = -1
|
||||
}
|
||||
|
||||
//check if the key is ctrl key
|
||||
private isCtrlkey(vkCode: number) {
|
||||
return vkCode === 162 || vkCode === 163
|
||||
@@ -842,6 +901,11 @@ export class SelectionService {
|
||||
return vkCode === 160 || vkCode === 161
|
||||
}
|
||||
|
||||
//check if the key is alt key
|
||||
private isAltkey(vkCode: number) {
|
||||
return vkCode === 164 || vkCode === 165
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a preloaded action window for quick response
|
||||
* Action windows handle specific operations on selected text
|
||||
@@ -1042,7 +1106,7 @@ export class SelectionService {
|
||||
* Manages appropriate event listeners for each mode
|
||||
*/
|
||||
private processTriggerMode() {
|
||||
if (this.triggerMode === 'selected') {
|
||||
if (this.triggerMode === TriggerMode.Selected) {
|
||||
if (this.isCtrlkeyListenerActive) {
|
||||
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
|
||||
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
|
||||
@@ -1051,7 +1115,7 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
this.selectionHook!.setSelectionPassiveMode(false)
|
||||
} else if (this.triggerMode === 'ctrlkey') {
|
||||
} else if (this.triggerMode === TriggerMode.Ctrlkey) {
|
||||
if (!this.isCtrlkeyListenerActive) {
|
||||
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
|
||||
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { WebDavConfig } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import https from 'https'
|
||||
import path from 'path'
|
||||
import Stream from 'stream'
|
||||
import {
|
||||
BufferLike,
|
||||
@@ -14,13 +16,14 @@ export default class WebDav {
|
||||
private webdavPath: string
|
||||
|
||||
constructor(params: WebDavConfig) {
|
||||
this.webdavPath = params.webdavPath
|
||||
this.webdavPath = params.webdavPath || '/'
|
||||
|
||||
this.instance = createClient(params.webdavHost, {
|
||||
username: params.webdavUser,
|
||||
password: params.webdavPass,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity
|
||||
maxContentLength: Infinity,
|
||||
httpsAgent: new https.Agent({ rejectUnauthorized: false })
|
||||
})
|
||||
|
||||
this.putFileContents = this.putFileContents.bind(this)
|
||||
@@ -49,7 +52,7 @@ export default class WebDav {
|
||||
throw error
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
const remoteFilePath = path.posix.join(this.webdavPath, filename)
|
||||
|
||||
try {
|
||||
return await this.instance.putFileContents(remoteFilePath, data, options)
|
||||
@@ -64,7 +67,7 @@ export default class WebDav {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
const remoteFilePath = path.posix.join(this.webdavPath, filename)
|
||||
|
||||
try {
|
||||
return await this.instance.getFileContents(remoteFilePath, options)
|
||||
@@ -74,6 +77,19 @@ export default class WebDav {
|
||||
}
|
||||
}
|
||||
|
||||
public getDirectoryContents = async () => {
|
||||
if (!this.instance) {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.instance.getDirectoryContents(this.webdavPath)
|
||||
} catch (error) {
|
||||
Logger.error('[WebDAV] Error getting directory contents on WebDAV:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public checkConnection = async () => {
|
||||
if (!this.instance) {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
@@ -105,7 +121,7 @@ export default class WebDav {
|
||||
throw new Error('WebDAV client not initialized')
|
||||
}
|
||||
|
||||
const remoteFilePath = `${this.webdavPath}/${filename}`
|
||||
const remoteFilePath = path.posix.join(this.webdavPath, filename)
|
||||
|
||||
try {
|
||||
return await this.instance.deleteFile(remoteFilePath)
|
||||
|
||||
@@ -56,14 +56,14 @@ export class WindowService {
|
||||
minHeight: 600,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
transparent: false,
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
|
||||
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
trafficLightPosition: { x: 12, y: 12 },
|
||||
...(isLinux ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
@@ -544,6 +544,25 @@ export class WindowService {
|
||||
public setPinMiniWindow(isPinned) {
|
||||
this.isPinnedMiniWindow = isPinned
|
||||
}
|
||||
|
||||
/**
|
||||
* 引用文本到主窗口
|
||||
* @param text 原始文本(未格式化)
|
||||
*/
|
||||
public quoteToMainWindow(text: string): void {
|
||||
try {
|
||||
this.showMainWindow()
|
||||
|
||||
const mainWindow = this.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
setTimeout(() => {
|
||||
mainWindow.webContents.send(IpcChannel.App_QuoteToMain, text)
|
||||
}, 100)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('Failed to quote to main window:', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const windowService = WindowService.getInstance()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { getConfigDir } from '@main/utils/file'
|
||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth'
|
||||
import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth'
|
||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||
import {
|
||||
OAuthClientInformation,
|
||||
OAuthClientInformationFull,
|
||||
OAuthTokens
|
||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||
import Logger from 'electron-log'
|
||||
import open from 'open'
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) {
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
|
||||
mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { FeedUrl } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, ThemeMode, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
|
||||
@@ -20,6 +21,7 @@ const api = {
|
||||
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
|
||||
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
|
||||
setFeedUrl: (feedUrl: FeedUrl) => ipcRenderer.invoke(IpcChannel.App_SetFeedUrl, feedUrl),
|
||||
setTheme: (theme: ThemeMode) => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
|
||||
handleZoomFactor: (delta: number, reset: boolean = false) =>
|
||||
ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
|
||||
@@ -84,7 +86,7 @@ const api = {
|
||||
getPathForFile: (file: File) => webUtils.getPathForFile(file)
|
||||
},
|
||||
fs: {
|
||||
read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
|
||||
read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding)
|
||||
},
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
|
||||
@@ -226,7 +228,8 @@ const api = {
|
||||
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
|
||||
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
|
||||
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
|
||||
}
|
||||
},
|
||||
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import MainSidebar from './components/app/MainSidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
@@ -13,11 +13,11 @@ import { NotificationProvider } from './context/NotificationProvider'
|
||||
import StyleSheetManager from './context/StyleSheetManager'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import NavigationHandler from './handler/NavigationHandler'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import DiscoverPage from './pages/discover'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import McpServersPage from './pages/mcp-servers'
|
||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
@@ -34,16 +34,18 @@ function App(): React.ReactElement {
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<NavigationHandler />
|
||||
<Sidebar />
|
||||
<MainSidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
{/* <Route path="/agents" element={<AgentsPage />} /> */}
|
||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||
<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="/mcp-servers/*" element={<McpServersPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
<Route path="/discover/*" element={<DiscoverPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
}
|
||||
|
||||
.minapp-drawer {
|
||||
max-width: calc(100vw - var(--sidebar-width));
|
||||
.ant-drawer-content-wrapper {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -33,7 +32,7 @@
|
||||
position: absolute;
|
||||
-webkit-app-region: drag;
|
||||
min-height: calc(var(--navbar-height) + 0.5px);
|
||||
width: calc(100vw - var(--sidebar-width));
|
||||
width: 100%;
|
||||
margin-top: -0.5px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
--color-primary-mute: #00b96b33;
|
||||
|
||||
--color-text: var(--color-text-1);
|
||||
--color-text-secondary: rgba(235, 235, 245, 0.7);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff19;
|
||||
--color-border: #383838;
|
||||
--color-border-soft: #ffffff10;
|
||||
--color-border-mute: #ffffff05;
|
||||
--color-error: #f44336;
|
||||
@@ -43,6 +44,9 @@
|
||||
--color-reference-text: #ffffff;
|
||||
--color-reference-background: #0b0e12;
|
||||
|
||||
--color-list-item: rgba(255, 255, 255, 0.1);
|
||||
--color-list-item-hover: rgba(255, 255, 255, 0.05);
|
||||
|
||||
--modal-background: #1f1f1f;
|
||||
|
||||
--color-highlight: rgba(0, 0, 0, 1);
|
||||
@@ -52,7 +56,7 @@
|
||||
--navbar-background-mac: rgba(20, 20, 20, 0.55);
|
||||
--navbar-background: #1f1f1f;
|
||||
|
||||
--navbar-height: 40px;
|
||||
--navbar-height: 42px;
|
||||
--sidebar-width: 50px;
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 100px;
|
||||
@@ -62,12 +66,13 @@
|
||||
--settings-width: 250px;
|
||||
--scrollbar-width: 5px;
|
||||
|
||||
--chat-background: #111111;
|
||||
--chat-background-user: #28b561;
|
||||
--chat-background-assistant: #2c2c2c;
|
||||
--chat-background: transparent;
|
||||
--chat-background-user: #232325;
|
||||
--chat-background-assistant: transparent;
|
||||
--chat-text-user: var(--color-black);
|
||||
|
||||
--list-item-border-radius: 16px;
|
||||
--list-item-border-radius: 8px;
|
||||
--border-width: 0.5px;
|
||||
}
|
||||
|
||||
[theme-mode='light'] {
|
||||
@@ -98,6 +103,7 @@
|
||||
--color-primary-mute: #00b96b33;
|
||||
|
||||
--color-text: var(--color-text-1);
|
||||
--color-text-secondary: rgba(0, 0, 0, 0.75);
|
||||
--color-icon: #00000099;
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000019;
|
||||
@@ -115,6 +121,9 @@
|
||||
--color-reference-text: #000000;
|
||||
--color-reference-background: #f1f7ff;
|
||||
|
||||
--color-list-item: rgba(255, 255, 255, 0.9);
|
||||
--color-list-item-hover: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--modal-background: var(--color-white);
|
||||
|
||||
--color-highlight: initial;
|
||||
@@ -124,8 +133,10 @@
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.55);
|
||||
--navbar-background: rgba(244, 244, 244);
|
||||
|
||||
--chat-background: #f3f3f3;
|
||||
--chat-background-user: #95ec69;
|
||||
--chat-background-assistant: #ffffff;
|
||||
--chat-background: transparent;
|
||||
--chat-background-user: #f3f3f3;
|
||||
--chat-background-assistant: transparent;
|
||||
--chat-text-user: var(--color-text);
|
||||
|
||||
--border-width: 0.5px;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#content-container {
|
||||
background-color: var(--color-background);
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
border-top-left-radius: 10px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
}
|
||||
|
||||
.group-container {
|
||||
@@ -10,3 +8,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
// margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -119,32 +119,21 @@ ul {
|
||||
#messages {
|
||||
background-color: var(--chat-background);
|
||||
}
|
||||
#inputbar {
|
||||
margin: -5px 15px 15px 15px;
|
||||
background: var(--color-background);
|
||||
}
|
||||
.system-prompt {
|
||||
background-color: var(--chat-background-assistant);
|
||||
}
|
||||
.message-content-container {
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
.message-thought-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.message-user {
|
||||
color: var(--chat-text-user);
|
||||
.markdown,
|
||||
.anticon,
|
||||
.iconfont,
|
||||
.lucide,
|
||||
.message-tokens {
|
||||
color: var(--chat-text-user) !important;
|
||||
}
|
||||
.message-action-button:hover {
|
||||
background-color: var(--color-white-soft);
|
||||
.message-content-container {
|
||||
margin: 5px 0;
|
||||
border-radius: 8px 0 8px 8px;
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
}
|
||||
.group-grid-container.horizontal,
|
||||
|
||||
@@ -306,9 +306,14 @@ mjx-container {
|
||||
|
||||
/* CodeMirror 相关样式 */
|
||||
.cm-editor {
|
||||
border-radius: 5px;
|
||||
|
||||
&.cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
font-family: var(--code-font-family);
|
||||
padding: 1px;
|
||||
border-radius: 5px;
|
||||
|
||||
.cm-gutters {
|
||||
|
||||
146
src/renderer/src/assets/styles/tailwind.css
Normal file
146
src/renderer/src/assets/styles/tailwind.css
Normal file
@@ -0,0 +1,146 @@
|
||||
@import 'tailwindcss' source('../../../src');
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 如需自定义:
|
||||
1. 清晰地组织自定义 CSS 到相应的层中。
|
||||
2. 基础样式(如全局重置、链接样式)放入 base 层;
|
||||
3. 可复用的组件样式(如果仍使用 @apply 或原生 CSS 嵌套创建)放入 components 层;
|
||||
4. 新的自定义工具类放入 utilities 层。
|
||||
*/
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.21 0.006 285.885);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.015 286.067);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-marquee: marquee var(--duration) infinite linear;
|
||||
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -134,26 +134,31 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
|
||||
return () => cleanupTokenizers(callerId)
|
||||
}, [callerId, cleanupTokenizers])
|
||||
|
||||
// 处理第二次开始的代码高亮
|
||||
// 触发代码高亮
|
||||
// - 进入视口后触发第一次高亮
|
||||
// - 内容变化后触发之后的高亮
|
||||
useEffect(() => {
|
||||
if (prevCodeLengthRef.current > 0) {
|
||||
setTimeout(highlightCode, 0)
|
||||
}
|
||||
}, [highlightCode])
|
||||
|
||||
// 视口检测逻辑,只处理第一次代码高亮
|
||||
useEffect(() => {
|
||||
const codeElement = codeContentRef.current
|
||||
if (!codeElement || prevCodeLengthRef.current > 0) return
|
||||
|
||||
let isMounted = true
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && isMounted) {
|
||||
setTimeout(highlightCode, 0)
|
||||
observer.disconnect()
|
||||
if (prevCodeLengthRef.current > 0) {
|
||||
setTimeout(highlightCode, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const codeElement = codeContentRef.current
|
||||
if (!codeElement) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].intersectionRatio > 0 && isMounted) {
|
||||
setTimeout(highlightCode, 0)
|
||||
observer.disconnect()
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: '50px 0px 50px 0px'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
observer.observe(codeElement)
|
||||
|
||||
@@ -231,7 +236,6 @@ const ContentContainer = styled.div<{
|
||||
$wrap: boolean
|
||||
$fadeIn: boolean
|
||||
}>`
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
border: 0.5px solid transparent;
|
||||
@@ -239,12 +243,11 @@ const ContentContainer = styled.div<{
|
||||
margin-top: 0;
|
||||
|
||||
.shiki {
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
padding: 1em;
|
||||
|
||||
code {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.line {
|
||||
display: block;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import { Flex } from 'antd'
|
||||
import React, { memo, startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Flex, Spin } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import React, { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
@@ -10,12 +12,16 @@ interface Props {
|
||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||
}
|
||||
|
||||
/** 预览 Mermaid 图表
|
||||
* 通过防抖渲染提供比较统一的体验,减少闪烁。
|
||||
* FIXME: 等将来容易判断代码块结束位置时再重构。
|
||||
*/
|
||||
const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
||||
const { mermaid, isLoading, error: mermaidError } = useMermaid()
|
||||
const { mermaid, isLoading: isLoadingMermaid, error: mermaidError } = useMermaid()
|
||||
const mermaidRef = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
||||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isRendering, setIsRendering] = useState(false)
|
||||
|
||||
// 使用通用图像工具
|
||||
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(mermaidRef, {
|
||||
@@ -32,55 +38,69 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
||||
handleDownload
|
||||
})
|
||||
|
||||
const render = useCallback(async () => {
|
||||
try {
|
||||
if (!children) return
|
||||
// 实际的渲染函数
|
||||
const renderMermaid = useCallback(
|
||||
async (content: string) => {
|
||||
if (!content || !mermaidRef.current) return
|
||||
|
||||
// 验证语法,提前抛出异常
|
||||
await mermaid.parse(children)
|
||||
try {
|
||||
setIsRendering(true)
|
||||
|
||||
if (!mermaidRef.current) return
|
||||
const { svg } = await mermaid.render(diagramId, children, mermaidRef.current)
|
||||
// 验证语法,提前抛出异常
|
||||
await mermaid.parse(content)
|
||||
|
||||
// 避免不可见时产生 undefined 和 NaN
|
||||
const fixedSvg = svg.replace(/translate\(undefined,\s*NaN\)/g, 'translate(0, 0)')
|
||||
mermaidRef.current.innerHTML = fixedSvg
|
||||
const { svg } = await mermaid.render(diagramId, content, mermaidRef.current)
|
||||
|
||||
// 没有语法错误时清除错误记录和定时器
|
||||
setError(null)
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current)
|
||||
errorTimeoutRef.current = null
|
||||
}
|
||||
} catch (error) {
|
||||
// 延迟显示错误
|
||||
if (errorTimeoutRef.current) clearTimeout(errorTimeoutRef.current)
|
||||
errorTimeoutRef.current = setTimeout(() => {
|
||||
// 避免不可见时产生 undefined 和 NaN
|
||||
const fixedSvg = svg.replace(/translate\(undefined,\s*NaN\)/g, 'translate(0, 0)')
|
||||
mermaidRef.current.innerHTML = fixedSvg
|
||||
|
||||
// 渲染成功,清除错误记录
|
||||
setError(null)
|
||||
} catch (error) {
|
||||
setError((error as Error).message)
|
||||
}, 500)
|
||||
}
|
||||
}, [children, diagramId, mermaid])
|
||||
|
||||
// 渲染Mermaid图表
|
||||
useEffect(() => {
|
||||
if (isLoading) return
|
||||
|
||||
startTransition(render)
|
||||
|
||||
// 清理定时器
|
||||
return () => {
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current)
|
||||
errorTimeoutRef.current = null
|
||||
} finally {
|
||||
setIsRendering(false)
|
||||
}
|
||||
},
|
||||
[diagramId, mermaid]
|
||||
)
|
||||
|
||||
// debounce 渲染
|
||||
const debouncedRender = useMemo(
|
||||
() =>
|
||||
debounce((content: string) => {
|
||||
startTransition(() => renderMermaid(content))
|
||||
}, 300),
|
||||
[renderMermaid]
|
||||
)
|
||||
|
||||
// 触发渲染
|
||||
useEffect(() => {
|
||||
if (isLoadingMermaid) return
|
||||
|
||||
if (children) {
|
||||
setIsRendering(true)
|
||||
debouncedRender(children)
|
||||
} else {
|
||||
debouncedRender.cancel()
|
||||
setIsRendering(false)
|
||||
}
|
||||
}, [isLoading, render])
|
||||
|
||||
return () => {
|
||||
debouncedRender.cancel()
|
||||
}
|
||||
}, [children, isLoadingMermaid, debouncedRender])
|
||||
|
||||
const isLoading = isLoadingMermaid || isRendering
|
||||
|
||||
return (
|
||||
<Flex vertical>
|
||||
{(mermaidError || error) && <StyledError>{mermaidError || error}</StyledError>}
|
||||
<StyledMermaid ref={mermaidRef} className="mermaid" />
|
||||
</Flex>
|
||||
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
|
||||
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
|
||||
{(mermaidError || error) && <StyledError>{mermaidError || error}</StyledError>}
|
||||
<StyledMermaid ref={mermaidRef} className="mermaid" />
|
||||
</Flex>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -249,8 +249,8 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
||||
}
|
||||
|
||||
const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
|
||||
/* FIXME: 在 bubble style 中撑开一些宽度*/
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.code-toolbar {
|
||||
background-color: ${(props) => (props.$isInSpecialView ? 'transparent' : 'var(--color-background-mute)')};
|
||||
@@ -285,13 +285,10 @@ const CodeHeader = styled.div<{ $isInSpecialView: boolean }>`
|
||||
|
||||
const SplitViewWrapper = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
> * {
|
||||
flex: 1 1 0;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -224,11 +224,10 @@ const CodeEditor = ({
|
||||
...customBasicSetup // override basicSetup
|
||||
}}
|
||||
style={{
|
||||
...style,
|
||||
fontSize: `${fontSize - 1}px`,
|
||||
border: '0.5px solid transparent',
|
||||
borderRadius: '5px',
|
||||
marginTop: 0,
|
||||
...style
|
||||
marginTop: 0
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Dropdown } from 'antd'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -12,7 +11,6 @@ interface ContextMenuProps {
|
||||
const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) => {
|
||||
const { t } = useTranslation()
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
|
||||
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
|
||||
const [selectedText, setSelectedText] = useState<string>('')
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
@@ -20,12 +18,6 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
e.preventDefault()
|
||||
const _selectedText = window.getSelection()?.toString()
|
||||
if (_selectedText) {
|
||||
const quotedText =
|
||||
_selectedText
|
||||
.split('\n')
|
||||
.map((line) => `> ${line}`)
|
||||
.join('\n') + '\n-------------'
|
||||
setSelectedQuoteText(quotedText)
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY })
|
||||
setSelectedText(_selectedText)
|
||||
}
|
||||
@@ -45,7 +37,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
}, [])
|
||||
|
||||
// 获取右键菜单项
|
||||
const getContextMenuItems = (t: (key: string) => string, selectedQuoteText: string, selectedText: string) => [
|
||||
const getContextMenuItems = (t: (key: string) => string, selectedText: string) => [
|
||||
{
|
||||
key: 'copy',
|
||||
label: t('common.copy'),
|
||||
@@ -66,8 +58,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
key: 'quote',
|
||||
label: t('chat.message.quote'),
|
||||
onClick: () => {
|
||||
if (selectedQuoteText) {
|
||||
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
|
||||
if (selectedText) {
|
||||
window.api?.quoteToMainWindow(selectedText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +70,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
{contextMenuPosition && (
|
||||
<Dropdown
|
||||
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
|
||||
menu={{ items: getContextMenuItems(t, selectedQuoteText, selectedText) }}
|
||||
menu={{ items: getContextMenuItems(t, selectedText) }}
|
||||
open={true}
|
||||
trigger={['contextMenu']}>
|
||||
<div />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getLeadingEmoji } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -8,12 +7,10 @@ interface EmojiIconProps {
|
||||
}
|
||||
|
||||
const EmojiIcon: FC<EmojiIconProps> = ({ emoji, className }) => {
|
||||
const _emoji = getLeadingEmoji(emoji || '⭐️') || '⭐️'
|
||||
|
||||
return (
|
||||
<Container className={className}>
|
||||
<EmojiBackground>{_emoji}</EmojiBackground>
|
||||
{_emoji}
|
||||
<EmojiBackground>{emoji || '⭐️'}</EmojiBackground>
|
||||
{emoji}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
|
||||
if (ref.current) {
|
||||
ref.current.addEventListener('emoji-click', (event: any) => {
|
||||
event.stopPropagation()
|
||||
onEmojiClick(event.detail.emoji.unicode)
|
||||
onEmojiClick(event.detail.unicode || event.detail.emoji.unicode)
|
||||
})
|
||||
}
|
||||
}, [onEmojiClick])
|
||||
|
||||
141
src/renderer/src/components/ImageViewer.tsx
Normal file
141
src/renderer/src/components/ImageViewer.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
FileImageOutlined,
|
||||
RotateLeftOutlined,
|
||||
RotateRightOutlined,
|
||||
SwapOutlined,
|
||||
UndoOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { download } from '@renderer/utils/download'
|
||||
import { Dropdown, Image as AntImage, ImageProps as AntImageProps, Space } from 'antd'
|
||||
import { Base64 } from 'js-base64'
|
||||
import mime from 'mime'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ImageViewerProps extends AntImageProps {
|
||||
src: string
|
||||
}
|
||||
|
||||
const ImageViewer: React.FC<ImageViewerProps> = ({ src, style, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// 复制图片到剪贴板
|
||||
const handleCopyImage = async (src: string) => {
|
||||
try {
|
||||
if (src.startsWith('data:')) {
|
||||
// 处理 base64 格式的图片
|
||||
const match = src.match(/^data:(image\/\w+);base64,(.+)$/)
|
||||
if (!match) throw new Error('无效的 base64 图片格式')
|
||||
const mimeType = match[1]
|
||||
const byteArray = Base64.toUint8Array(match[2])
|
||||
const blob = new Blob([byteArray], { type: mimeType })
|
||||
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
|
||||
} else if (src.startsWith('file://')) {
|
||||
// 处理本地文件路径
|
||||
const bytes = await window.api.fs.read(src)
|
||||
const mimeType = mime.getType(src) || 'application/octet-stream'
|
||||
const blob = new Blob([bytes], { type: mimeType })
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[mimeType]: blob
|
||||
})
|
||||
])
|
||||
} else {
|
||||
// 处理 URL 格式的图片
|
||||
const response = await fetch(src)
|
||||
const blob = await response.blob()
|
||||
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
window.message.success(t('message.copy.success'))
|
||||
} catch (error) {
|
||||
console.error('复制图片失败:', error)
|
||||
window.message.error(t('message.copy.failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const getContextMenuItems = (src: string) => {
|
||||
return [
|
||||
{
|
||||
key: 'copy-url',
|
||||
label: t('common.copy'),
|
||||
icon: <CopyOutlined />,
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(src)
|
||||
window.message.success(t('message.copy.success'))
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'download',
|
||||
label: t('common.download'),
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: () => download(src)
|
||||
},
|
||||
{
|
||||
key: 'copy-image',
|
||||
label: t('code_block.preview.copy.image'),
|
||||
icon: <FileImageOutlined />,
|
||||
onClick: () => handleCopyImage(src)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: getContextMenuItems(src) }} trigger={['contextMenu']}>
|
||||
<AntImage
|
||||
src={src}
|
||||
style={style}
|
||||
{...props}
|
||||
preview={{
|
||||
mask: typeof props.preview === 'object' ? props.preview.mask : false,
|
||||
toolbarRender: (
|
||||
_,
|
||||
{
|
||||
transform: { scale },
|
||||
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
|
||||
}
|
||||
) => (
|
||||
<ToolbarWrapper size={12} className="toolbar-wrapper">
|
||||
<SwapOutlined rotate={90} onClick={onFlipY} />
|
||||
<SwapOutlined onClick={onFlipX} />
|
||||
<RotateLeftOutlined onClick={onRotateLeft} />
|
||||
<RotateRightOutlined onClick={onRotateRight} />
|
||||
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
|
||||
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
|
||||
<UndoOutlined onClick={onReset} />
|
||||
<CopyOutlined onClick={() => handleCopyImage(src)} />
|
||||
<DownloadOutlined onClick={() => download(src)} />
|
||||
</ToolbarWrapper>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarWrapper = styled(Space)`
|
||||
padding: 0px 24px;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 100px;
|
||||
.anticon {
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.anticon:hover {
|
||||
opacity: 0.3;
|
||||
}
|
||||
`
|
||||
|
||||
export default ImageViewer
|
||||
@@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => {
|
||||
height={'100%'}
|
||||
maskClosable={false}
|
||||
closeIcon={null}
|
||||
style={{
|
||||
marginLeft: 'var(--sidebar-width)',
|
||||
backgroundColor: window.root.style.background
|
||||
}}>
|
||||
style={{ backgroundColor: window.root.style.background }}>
|
||||
{!isReady && (
|
||||
<EmptyView>
|
||||
<Avatar
|
||||
@@ -418,7 +415,7 @@ const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${isMac ? '20px' : '10px'};
|
||||
padding-left: ${isMac ? '80px' : '10px'};
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -78,7 +78,7 @@ const WebviewContainer = memo(
|
||||
)
|
||||
|
||||
const WebviewStyle: React.CSSProperties = {
|
||||
width: 'calc(100vw - var(--sidebar-width))',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - var(--navbar-height))',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
display: 'inline-flex'
|
||||
|
||||
@@ -1,26 +1,42 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Spin, TreeSelect } from 'antd'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import {
|
||||
exportMarkdownToObsidian,
|
||||
messagesToMarkdown,
|
||||
messageToMarkdown,
|
||||
messageToMarkdownWithReasoning,
|
||||
topicToMarkdown
|
||||
} from '@renderer/utils/export'
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Spin, Switch, TreeSelect } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface ObsidianExportDialogProps {
|
||||
title: string
|
||||
markdown: string
|
||||
open: boolean
|
||||
onClose: (success: boolean) => void
|
||||
obsidianTags: string | null
|
||||
processingMethod: string | '3' //默认新增(存在就覆盖)
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string
|
||||
type: 'folder' | 'markdown'
|
||||
name: string
|
||||
}
|
||||
|
||||
const ObsidianProcessingMethod = {
|
||||
APPEND: '1',
|
||||
PREPEND: '2',
|
||||
NEW_OR_OVERWRITE: '3'
|
||||
} as const
|
||||
|
||||
interface PopupContainerProps {
|
||||
title: string
|
||||
obsidianTags: string | null
|
||||
processingMethod: (typeof ObsidianProcessingMethod)[keyof typeof ObsidianProcessingMethod]
|
||||
open: boolean
|
||||
resolve: (success: boolean) => void
|
||||
message?: Message
|
||||
messages?: Message[]
|
||||
topic?: Topic
|
||||
}
|
||||
|
||||
// 转换文件信息数组为树形结构
|
||||
const convertToTreeData = (files: FileInfo[]) => {
|
||||
const treeData: any[] = [
|
||||
@@ -113,13 +129,15 @@ const convertToTreeData = (files: FileInfo[]) => {
|
||||
return treeData
|
||||
}
|
||||
|
||||
const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
const PopupContainer: React.FC<PopupContainerProps> = ({
|
||||
title,
|
||||
markdown,
|
||||
open,
|
||||
onClose,
|
||||
obsidianTags,
|
||||
processingMethod
|
||||
processingMethod,
|
||||
open,
|
||||
resolve,
|
||||
message,
|
||||
messages,
|
||||
topic
|
||||
}) => {
|
||||
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
|
||||
const [state, setState] = useState({
|
||||
@@ -130,8 +148,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
processingMethod: processingMethod,
|
||||
folder: ''
|
||||
})
|
||||
|
||||
// 是否手动编辑过标题
|
||||
const [hasTitleBeenManuallyEdited, setHasTitleBeenManuallyEdited] = useState(false)
|
||||
const [vaults, setVaults] = useState<Array<{ path: string; name: string }>>([])
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
@@ -139,8 +155,8 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
const [selectedVault, setSelectedVault] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [exportReasoning, setExportReasoning] = useState(false)
|
||||
|
||||
// 处理文件数据转为树形结构
|
||||
useEffect(() => {
|
||||
if (files.length > 0) {
|
||||
const treeData = convertToTreeData(files)
|
||||
@@ -157,28 +173,21 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
}
|
||||
}, [files])
|
||||
|
||||
// 组件加载时获取Vault列表
|
||||
useEffect(() => {
|
||||
const fetchVaults = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const vaultsData = await window.obsidian.getVaults()
|
||||
|
||||
if (vaultsData.length === 0) {
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vaults'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setVaults(vaultsData)
|
||||
|
||||
// 如果没有选择的vault,使用默认值或第一个
|
||||
const vaultToUse = defaultObsidianVault || vaultsData[0]?.name
|
||||
if (vaultToUse) {
|
||||
setSelectedVault(vaultToUse)
|
||||
|
||||
// 获取选中vault的文件和文件夹
|
||||
const filesData = await window.obsidian.getFiles(vaultToUse)
|
||||
setFiles(filesData)
|
||||
}
|
||||
@@ -189,11 +198,9 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchVaults()
|
||||
}, [defaultObsidianVault])
|
||||
|
||||
// 当选择的vault变化时,获取其文件和文件夹
|
||||
useEffect(() => {
|
||||
if (selectedVault) {
|
||||
const fetchFiles = async () => {
|
||||
@@ -209,7 +216,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchFiles()
|
||||
}
|
||||
}, [selectedVault])
|
||||
@@ -219,82 +225,71 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vault_selected'))
|
||||
return
|
||||
}
|
||||
|
||||
//构建content 并复制到粘贴板
|
||||
let markdown = ''
|
||||
if (topic) {
|
||||
markdown = await topicToMarkdown(topic, exportReasoning)
|
||||
} else if (messages && messages.length > 0) {
|
||||
markdown = messagesToMarkdown(messages, exportReasoning)
|
||||
} else if (message) {
|
||||
markdown = exportReasoning ? messageToMarkdownWithReasoning(message) : messageToMarkdown(message)
|
||||
} else {
|
||||
markdown = ''
|
||||
}
|
||||
let content = ''
|
||||
if (state.processingMethod !== '3') {
|
||||
if (state.processingMethod !== ObsidianProcessingMethod.NEW_OR_OVERWRITE) {
|
||||
content = `\n---\n${markdown}`
|
||||
} else {
|
||||
content = `---
|
||||
\ntitle: ${state.title}
|
||||
\ncreated: ${state.createdAt}
|
||||
\nsource: ${state.source}
|
||||
\ntags: ${state.tags}
|
||||
\n---\n${markdown}`
|
||||
content = `---\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}`
|
||||
}
|
||||
if (content === '') {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||
return
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(content)
|
||||
|
||||
// 导出到Obsidian
|
||||
exportMarkdownToObsidian({
|
||||
...state,
|
||||
folder: state.folder,
|
||||
vault: selectedVault
|
||||
})
|
||||
|
||||
onClose(true)
|
||||
setOpen(false)
|
||||
resolve(true)
|
||||
}
|
||||
|
||||
const [openState, setOpen] = useState(open)
|
||||
useEffect(() => {
|
||||
setOpen(open)
|
||||
}, [open])
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose(false)
|
||||
setOpen(false)
|
||||
resolve(false)
|
||||
}
|
||||
|
||||
const handleChange = (key: string, value: any) => {
|
||||
setState((prevState) => ({ ...prevState, [key]: value }))
|
||||
}
|
||||
|
||||
// 处理title输入变化
|
||||
const handleTitleInputChange = (newTitle: string) => {
|
||||
handleChange('title', newTitle)
|
||||
setHasTitleBeenManuallyEdited(true)
|
||||
}
|
||||
|
||||
const handleVaultChange = (value: string) => {
|
||||
setSelectedVault(value)
|
||||
// 文件夹会通过useEffect自动获取
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
folder: ''
|
||||
}))
|
||||
setState((prevState) => ({ ...prevState, folder: '' }))
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (value: string) => {
|
||||
// 更新folder值
|
||||
handleChange('folder', value)
|
||||
|
||||
// 检查是否选中md文件
|
||||
if (value) {
|
||||
const selectedFile = files.find((file) => file.path === value)
|
||||
if (selectedFile) {
|
||||
if (selectedFile.type === 'markdown') {
|
||||
// 如果是md文件,自动设置标题为文件名并设置处理方式为1(追加)
|
||||
const fileName = selectedFile.name
|
||||
const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName
|
||||
handleChange('title', titleWithoutExt)
|
||||
// 重置手动编辑标记,因为这是非用户设置的title
|
||||
setHasTitleBeenManuallyEdited(false)
|
||||
handleChange('processingMethod', '1')
|
||||
handleChange('processingMethod', ObsidianProcessingMethod.APPEND)
|
||||
} else {
|
||||
// 如果是文件夹,自动设置标题为话题名并设置处理方式为3(新建)
|
||||
handleChange('processingMethod', '3')
|
||||
// 仅当用户未手动编辑过 title 时,才将其重置为 props.title
|
||||
handleChange('processingMethod', ObsidianProcessingMethod.NEW_OR_OVERWRITE)
|
||||
if (!hasTitleBeenManuallyEdited) {
|
||||
// title 是 props.title
|
||||
handleChange('title', title)
|
||||
}
|
||||
}
|
||||
@@ -305,7 +300,7 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
return (
|
||||
<Modal
|
||||
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||
open={open}
|
||||
open={openState}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
@@ -317,9 +312,9 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
type: 'primary',
|
||||
disabled: vaults.length === 0 || loading || !!error
|
||||
}}
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}>
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}
|
||||
afterClose={() => setOpen(open)}>
|
||||
{error && <Alert message={error} type="error" showIcon style={{ marginBottom: 16 }} />}
|
||||
|
||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
|
||||
<Input
|
||||
@@ -328,7 +323,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_vault')}>
|
||||
{vaults.length > 0 ? (
|
||||
<Select
|
||||
@@ -354,7 +348,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_path')}>
|
||||
<Spin spinning={loading}>
|
||||
{selectedVault ? (
|
||||
@@ -376,7 +369,6 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
)}
|
||||
</Spin>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
|
||||
<Input
|
||||
value={state.tags}
|
||||
@@ -398,21 +390,29 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}>
|
||||
<Select
|
||||
value={state.processingMethod}
|
||||
onChange={(value) => handleChange('processingMethod', value)}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_operate_placeholder')}
|
||||
allowClear>
|
||||
<Option value="1">{i18n.t('chat.topics.export.obsidian_operate_append')}</Option>
|
||||
<Option value="2">{i18n.t('chat.topics.export.obsidian_operate_prepend')}</Option>
|
||||
<Option value="3">{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}</Option>
|
||||
<Option value={ObsidianProcessingMethod.APPEND}>
|
||||
{i18n.t('chat.topics.export.obsidian_operate_append')}
|
||||
</Option>
|
||||
<Option value={ObsidianProcessingMethod.PREPEND}>
|
||||
{i18n.t('chat.topics.export.obsidian_operate_prepend')}
|
||||
</Option>
|
||||
<Option value={ObsidianProcessingMethod.NEW_OR_OVERWRITE}>
|
||||
{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
|
||||
<Switch checked={exportReasoning} onChange={setExportReasoning} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ObsidianExportDialog
|
||||
export { ObsidianProcessingMethod, PopupContainer }
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ObsidianProcessingMethod, PopupContainer } from '@renderer/components/ObsidianExportDialog'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
|
||||
interface ObsidianExportOptions {
|
||||
title: string
|
||||
markdown: string
|
||||
processingMethod: string | '3' // 默认新增(存在就覆盖)
|
||||
processingMethod: (typeof ObsidianProcessingMethod)[keyof typeof ObsidianProcessingMethod]
|
||||
topic?: Topic
|
||||
message?: Message
|
||||
messages?: Message[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置Obsidian 笔记属性弹窗
|
||||
* @param options.title 标题
|
||||
* @param options.markdown markdown内容
|
||||
* @param options.processingMethod 处理方式
|
||||
* @returns
|
||||
*/
|
||||
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
const root = createRoot(div)
|
||||
|
||||
const handleClose = (success: boolean) => {
|
||||
root.unmount()
|
||||
document.body.removeChild(div)
|
||||
resolve(success)
|
||||
}
|
||||
// 不再从store中获取tag配置
|
||||
root.render(
|
||||
<ObsidianExportDialog
|
||||
title={options.title}
|
||||
markdown={options.markdown}
|
||||
obsidianTags=""
|
||||
processingMethod={options.processingMethod}
|
||||
open={true}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
show: showObsidianExportDialog
|
||||
export default class ObsidianExportPopup {
|
||||
static hide() {
|
||||
TopView.hide('ObsidianExportPopup')
|
||||
}
|
||||
static show(options: ObsidianExportOptions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
title={options.title}
|
||||
processingMethod={options.processingMethod}
|
||||
topic={options.topic}
|
||||
message={options.message}
|
||||
messages={options.messages}
|
||||
obsidianTags={''}
|
||||
open={true}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
ObsidianExportPopup.hide()
|
||||
}}
|
||||
/>,
|
||||
'ObsidianExportPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const FullScreenContainer: React.FC<PropsWithChildren> = useCallback(({ children }) => {
|
||||
return (
|
||||
<Box flex={1} position="absolute" w="100%" h="100%">
|
||||
<Box flex={1} position="absolute" w="100%" h="100%" className="topview-fullscreen-container">
|
||||
<Box position="absolute" w="100%" h="100%" onClick={onPop} />
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
@@ -14,9 +14,9 @@ interface BackupFile {
|
||||
|
||||
interface WebdavConfig {
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
webdavPass: string
|
||||
webdavPath: string
|
||||
webdavUser?: string
|
||||
webdavPass?: string
|
||||
webdavPath?: string
|
||||
}
|
||||
|
||||
interface WebdavBackupManagerProps {
|
||||
@@ -47,7 +47,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = webdavConfig
|
||||
|
||||
const fetchBackupFiles = useCallback(async () => {
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
if (!webdavHost) {
|
||||
message.error(t('message.error.invalid.webdav'))
|
||||
return
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
return
|
||||
}
|
||||
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
if (!webdavHost) {
|
||||
message.error(t('message.error.invalid.webdav'))
|
||||
return
|
||||
}
|
||||
@@ -132,7 +132,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
}
|
||||
|
||||
const handleDeleteSingle = async (fileName: string) => {
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
if (!webdavHost) {
|
||||
message.error(t('message.error.invalid.webdav'))
|
||||
return
|
||||
}
|
||||
@@ -165,7 +165,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
|
||||
}
|
||||
|
||||
const handleRestore = async (fileName: string) => {
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
if (!webdavHost) {
|
||||
message.error(t('message.error.invalid.webdav'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export function useWebdavRestoreModal({
|
||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([])
|
||||
|
||||
const showRestoreModal = useCallback(async () => {
|
||||
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
if (!webdavHost) {
|
||||
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
|
||||
return
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export function useWebdavRestoreModal({
|
||||
}, [webdavHost, webdavUser, webdavPass, webdavPath, t])
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
if (!selectedFile || !webdavHost || !webdavUser || !webdavPass || !webdavPath) {
|
||||
if (!selectedFile || !webdavHost) {
|
||||
window.message.error({
|
||||
content: !selectedFile ? t('message.error.no.file.selected') : t('message.error.invalid.webdav'),
|
||||
key: 'restore-error'
|
||||
@@ -170,7 +170,7 @@ export function useWebdavRestoreModal({
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [selectedFile, webdavHost, webdavUser, webdavPass, webdavPath, t, restoreMethod])
|
||||
}, [selectedFile, webdavHost, t, restoreMethod])
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsRestoreModalVisible(false)
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Scrollbar > props handling > should handle right prop correctly 1`] = `
|
||||
.c0 {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.c0::-webkit-scrollbar-thumb {
|
||||
transition: background 2s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.c0::-webkit-scrollbar-thumb:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-testid="scrollbar"
|
||||
>
|
||||
内容
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Scrollbar > rendering > should match default styled snapshot 1`] = `
|
||||
.c0 {
|
||||
overflow-y: auto;
|
||||
|
||||
90
src/renderer/src/components/app/MainNavbar.tsx
Normal file
90
src/renderer/src/components/app/MainNavbar.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { MessageSquareDiff, Search } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SearchPopup from '../Popups/SearchPopup'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const HeaderNavbar: FC<Props> = () => {
|
||||
return (
|
||||
<Container>
|
||||
<div>
|
||||
{!isMac && (
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
|
||||
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
|
||||
<MessageSquareDiff size={18} />
|
||||
</NavbarIcon>
|
||||
</Tooltip>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: var(--assistant-width);
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
padding-left: var(--sidebar-width);
|
||||
height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
background-color: transparent;
|
||||
-webkit-app-region: drag;
|
||||
padding-left: ${isMac ? '75px' : '0'};
|
||||
`
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
-webkit-app-region: no-drag;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-appstore {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
379
src/renderer/src/components/app/MainSidebar.tsx
Normal file
379
src/renderer/src/components/app/MainSidebar.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
|
||||
import { UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useChat } from '@renderer/hooks/useChat'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { isEmoji } from '@renderer/utils'
|
||||
import { Avatar, Tooltip } from 'antd'
|
||||
import {
|
||||
Blocks,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Compass,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Languages,
|
||||
MessageSquare,
|
||||
Moon,
|
||||
Palette,
|
||||
SquareTerminal,
|
||||
Sun,
|
||||
SunMoon
|
||||
} from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Tabs from '../../pages/home/Tabs'
|
||||
import MainNavbar from './MainNavbar'
|
||||
|
||||
type Tab = 'assistants' | 'topic' | 'settings'
|
||||
|
||||
const MainSidebar: FC = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const navigate = useNavigate()
|
||||
const [tab, setTab] = useState<Tab>('assistants')
|
||||
const avatar = useAvatar()
|
||||
const { userName, defaultPaintingProvider } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const { theme, settedTheme, toggleTheme } = useTheme()
|
||||
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
|
||||
|
||||
const location = useLocation()
|
||||
const { pathname } = location
|
||||
|
||||
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
|
||||
useEffect(() => {
|
||||
NavigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, () => setTab('topic'))
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
|
||||
const newAssistant = assistants.find((a) => a.id === assistantId)
|
||||
if (newAssistant) {
|
||||
setActiveAssistant(newAssistant)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [assistants, setActiveAssistant])
|
||||
|
||||
useEffect(() => {
|
||||
const canMinimize = !showAssistants && !showTopics
|
||||
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
|
||||
|
||||
return () => {
|
||||
window.api.window.resetMinimumSize()
|
||||
}
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
useEffect(() => {
|
||||
setIsAppMenuExpanded(false)
|
||||
}, [activeAssistant.id, activeTopic.id])
|
||||
|
||||
const onAvatarClick = () => {
|
||||
navigate('/settings/provider')
|
||||
}
|
||||
|
||||
const onChageTheme = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
toggleTheme()
|
||||
}
|
||||
|
||||
const appMenuItems = [
|
||||
// { icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
|
||||
{ icon: <Compass size={18} className="icon" />, text: t('discover.title'), path: '/discover' },
|
||||
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
|
||||
{
|
||||
icon: <Palette size={18} className="icon" />,
|
||||
text: t('paintings.title'),
|
||||
path: `/paintings/${defaultPaintingProvider}`
|
||||
},
|
||||
// { icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
|
||||
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
|
||||
{ icon: <SquareTerminal size={18} className="icon" />, text: t('common.mcp'), path: '/mcp-servers' },
|
||||
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
|
||||
]
|
||||
|
||||
const isRoutes = (path: string): boolean => pathname.startsWith(path)
|
||||
|
||||
const onChageTab = (tab: Tab) => {
|
||||
setTab(tab)
|
||||
setIsAppMenuExpanded(false)
|
||||
}
|
||||
|
||||
if (!showAssistants) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (location.pathname !== '/') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container id="main-sidebar">
|
||||
<MainNavbar />
|
||||
<MainMenu>
|
||||
<MainMenuItem
|
||||
active={tab === 'assistants' && location.pathname === '/'}
|
||||
onClick={() => onChageTab('assistants')}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>
|
||||
<Bot size={18} />
|
||||
</MainMenuItemIcon>
|
||||
<MainMenuItemText>{t('assistants.title')}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
{tab === 'topic' && (
|
||||
<MainMenuItemRight>
|
||||
<MainMenuItemRightText>{activeAssistant.name}</MainMenuItemRightText>
|
||||
</MainMenuItemRight>
|
||||
)}
|
||||
</MainMenuItem>
|
||||
<MainMenuItem active={tab === 'topic' && location.pathname === '/'} onClick={() => onChageTab('topic')}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>
|
||||
<MessageSquare size={18} />
|
||||
</MainMenuItemIcon>
|
||||
<MainMenuItemText>{t('common.topics')}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
</MainMenuItem>
|
||||
<MainMenuItem
|
||||
style={{ opacity: isAppMenuExpanded ? 0.5 : 1 }}
|
||||
onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>
|
||||
<Blocks size={19} className="icon" />
|
||||
</MainMenuItemIcon>
|
||||
<MainMenuItemText>{t('common.apps')}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
<MainMenuItemRight>
|
||||
{isAppMenuExpanded ? (
|
||||
<ChevronDown size={18} color="var(--color-text-3)" />
|
||||
) : (
|
||||
<ChevronRight size={18} color="var(--color-text-3)" />
|
||||
)}
|
||||
</MainMenuItemRight>
|
||||
</MainMenuItem>
|
||||
{isAppMenuExpanded && (
|
||||
<SubMenu>
|
||||
{appMenuItems.map((item) => (
|
||||
<MainMenuItem key={item.path} active={isRoutes(item.path)} onClick={() => navigate(item.path)}>
|
||||
<MainMenuItemLeft>
|
||||
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
|
||||
<MainMenuItemText>{item.text}</MainMenuItemText>
|
||||
</MainMenuItemLeft>
|
||||
</MainMenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
)}
|
||||
</MainMenu>
|
||||
<Tabs
|
||||
tab={tab}
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
setActiveTopic={setActiveTopic}
|
||||
/>
|
||||
<UserMenu onClick={onAvatarClick}>
|
||||
<UserMenuLeft>
|
||||
{isEmoji(avatar) ? (
|
||||
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
|
||||
{avatar}
|
||||
</EmojiAvatar>
|
||||
) : (
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
|
||||
)}
|
||||
<UserMenuText>{userName}</UserMenuText>
|
||||
</UserMenuLeft>
|
||||
<Tooltip
|
||||
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settedTheme}`)}
|
||||
mouseEnterDelay={0.8}
|
||||
placement="right">
|
||||
<Icon theme={theme} onClick={onChageTheme}>
|
||||
{settedTheme === ThemeMode.dark ? (
|
||||
<Moon size={18} className="icon" />
|
||||
) : settedTheme === ThemeMode.light ? (
|
||||
<Sun size={18} className="icon" />
|
||||
) : (
|
||||
<SunMoon size={18} className="icon" />
|
||||
)}
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
</UserMenu>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--assistant-width);
|
||||
max-width: var(--assistant-width);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const MainMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const MainMenuItem = styled.div<{ active?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')};
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
|
||||
}
|
||||
`
|
||||
|
||||
const MainMenuItemLeft = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const MainMenuItemRight = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const MainMenuItemRightText = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const MainMenuItemIcon = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
const MainMenuItemText = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const UserMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 8px;
|
||||
gap: 5px;
|
||||
|
||||
border-radius: 8px;
|
||||
&:hover {
|
||||
background-color: var(--color-list-item);
|
||||
}
|
||||
`
|
||||
|
||||
const UserMenuLeft = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const AvatarImg = styled(Avatar)`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const UserMenuText = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const Icon = styled.div<{ theme: string }>`
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
-webkit-app-region: none;
|
||||
border: 0.5px solid transparent;
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
.icon {
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
|
||||
border: 0.5px solid var(--color-border);
|
||||
.icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes borderBreath {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
&.opened-minapp {
|
||||
position: relative;
|
||||
}
|
||||
&.opened-minapp::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: inherit;
|
||||
opacity: 0.3;
|
||||
border: 0.5px solid var(--color-primary);
|
||||
}
|
||||
`
|
||||
|
||||
const SubMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
export default MainSidebar
|
||||
@@ -1,24 +1,16 @@
|
||||
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronDown, X } from 'lucide-react'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||
const backgroundColor = useNavBackgroundColor()
|
||||
|
||||
return (
|
||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
||||
{children}
|
||||
</NavbarContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
|
||||
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
|
||||
return <NavbarContainer {...props}>{children}</NavbarContainer>
|
||||
}
|
||||
|
||||
export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
|
||||
@@ -34,34 +26,56 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
||||
const isFullscreen = useFullscreen()
|
||||
|
||||
return (
|
||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
||||
<CloseIconWindows />
|
||||
{children}
|
||||
<CloseIconMac />
|
||||
</NavbarMainContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const CloseIconMac = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!isMac) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Button type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag" />
|
||||
}
|
||||
|
||||
const CloseIconWindows = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (isMac) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
type="default"
|
||||
shape="circle"
|
||||
icon={<ChevronDown size={16} />}
|
||||
onClick={() => navigate('/')}
|
||||
className="nodrag"
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const NavbarContainer = styled.div`
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
const NavbarLeftContainer = styled.div`
|
||||
min-width: var(--assistants-width);
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
const NavbarCenterContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 ${isMac ? '20px' : 0};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||
@@ -72,3 +86,32 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
justify-content: space-between;
|
||||
padding-left: ${isMac ? '70px' : '10px'};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
const NavbarCenterContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
padding: 0 8px;
|
||||
font-weight: bold;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
@@ -9,17 +9,19 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { isEmoji } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import {
|
||||
CircleHelp,
|
||||
Compass,
|
||||
FileSearch,
|
||||
Folder,
|
||||
Languages,
|
||||
LayoutGrid,
|
||||
MessageSquareQuote,
|
||||
MessageSquare,
|
||||
Moon,
|
||||
Palette,
|
||||
Settings,
|
||||
@@ -62,10 +64,11 @@ const Sidebar: FC = () => {
|
||||
|
||||
const docsId = 'cherrystudio-docs'
|
||||
const onOpenDocs = () => {
|
||||
const isChinese = i18n.language.startsWith('zh')
|
||||
openMinapp({
|
||||
id: docsId,
|
||||
name: t('docs.title'),
|
||||
url: 'https://docs.cherry-ai.com/',
|
||||
url: isChinese ? 'https://docs.cherry-ai.com/' : 'https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
@@ -147,13 +150,14 @@ const MainMenus: FC = () => {
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
|
||||
|
||||
const iconMap = {
|
||||
assistants: <MessageSquareQuote size={18} className="icon" />,
|
||||
assistants: <MessageSquare size={18} className="icon" />,
|
||||
agents: <Sparkle size={18} className="icon" />,
|
||||
paintings: <Palette size={18} className="icon" />,
|
||||
translate: <Languages size={18} className="icon" />,
|
||||
minapp: <LayoutGrid size={18} className="icon" />,
|
||||
knowledge: <FileSearch size={18} className="icon" />,
|
||||
files: <Folder size={17} className="icon" />
|
||||
files: <Folder size={17} className="icon" />,
|
||||
discover: <Compass size={18} className="icon" />
|
||||
}
|
||||
|
||||
const pathMap = {
|
||||
@@ -163,7 +167,8 @@ const MainMenus: FC = () => {
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files'
|
||||
files: '/files',
|
||||
discover: '/discover'
|
||||
}
|
||||
|
||||
return sidebarIcons.visible.map((icon) => {
|
||||
|
||||
@@ -395,6 +395,37 @@ export function getModelLogo(modelId: string) {
|
||||
}
|
||||
|
||||
export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
defaultModel: [
|
||||
{
|
||||
// 默认助手模型
|
||||
id: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-ai/DeepSeek-V3',
|
||||
provider: 'silicon',
|
||||
group: 'deepseek-ai'
|
||||
},
|
||||
{
|
||||
// 默认话题命名模型
|
||||
id: 'Qwen/Qwen3-8B',
|
||||
name: 'Qwen/Qwen3-8B',
|
||||
provider: 'silicon',
|
||||
group: 'Qwen'
|
||||
},
|
||||
{
|
||||
// 默认翻译模型
|
||||
id: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-ai/DeepSeek-V3',
|
||||
provider: 'silicon',
|
||||
group: 'deepseek-ai'
|
||||
},
|
||||
{
|
||||
// 默认快捷助手模型
|
||||
id: 'deepseek-ai/DeepSeek-V3',
|
||||
name: 'deepseek-ai/DeepSeek-V3',
|
||||
provider: 'silicon',
|
||||
group: 'deepseek-ai'
|
||||
}
|
||||
],
|
||||
|
||||
aihubmix: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
@@ -600,17 +631,17 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'Qwen2.5-7B-Instruct',
|
||||
group: 'Qwen'
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/Llama-3.3-70B-Instruct',
|
||||
name: 'meta-llama/Llama-3.3-70B-Instruct',
|
||||
provider: 'silicon',
|
||||
group: 'meta-llama'
|
||||
},
|
||||
{
|
||||
id: 'BAAI/bge-m3',
|
||||
name: 'BAAI/bge-m3',
|
||||
provider: 'silicon',
|
||||
group: 'BAAI'
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen3-8B',
|
||||
name: 'Qwen/Qwen3-8B',
|
||||
provider: 'silicon',
|
||||
group: 'Qwen'
|
||||
}
|
||||
],
|
||||
ppio: [
|
||||
@@ -1348,15 +1379,39 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
],
|
||||
grok: [
|
||||
{
|
||||
id: 'grok-beta',
|
||||
id: 'grok-3',
|
||||
provider: 'grok',
|
||||
name: 'Grok Beta',
|
||||
name: 'Grok 3',
|
||||
group: 'Grok'
|
||||
},
|
||||
{
|
||||
id: 'grok-vision-beta',
|
||||
id: 'grok-3-fast',
|
||||
provider: 'grok',
|
||||
name: 'Grok Vision Beta',
|
||||
name: 'Grok 3 Fast',
|
||||
group: 'Grok'
|
||||
},
|
||||
{
|
||||
id: 'grok-3-mini',
|
||||
provider: 'grok',
|
||||
name: 'Grok 3 Mini',
|
||||
group: 'Grok'
|
||||
},
|
||||
{
|
||||
id: 'grok-3-mini-fast',
|
||||
provider: 'grok',
|
||||
name: 'Grok 3 Mini Fast',
|
||||
group: 'Grok'
|
||||
},
|
||||
{
|
||||
id: 'grok-2-vision-1212',
|
||||
provider: 'grok',
|
||||
name: 'Grok 2 Vision 1212',
|
||||
group: 'Grok'
|
||||
},
|
||||
{
|
||||
id: 'grok-2-1212',
|
||||
provider: 'grok',
|
||||
name: 'Grok 2 1212',
|
||||
group: 'Grok'
|
||||
}
|
||||
],
|
||||
@@ -1685,24 +1740,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'ERNIE-Speed-128K',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
provider: 'dmxapi',
|
||||
name: 'THUDM/glm-4-9b-chat',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flash',
|
||||
provider: 'dmxapi',
|
||||
name: 'glm-4-flash',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'hunyuan-lite',
|
||||
provider: 'dmxapi',
|
||||
name: 'hunyuan-lite',
|
||||
group: '免费模型'
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'dmxapi',
|
||||
@@ -2315,7 +2352,8 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
||||
}
|
||||
|
||||
return (
|
||||
model.id.toLowerCase().includes('qwen3') ||
|
||||
model.id.toLowerCase().startsWith('qwen3') ||
|
||||
model.id.toLowerCase().startsWith('qwen/qwen3') ||
|
||||
[
|
||||
'qwen-plus-latest',
|
||||
'qwen-plus-0428',
|
||||
@@ -2606,7 +2644,8 @@ export function groupQwenModels(models: Model[]): Record<string, Model[]> {
|
||||
|
||||
export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
|
||||
// Gemini models
|
||||
'gemini-.*$': { min: 0, max: 24576 },
|
||||
'gemini-.*-flash.*$': { min: 0, max: 24576 },
|
||||
'gemini-.*-pro.*$': { min: 128, max: 32768 },
|
||||
|
||||
// Qwen models
|
||||
'qwen-plus-.*$': { min: 0, max: 38912 },
|
||||
|
||||
@@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
|
||||
`
|
||||
|
||||
export const SUMMARIZE_PROMPT =
|
||||
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks or other special symbols"
|
||||
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks, markdown language markers, or other special symbols"
|
||||
|
||||
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
|
||||
export const SEARCH_SUMMARY_PROMPT = `
|
||||
|
||||
@@ -124,8 +124,8 @@ export const PROVIDER_CONFIG = {
|
||||
websites: {
|
||||
official: 'https://o3.fan',
|
||||
apiKey: 'https://o3.fan/token',
|
||||
docs: 'https://docs.o3.fan',
|
||||
models: 'https://docs.o3.fan/models'
|
||||
docs: '',
|
||||
models: 'https://o3.fan/info/models/'
|
||||
}
|
||||
},
|
||||
burncloud: {
|
||||
@@ -169,7 +169,7 @@ export const PROVIDER_CONFIG = {
|
||||
official: 'https://www.siliconflow.cn',
|
||||
apiKey: 'https://cloud.siliconflow.cn/i/d1nTBKXU',
|
||||
docs: 'https://docs.siliconflow.cn/',
|
||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||
models: 'https://cloud.siliconflow.cn/models'
|
||||
}
|
||||
},
|
||||
'gitee-ai': {
|
||||
@@ -394,7 +394,7 @@ export const PROVIDER_CONFIG = {
|
||||
official: 'https://openrouter.ai/',
|
||||
apiKey: 'https://openrouter.ai/settings/keys',
|
||||
docs: 'https://openrouter.ai/docs/quick-start',
|
||||
models: 'https://openrouter.ai/docs/models'
|
||||
models: 'https://openrouter.ai/models'
|
||||
}
|
||||
},
|
||||
groq: {
|
||||
@@ -446,7 +446,7 @@ export const PROVIDER_CONFIG = {
|
||||
websites: {
|
||||
official: 'https://x.ai/',
|
||||
docs: 'https://docs.x.ai/',
|
||||
models: 'https://docs.x.ai/docs#getting-started'
|
||||
models: 'https://docs.x.ai/docs/models'
|
||||
}
|
||||
},
|
||||
hyperbolic: {
|
||||
|
||||
@@ -1,65 +1,127 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
export const TranslateLanguageOptions = [
|
||||
export interface TranslateLanguageOption {
|
||||
value: string
|
||||
langCode?: string
|
||||
label: string
|
||||
emoji: string
|
||||
}
|
||||
|
||||
export const TranslateLanguageOptions: TranslateLanguageOption[] = [
|
||||
{
|
||||
value: 'english',
|
||||
langCode: 'en-us',
|
||||
label: i18n.t('languages.english'),
|
||||
emoji: '🇬🇧'
|
||||
},
|
||||
{
|
||||
value: 'chinese',
|
||||
langCode: 'zh-cn',
|
||||
label: i18n.t('languages.chinese'),
|
||||
emoji: '🇨🇳'
|
||||
},
|
||||
{
|
||||
value: 'chinese-traditional',
|
||||
langCode: 'zh-tw',
|
||||
label: i18n.t('languages.chinese-traditional'),
|
||||
emoji: '🇭🇰'
|
||||
},
|
||||
{
|
||||
value: 'japanese',
|
||||
langCode: 'ja-jp',
|
||||
label: i18n.t('languages.japanese'),
|
||||
emoji: '🇯🇵'
|
||||
},
|
||||
{
|
||||
value: 'korean',
|
||||
langCode: 'ko-kr',
|
||||
label: i18n.t('languages.korean'),
|
||||
emoji: '🇰🇷'
|
||||
},
|
||||
{
|
||||
value: 'russian',
|
||||
label: i18n.t('languages.russian'),
|
||||
emoji: '🇷🇺'
|
||||
},
|
||||
{
|
||||
value: 'spanish',
|
||||
label: i18n.t('languages.spanish'),
|
||||
emoji: '🇪🇸'
|
||||
},
|
||||
|
||||
{
|
||||
value: 'french',
|
||||
langCode: 'fr-fr',
|
||||
label: i18n.t('languages.french'),
|
||||
emoji: '🇫🇷'
|
||||
},
|
||||
{
|
||||
value: 'german',
|
||||
langCode: 'de-de',
|
||||
label: i18n.t('languages.german'),
|
||||
emoji: '🇩🇪'
|
||||
},
|
||||
{
|
||||
value: 'italian',
|
||||
langCode: 'it-it',
|
||||
label: i18n.t('languages.italian'),
|
||||
emoji: '🇮🇹'
|
||||
},
|
||||
{
|
||||
value: 'spanish',
|
||||
langCode: 'es-es',
|
||||
label: i18n.t('languages.spanish'),
|
||||
emoji: '🇪🇸'
|
||||
},
|
||||
{
|
||||
value: 'portuguese',
|
||||
langCode: 'pt-pt',
|
||||
label: i18n.t('languages.portuguese'),
|
||||
emoji: '🇵🇹'
|
||||
},
|
||||
{
|
||||
value: 'russian',
|
||||
langCode: 'ru-ru',
|
||||
label: i18n.t('languages.russian'),
|
||||
emoji: '🇷🇺'
|
||||
},
|
||||
{
|
||||
value: 'polish',
|
||||
langCode: 'pl-pl',
|
||||
label: i18n.t('languages.polish'),
|
||||
emoji: '🇵🇱'
|
||||
},
|
||||
{
|
||||
value: 'arabic',
|
||||
langCode: 'ar-ar',
|
||||
label: i18n.t('languages.arabic'),
|
||||
emoji: '🇸🇦'
|
||||
},
|
||||
{
|
||||
value: 'german',
|
||||
label: i18n.t('languages.german'),
|
||||
emoji: '🇩🇪'
|
||||
value: 'turkish',
|
||||
langCode: 'tr-tr',
|
||||
label: i18n.t('languages.turkish'),
|
||||
emoji: '🇹🇷'
|
||||
},
|
||||
{
|
||||
value: 'thai',
|
||||
langCode: 'th-th',
|
||||
label: i18n.t('languages.thai'),
|
||||
emoji: '🇹🇭'
|
||||
},
|
||||
{
|
||||
value: 'vietnamese',
|
||||
langCode: 'vi-vn',
|
||||
label: i18n.t('languages.vietnamese'),
|
||||
emoji: '🇻🇳'
|
||||
},
|
||||
{
|
||||
value: 'indonesian',
|
||||
langCode: 'id-id',
|
||||
label: i18n.t('languages.indonesian'),
|
||||
emoji: '🇮🇩'
|
||||
},
|
||||
{
|
||||
value: 'urdu',
|
||||
langCode: 'ur-pk',
|
||||
label: i18n.t('languages.urdu'),
|
||||
emoji: '🇵🇰'
|
||||
},
|
||||
{
|
||||
value: 'malay',
|
||||
langCode: 'ms-my',
|
||||
label: i18n.t('languages.malay'),
|
||||
emoji: '🇲🇾'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -45,6 +45,13 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
},
|
||||
Tooltip: {
|
||||
fontSize: 13
|
||||
},
|
||||
ColorPicker: {
|
||||
fontFamily: 'var(--code-font-family)'
|
||||
},
|
||||
Segmented: {
|
||||
itemActiveBg: 'var(--color-background-mute)',
|
||||
itemHoverBg: 'var(--color-background-mute)'
|
||||
}
|
||||
},
|
||||
token: {
|
||||
|
||||
@@ -38,8 +38,22 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||
setSettedTheme(nextTheme || ThemeMode.system)
|
||||
}
|
||||
|
||||
const tailwindThemeChange = (theme) => {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(theme)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.api?.setTheme(settedTheme || actualTheme)
|
||||
}, [settedTheme, actualTheme])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('theme-mode', settedTheme)
|
||||
tailwindThemeChange(settedTheme)
|
||||
}, [settedTheme])
|
||||
|
||||
useEffect(() => {
|
||||
// Set initial theme and OS attributes on body
|
||||
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
|
||||
document.body.setAttribute('theme-mode', actualTheme)
|
||||
|
||||
|
||||
@@ -281,7 +281,6 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
|
||||
modelId: oldMessage.modelId,
|
||||
model: oldMessage.model,
|
||||
type: oldMessage.type === 'clear' ? 'clear' : undefined,
|
||||
isPreset: oldMessage.isPreset,
|
||||
useful: oldMessage.useful,
|
||||
askId: oldMessage.askId,
|
||||
mentions: oldMessage.mentions,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import './assets/styles/index.scss'
|
||||
import '@ant-design/v5-patch-for-react-19'
|
||||
import './assets/styles/tailwind.css'
|
||||
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
|
||||
19
src/renderer/src/hooks/use-mobile.ts
Normal file
19
src/renderer/src/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
47
src/renderer/src/hooks/useChat.tsx
Normal file
47
src/renderer/src/hooks/useChat.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useAssistants } from './useAssistant'
|
||||
import { useSettings } from './useSettings'
|
||||
|
||||
export const useChat = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
|
||||
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
|
||||
const { clickAssistantToShowTopic } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTopic) {
|
||||
dispatch(loadTopicMessagesThunk(activeTopic.id))
|
||||
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
|
||||
}
|
||||
}, [activeTopic, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
const firstTopic = activeAssistant.topics[0]
|
||||
firstTopic && dispatch(setActiveTopic(firstTopic))
|
||||
}, [activeAssistant, dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (clickAssistantToShowTopic) {
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
}
|
||||
}, [clickAssistantToShowTopic, activeAssistant])
|
||||
|
||||
return {
|
||||
activeAssistant,
|
||||
activeTopic,
|
||||
setActiveAssistant: (assistant: Assistant) => {
|
||||
dispatch(setActiveAssistant(assistant))
|
||||
},
|
||||
setActiveTopic: (topic: Topic) => {
|
||||
dispatch(setActiveTopic(topic))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,11 +332,11 @@ export function useMessageOperations(topic: Topic) {
|
||||
}
|
||||
|
||||
// 6. Log operations for debugging
|
||||
console.log('[editMessageBlocks] Operations:', {
|
||||
blocksToRemove: blockIdsToRemove.length,
|
||||
blocksToUpdate: blocksToUpdate.length,
|
||||
blocksToAdd: blocksToAdd.length
|
||||
})
|
||||
// console.log('[editMessageBlocks] Operations:', {
|
||||
// blocksToRemove: blockIdsToRemove.length,
|
||||
// blocksToUpdate: blocksToUpdate.length,
|
||||
// blocksToAdd: blocksToAdd.length
|
||||
// })
|
||||
|
||||
// 7. Update Redux state and database
|
||||
// First update message and add/update blocks
|
||||
|
||||
@@ -10,16 +10,19 @@ export function usePaintings() {
|
||||
const edit = useAppSelector((state) => state.paintings.edit)
|
||||
const upscale = useAppSelector((state) => state.paintings.upscale)
|
||||
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
|
||||
const tokenFluxPaintings = useAppSelector((state) => state.paintings.tokenFluxPaintings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
paintings,
|
||||
DMXAPIPaintings,
|
||||
tokenFluxPaintings,
|
||||
persistentData: {
|
||||
generate,
|
||||
remix,
|
||||
edit,
|
||||
upscale
|
||||
upscale,
|
||||
tokenFluxPaintings
|
||||
},
|
||||
addPainting: (namespace: keyof PaintingsState, painting: PaintingAction) => {
|
||||
dispatch(addPainting({ namespace, painting }))
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
SendMessageShortcut,
|
||||
setAssistantIconType,
|
||||
setAutoCheckUpdate as _setAutoCheckUpdate,
|
||||
setEarlyAccess as _setEarlyAccess,
|
||||
setLaunchOnBoot,
|
||||
setLaunchToTray,
|
||||
setPinTopicsToTop,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setShowTokens,
|
||||
setSidebarIcons,
|
||||
setTargetLanguage,
|
||||
setTheme,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
setWindowStyle
|
||||
} from '@renderer/store/settings'
|
||||
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||
import { FeedUrl } from '@shared/config/constant'
|
||||
|
||||
export function useSettings() {
|
||||
const settings = useAppSelector((state) => state.settings)
|
||||
@@ -57,6 +60,11 @@ export function useSettings() {
|
||||
window.api.setAutoUpdate(isAutoUpdate)
|
||||
},
|
||||
|
||||
setEarlyAccess(isEarlyAccess: boolean) {
|
||||
dispatch(_setEarlyAccess(isEarlyAccess))
|
||||
window.api.setFeedUrl(isEarlyAccess ? FeedUrl.EARLY_ACCESS : FeedUrl.PRODUCTION)
|
||||
},
|
||||
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
},
|
||||
@@ -83,6 +91,9 @@ export function useSettings() {
|
||||
},
|
||||
setAssistantIconType(assistantIconType: AssistantIconType) {
|
||||
dispatch(setAssistantIconType(assistantIconType))
|
||||
},
|
||||
setShowTokens(showTokens: boolean) {
|
||||
dispatch(setShowTokens(showTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setTagsOrder, updateAssistants } from '@renderer/store/assistants'
|
||||
import { flatMap, groupBy, uniq } from 'lodash'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -11,17 +13,48 @@ import { useAssistants } from './useAssistant'
|
||||
export const useTags = () => {
|
||||
const { assistants } = useAssistants()
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || [])
|
||||
|
||||
// 计算所有标签
|
||||
const allTags = useMemo(() => {
|
||||
return uniq(flatMap(assistants, (assistant) => assistant.tags || []))
|
||||
}, [assistants])
|
||||
const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || []))
|
||||
if (savedTagsOrder.length > 0) {
|
||||
return [
|
||||
...savedTagsOrder.filter((tag) => tags.includes(tag)),
|
||||
...tags.filter((tag) => !savedTagsOrder.includes(tag))
|
||||
]
|
||||
}
|
||||
return tags
|
||||
}, [assistants, savedTagsOrder])
|
||||
|
||||
const getAssistantsByTag = useCallback(
|
||||
(tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)),
|
||||
[assistants]
|
||||
)
|
||||
|
||||
const updateTagsOrder = useCallback(
|
||||
(newOrder: string[]) => {
|
||||
dispatch(setTagsOrder(newOrder))
|
||||
updateAssistants(
|
||||
assistants.map((assistant) => {
|
||||
if (!assistant.tags || assistant.tags.length === 0) {
|
||||
return assistant
|
||||
}
|
||||
const newTags = [...assistant.tags]
|
||||
newTags.sort((a, b) => {
|
||||
return newOrder.indexOf(a) - newOrder.indexOf(b)
|
||||
})
|
||||
return {
|
||||
...assistant,
|
||||
tags: newTags
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
[assistants, dispatch]
|
||||
)
|
||||
|
||||
const getGroupedAssistants = useMemo(() => {
|
||||
// 按标签分组,处理多标签的情况
|
||||
const assistantsByTags = flatMap(assistants, (assistant) => {
|
||||
@@ -42,12 +75,30 @@ export const useTags = () => {
|
||||
grouped.unshift(untagged)
|
||||
}
|
||||
|
||||
// 根据savedTagsOrder对标签组进行排序
|
||||
if (savedTagsOrder.length > 0) {
|
||||
const untagged = grouped.length > 0 && grouped[0].tag === t('assistants.tags.untagged') ? grouped.shift() : null
|
||||
grouped.sort((a, b) => {
|
||||
const indexA = savedTagsOrder.indexOf(a.tag)
|
||||
const indexB = savedTagsOrder.indexOf(b.tag)
|
||||
if (indexA === -1 && indexB === -1) return 0
|
||||
if (indexA === -1) return 1
|
||||
if (indexB === -1) return -1
|
||||
|
||||
return indexA - indexB
|
||||
})
|
||||
if (untagged) {
|
||||
grouped.unshift(untagged)
|
||||
}
|
||||
}
|
||||
|
||||
return grouped
|
||||
}, [assistants, t])
|
||||
}, [assistants, t, savedTagsOrder])
|
||||
|
||||
return {
|
||||
allTags,
|
||||
getAssistantsByTag,
|
||||
getGroupedAssistants
|
||||
getGroupedAssistants,
|
||||
updateTagsOrder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,16 @@
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
||||
import store from '@renderer/store'
|
||||
import { updateTopic } from '@renderer/store/assistants'
|
||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
||||
import { find, isEmpty } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { useAssistant } from './useAssistant'
|
||||
import { getStoreSetting } from './useSettings'
|
||||
|
||||
const renamingTopics = new Set<string>()
|
||||
|
||||
let _activeTopic: Topic
|
||||
let _setActiveTopic: (topic: Topic) => void
|
||||
|
||||
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
|
||||
const { assistant } = useAssistant(_assistant.id)
|
||||
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
|
||||
|
||||
_activeTopic = activeTopic
|
||||
_setActiveTopic = setActiveTopic
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTopic) {
|
||||
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
|
||||
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
|
||||
}
|
||||
}, [activeTopic])
|
||||
|
||||
useEffect(() => {
|
||||
// activeTopic not in assistant.topics
|
||||
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
}, [activeTopic?.id, assistant])
|
||||
|
||||
return { activeTopic, setActiveTopic }
|
||||
}
|
||||
|
||||
export function useTopic(assistant: Assistant, topicId?: string) {
|
||||
return assistant?.topics.find((topic) => topic.id === topicId)
|
||||
}
|
||||
@@ -86,7 +55,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
||||
.substring(0, 50)
|
||||
if (topicName) {
|
||||
const data = { ...topic, name: topicName } as Topic
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
}
|
||||
return
|
||||
@@ -97,8 +65,9 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
||||
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
||||
if (summaryText) {
|
||||
const data = { ...topic, name: summaryText }
|
||||
_setActiveTopic(data)
|
||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
||||
} else {
|
||||
window.message?.error(i18n.t('message.error.fetchTopicName'))
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"add.name.placeholder": "Enter name",
|
||||
"add.prompt": "Prompt",
|
||||
"add.prompt.placeholder": "Enter prompt",
|
||||
"add.prompt.variables.tip": "Available variables: {{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "Available variables",
|
||||
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name"
|
||||
},
|
||||
"add.title": "Create Agent",
|
||||
"import": {
|
||||
"title": "Import from External",
|
||||
@@ -30,16 +33,7 @@
|
||||
"agent": "Export Agent"
|
||||
},
|
||||
"delete.popup.content": "Are you sure you want to delete this agent?",
|
||||
"edit.message.add.title": "Add",
|
||||
"edit.message.assistant.placeholder": "Enter assistant message",
|
||||
"edit.message.assistant.title": "Assistant",
|
||||
"edit.message.empty.content": "Conversation input content cannot be empty",
|
||||
"edit.message.group.title": "Message Group",
|
||||
"edit.message.title": "Preset messages",
|
||||
"edit.message.user.placeholder": "Enter user message",
|
||||
"edit.message.user.title": "User",
|
||||
"edit.model.select.title": "Select Model",
|
||||
"edit.settings.hide_preset_messages": "Hide Preset Message",
|
||||
"edit.title": "Edit Agent",
|
||||
"manage.title": "Manage Agents",
|
||||
"my_agents": "My Agents",
|
||||
@@ -76,7 +70,6 @@
|
||||
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
|
||||
"settings.mcp.description": "Default enabled MCP servers",
|
||||
"settings.model": "Model Settings",
|
||||
"settings.preset_messages": "Preset Messages",
|
||||
"settings.prompt": "Prompt Settings",
|
||||
"settings.reasoning_effort": "Reasoning effort",
|
||||
"settings.reasoning_effort.off": "Off",
|
||||
@@ -322,6 +315,7 @@
|
||||
"translate": "Translate",
|
||||
"topics.export.siyuan": "Export to Siyuan Note",
|
||||
"topics.export.wait_for_title_naming": "Generating title...",
|
||||
"topics.export.obsidian_reasoning": "Include Reasoning Chain",
|
||||
"topics.export.title_naming_success": "Title generated successfully",
|
||||
"topics.export.title_naming_failed": "Failed to generate title, using default title",
|
||||
"input.translating": "Translating...",
|
||||
@@ -427,7 +421,9 @@
|
||||
"pinyin.asc": "Sort by Pinyin (A-Z)",
|
||||
"pinyin.desc": "Sort by Pinyin (Z-A)"
|
||||
},
|
||||
"no_results": "No results"
|
||||
"no_results": "No results",
|
||||
"apps": "Apps",
|
||||
"mcp": "Tools"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Docs"
|
||||
@@ -586,7 +582,14 @@
|
||||
"korean": "Korean",
|
||||
"portuguese": "Portuguese",
|
||||
"russian": "Russian",
|
||||
"spanish": "Spanish"
|
||||
"spanish": "Spanish",
|
||||
"polish": "Polish",
|
||||
"turkish": "Turkish",
|
||||
"thai": "Thai",
|
||||
"vietnamese": "Vietnamese",
|
||||
"indonesian": "Indonesian",
|
||||
"urdu": "Urdu",
|
||||
"malay": "Malay"
|
||||
},
|
||||
"lmstudio": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
@@ -627,6 +630,7 @@
|
||||
"error.enter.api.key": "Please enter your API key first",
|
||||
"error.enter.model": "Please select a model first",
|
||||
"error.enter.name": "Please enter the name of the knowledge base",
|
||||
"error.fetchTopicName": "Failed to name the topic",
|
||||
"error.get_embedding_dimensions": "Failed to get embedding dimensions",
|
||||
"error.invalid.api.host": "Invalid API Host",
|
||||
"error.invalid.api.key": "Invalid API Key",
|
||||
@@ -941,10 +945,22 @@
|
||||
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
|
||||
},
|
||||
"text_desc_required": "Please enter image description first",
|
||||
"image_handle_required": "Please upload an image first.",
|
||||
"req_error_text": "Operation failed. Please try again. Avoid using 'copyrighted' or 'sensitive' words in your prompt.",
|
||||
"req_error_token": "Please check the validity of the token",
|
||||
"req_error_no_balance": "Please check the validity of the token",
|
||||
"auto_create_paint": "Auto-create image",
|
||||
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically."
|
||||
|
||||
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically.",
|
||||
"select_model": "Select Model",
|
||||
"input_parameters": "Input Parameters",
|
||||
"input_image": "Input Image",
|
||||
"generated_image": "Generated Image",
|
||||
"pricing": "Pricing",
|
||||
"model_and_pricing": "Model & Pricing",
|
||||
"per_image": "per image",
|
||||
"per_images": "per images",
|
||||
"required_field": "Required field",
|
||||
"uploaded_input": "Uploaded input"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Explain this concept to me",
|
||||
@@ -1097,7 +1113,9 @@
|
||||
"token": "Joplin Authorization Token",
|
||||
"token_placeholder": "Joplin Authorization Token",
|
||||
"url": "Joplin Web Clipper Service URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "Include Reasoning Chain in Export",
|
||||
"export_reasoning.help": "When enabled, the exported content will include the reasoning chain (thought process) generated by the assistant."
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "When enabled, $$ will be forcibly used to mark LaTeX formulas when exporting to Markdown. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.",
|
||||
"markdown_export.force_dollar_math.title": "Force $$ for LaTeX formulas",
|
||||
@@ -1106,12 +1124,14 @@
|
||||
"markdown_export.path_placeholder": "Export Path",
|
||||
"markdown_export.select": "Select",
|
||||
"markdown_export.title": "Markdown Export",
|
||||
"markdown_export.show_model_name.title": "Use Model Name on Export",
|
||||
"markdown_export.show_model_name.help": "When enabled, the topic-naming model will be used to create titles for exported messages. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.",
|
||||
"markdown_export.show_model_provider.title": "Show Model Provider",
|
||||
"markdown_export.show_model_provider.help": "Display the model provider (e.g., OpenAI, Gemini) when exporting to Markdown",
|
||||
"minute_interval_one": "{{count}} minute",
|
||||
"minute_interval_other": "{{count}} minutes",
|
||||
"notion.api_key": "Notion API Key",
|
||||
"notion.api_key_placeholder": "Enter Notion API Key",
|
||||
"notion.auto_split": "Auto split when exporting",
|
||||
"notion.auto_split_tip": "Automatically split pages when exporting long topics to Notion",
|
||||
"notion.check": {
|
||||
"button": "Check",
|
||||
"empty_api_key": "API key is not configured",
|
||||
@@ -1125,10 +1145,9 @@
|
||||
"notion.help": "Notion Configuration Documentation",
|
||||
"notion.page_name_key": "Page Title Field Name",
|
||||
"notion.page_name_key_placeholder": "Enter page title field name, default is Name",
|
||||
"notion.split_size": "Split size",
|
||||
"notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90",
|
||||
"notion.split_size_placeholder": "Enter block limit per page (default 90)",
|
||||
"notion.title": "Notion Configuration",
|
||||
"notion.title": "Notion Settings",
|
||||
"notion.export_reasoning.title": "Include Reasoning Chain in Export",
|
||||
"notion.export_reasoning.help": "When enabled, exported content will include reasoning chain (thought process).",
|
||||
"title": "Data Settings",
|
||||
"webdav": {
|
||||
"autoSync": "Auto Backup",
|
||||
@@ -1324,6 +1343,8 @@
|
||||
"general.emoji_picker": "Emoji Picker",
|
||||
"general.image_upload": "Image Upload",
|
||||
"general.auto_check_update.title": "Auto Update",
|
||||
"general.early_access.title": "Early Access",
|
||||
"general.early_access.tooltip": "Enable to use the latest version from GitHub, which may be slower. Please backup your data in advance.",
|
||||
"general.reset.button": "Reset",
|
||||
"general.reset.title": "Data Reset",
|
||||
"general.restore.button": "Restore",
|
||||
@@ -1491,6 +1512,7 @@
|
||||
"advancedSettings": "Advanced Settings"
|
||||
},
|
||||
"messages.prompt": "Show prompt",
|
||||
"messages.tokens": "Show token usage",
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||
@@ -1794,6 +1816,8 @@
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Any language",
|
||||
"target_language": "Target Language",
|
||||
"alter_language": "Alternative Language",
|
||||
"button.translate": "Translate",
|
||||
"close": "Close",
|
||||
"closed": "Translation closed",
|
||||
@@ -1844,6 +1868,13 @@
|
||||
"show_window": "Show Window",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"update": {
|
||||
"title": "Update",
|
||||
"message": "New version {{version}} is ready, do you want to install it now?",
|
||||
"later": "Later",
|
||||
"install": "Install",
|
||||
"noReleaseNotes": "No release notes"
|
||||
},
|
||||
"selection": {
|
||||
"name": "Selection Assistant",
|
||||
"action": {
|
||||
@@ -1853,7 +1884,8 @@
|
||||
"summary": "Summarize",
|
||||
"search": "Search",
|
||||
"refine": "Refine",
|
||||
"copy": "Copy"
|
||||
"copy": "Copy",
|
||||
"quote": "Quote"
|
||||
},
|
||||
"window": {
|
||||
"pin": "Pin",
|
||||
@@ -1866,6 +1898,9 @@
|
||||
"esc_stop": "Esc: Stop",
|
||||
"c_copy": "C: Copy",
|
||||
"r_regenerate": "R: Regenerate"
|
||||
},
|
||||
"translate": {
|
||||
"smart_translate_tips": "Smart Translation: Content will be translated to the target language first; content already in the target language will be translated to the alternative language"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"add.name.placeholder": "名前を入力",
|
||||
"add.prompt": "プロンプト",
|
||||
"add.prompt.placeholder": "プロンプトを入力",
|
||||
"add.prompt.variables.tip": "利用可能な変数:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "利用可能な変数",
|
||||
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名"
|
||||
},
|
||||
"add.title": "エージェントを作成",
|
||||
"import": {
|
||||
"title": "外部からインポート",
|
||||
@@ -30,16 +33,7 @@
|
||||
"agent": "エージェントをエクスポート"
|
||||
},
|
||||
"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": "マイエージェント",
|
||||
@@ -76,7 +70,6 @@
|
||||
"settings.default_model": "デフォルトモデル",
|
||||
"settings.knowledge_base": "ナレッジベース設定",
|
||||
"settings.model": "モデル設定",
|
||||
"settings.preset_messages": "プリセットメッセージ",
|
||||
"settings.prompt": "プロンプト設定",
|
||||
"settings.reasoning_effort": "思考連鎖の長さ",
|
||||
"settings.reasoning_effort.off": "オフ",
|
||||
@@ -322,6 +315,7 @@
|
||||
"translate": "翻訳",
|
||||
"topics.export.siyuan": "思源笔记にエクスポート",
|
||||
"topics.export.wait_for_title_naming": "タイトルを生成中...",
|
||||
"topics.export.obsidian_reasoning": "思考過程を含める",
|
||||
"topics.export.title_naming_success": "タイトルの生成に成功しました",
|
||||
"topics.export.title_naming_failed": "タイトルの生成に失敗しました。デフォルトのタイトルを使用します",
|
||||
"input.translating": "翻訳中...",
|
||||
@@ -427,7 +421,9 @@
|
||||
"pinyin.asc": "ピンインで昇順ソート",
|
||||
"pinyin.desc": "ピンインで降順ソート"
|
||||
},
|
||||
"no_results": "検索結果なし"
|
||||
"no_results": "検索結果なし",
|
||||
"apps": "アプリ",
|
||||
"mcp": "ツール"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ドキュメント"
|
||||
@@ -586,7 +582,14 @@
|
||||
"korean": "韓国語",
|
||||
"portuguese": "ポルトガル語",
|
||||
"russian": "ロシア語",
|
||||
"spanish": "スペイン語"
|
||||
"spanish": "スペイン語",
|
||||
"polish": "ポーランド語",
|
||||
"turkish": "トルコ語",
|
||||
"thai": "タイ語",
|
||||
"vietnamese": "ベトナム語",
|
||||
"indonesian": "インドネシア語",
|
||||
"urdu": "ウルドゥー語",
|
||||
"malay": "マレー語"
|
||||
},
|
||||
"lmstudio": {
|
||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||
@@ -627,6 +630,7 @@
|
||||
"error.enter.api.key": "APIキーを入力してください",
|
||||
"error.enter.model": "モデルを選択してください",
|
||||
"error.enter.name": "ナレッジベース名を入力してください",
|
||||
"error.fetchTopicName": "トピックの命名に失敗しました",
|
||||
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
|
||||
"error.invalid.api.host": "無効なAPIアドレスです",
|
||||
"error.invalid.api.key": "無効なAPIキーです",
|
||||
@@ -941,9 +945,22 @@
|
||||
"rendering_speed": "レンダリング速度",
|
||||
"translating": "翻訳中...",
|
||||
"text_desc_required": "画像の説明を先に入力してください",
|
||||
"image_handle_required": "最初に画像をアップロードしてください。",
|
||||
"req_error_text": "実行に失敗しました。もう一度お試しください。プロンプトに「著作権用語」や「センシティブな用語」を含めないでください。",
|
||||
"req_error_token": "トークンの有効性を確認してください",
|
||||
"req_error_no_balance": "トークンの有効性を確認してください",
|
||||
"auto_create_paint": "画像を自動作成",
|
||||
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。"
|
||||
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。",
|
||||
"select_model": "モデルを選択",
|
||||
"input_parameters": "パラメータ入力",
|
||||
"input_image": "入力画像",
|
||||
"generated_image": "生成画像",
|
||||
"pricing": "料金",
|
||||
"model_and_pricing": "モデルと料金",
|
||||
"per_image": "1枚あたり",
|
||||
"per_images": "複数枚あたり",
|
||||
"required_field": "必須項目",
|
||||
"uploaded_input": "アップロード済みの入力"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "この概念を説明してください",
|
||||
@@ -1094,7 +1111,9 @@
|
||||
"token": "Joplin 認証トークン",
|
||||
"token_placeholder": "Joplin 認証トークンを入力してください",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "エクスポート時に思考過程を含める",
|
||||
"export_reasoning.help": "有効にすると、エクスポートされる内容にアシスタントが生成した思考過程(リースニングチェーン)が含まれます。"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "有効にすると、Markdownにエクスポートする際にLaTeX数式を$$で強制的にマークします。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX数式に$$を強制使用",
|
||||
@@ -1103,29 +1122,12 @@
|
||||
"markdown_export.path_placeholder": "エクスポートパス",
|
||||
"markdown_export.select": "選択",
|
||||
"markdown_export.title": "Markdown エクスポート",
|
||||
"markdown_export.show_model_name.title": "エクスポート時にモデル名を使用",
|
||||
"markdown_export.show_model_name.help": "有効にすると、トピック命名モデルがエクスポートされたメッセージのタイトル作成に使用されます。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。",
|
||||
"markdown_export.show_model_provider.title": "モデルプロバイダーを表示",
|
||||
"markdown_export.show_model_provider.help": "Markdownエクスポート時にモデルプロバイダー(例:OpenAI、Geminiなど)を表示します。",
|
||||
"minute_interval_one": "{{count}} 分",
|
||||
"minute_interval_other": "{{count}} 分",
|
||||
"notion.api_key": "Notion APIキー",
|
||||
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||
"notion.auto_split": "ダイアログをエクスポートすると自動ページ分割",
|
||||
"notion.auto_split_tip": "ダイアログが長い場合、Notionに自動的にページ分割してエクスポートします",
|
||||
"notion.check": {
|
||||
"button": "確認",
|
||||
"empty_api_key": "Api_keyが設定されていません",
|
||||
"empty_database_id": "Database_idが設定されていません",
|
||||
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"success": "接続に成功しました。"
|
||||
},
|
||||
"notion.database_id": "Notion データベースID",
|
||||
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
|
||||
"notion.help": "Notion 設定ドキュメント",
|
||||
"notion.page_name_key": "ページタイトルフィールド名",
|
||||
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||
"notion.split_size": "自動ページ分割サイズ",
|
||||
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
|
||||
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"title": "データ設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動バックアップ",
|
||||
@@ -1245,7 +1247,25 @@
|
||||
"new_folder.button": "新しいフォルダー"
|
||||
},
|
||||
"message_title.use_topic_naming.title": "トピック命名モデルを使用してメッセージのタイトルを作成",
|
||||
"message_title.use_topic_naming.help": "この設定は、すべてのMarkdownエクスポート方法に影響します。"
|
||||
"message_title.use_topic_naming.help": "この設定は、すべてのMarkdownエクスポート方法に影響します。",
|
||||
"notion.api_key": "Notion APIキー",
|
||||
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||
"notion.check": {
|
||||
"button": "確認",
|
||||
"empty_api_key": "Api_keyが設定されていません",
|
||||
"empty_database_id": "Database_idが設定されていません",
|
||||
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||
"success": "接続に成功しました。"
|
||||
},
|
||||
"notion.database_id": "Notion データベースID",
|
||||
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
|
||||
"notion.help": "Notion 設定ドキュメント",
|
||||
"notion.page_name_key": "ページタイトルフィールド名",
|
||||
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||
"notion.title": "Notion 設定",
|
||||
"notion.export_reasoning.title": "エクスポート時に思考チェーンを含める",
|
||||
"notion.export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。"
|
||||
},
|
||||
"display.assistant.title": "アシスタント設定",
|
||||
"display.custom.css": "カスタムCSS",
|
||||
@@ -1486,6 +1506,7 @@
|
||||
"advancedSettings": "詳細設定"
|
||||
},
|
||||
"messages.prompt": "プロンプト表示",
|
||||
"messages.tokens": "トークン使用量を表示",
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||
@@ -1735,6 +1756,8 @@
|
||||
"content_limit_tooltip": "検索結果の内容長を制限し、制限を超える内容は切り捨てられます。"
|
||||
},
|
||||
"general.auto_check_update.title": "自動更新",
|
||||
"general.early_access.title": "早期アクセス",
|
||||
"general.early_access.tooltip": "有効にすると、GitHub の最新バージョンを使用します。ダウンロード速度が遅く、不安定な場合があります。データを事前にバックアップしてください。",
|
||||
"quickPhrase": {
|
||||
"title": "クイックフレーズ",
|
||||
"add": "フレーズを追加",
|
||||
@@ -1793,6 +1816,8 @@
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意の言語",
|
||||
"target_language": "目標言語",
|
||||
"alter_language": "備用言語",
|
||||
"button.translate": "翻訳",
|
||||
"close": "閉じる",
|
||||
"closed": "翻訳は閉じられました",
|
||||
@@ -1843,6 +1868,13 @@
|
||||
"show_window": "ウィンドウを表示",
|
||||
"visualization": "可視化"
|
||||
},
|
||||
"update": {
|
||||
"title": "更新",
|
||||
"message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?",
|
||||
"later": "後で",
|
||||
"install": "今すぐインストール",
|
||||
"noReleaseNotes": "暫無更新日誌"
|
||||
},
|
||||
"selection": {
|
||||
"name": "テキスト選択ツール",
|
||||
"action": {
|
||||
@@ -1852,7 +1884,8 @@
|
||||
"summary": "要約",
|
||||
"search": "検索",
|
||||
"refine": "最適化",
|
||||
"copy": "コピー"
|
||||
"copy": "コピー",
|
||||
"quote": "引用"
|
||||
},
|
||||
"window": {
|
||||
"pin": "最前面に固定",
|
||||
@@ -1865,6 +1898,9 @@
|
||||
"esc_stop": "Escで停止",
|
||||
"c_copy": "Cでコピー",
|
||||
"r_regenerate": "Rで再生成"
|
||||
},
|
||||
"translate": {
|
||||
"smart_translate_tips": "スマート翻訳:内容は優先的に目標言語に翻訳されます。すでに目標言語の場合は、備用言語に翻訳されます。"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -8,19 +8,13 @@
|
||||
"add.name.placeholder": "Введите имя",
|
||||
"add.prompt": "Промпт",
|
||||
"add.prompt.placeholder": "Введите промпт",
|
||||
"add.prompt.variables.tip": "Доступные переменные: {{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "Доступные переменные",
|
||||
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели"
|
||||
},
|
||||
"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": "Мои агенты",
|
||||
@@ -76,7 +70,6 @@
|
||||
"settings.default_model": "Модель по умолчанию",
|
||||
"settings.knowledge_base": "Настройки базы знаний",
|
||||
"settings.model": "Настройки модели",
|
||||
"settings.preset_messages": "Предустановленные сообщения",
|
||||
"settings.prompt": "Настройки промптов",
|
||||
"settings.reasoning_effort.off": "Выключить",
|
||||
"settings.reasoning_effort.high": "Стараюсь думать",
|
||||
@@ -322,6 +315,7 @@
|
||||
"translate": "Перевести",
|
||||
"topics.export.siyuan": "Экспорт в Siyuan Note",
|
||||
"topics.export.wait_for_title_naming": "Создание заголовка...",
|
||||
"topics.export.obsidian_reasoning": "Включить цепочку рассуждений",
|
||||
"topics.export.title_naming_success": "Заголовок успешно создан",
|
||||
"topics.export.title_naming_failed": "Не удалось создать заголовок, используется заголовок по умолчанию",
|
||||
"input.translating": "Перевод...",
|
||||
@@ -427,7 +421,9 @@
|
||||
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
|
||||
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
|
||||
},
|
||||
"no_results": "Результатов не найдено"
|
||||
"no_results": "Результатов не найдено",
|
||||
"apps": "Приложения",
|
||||
"mcp": "Инструменты"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация"
|
||||
@@ -586,7 +582,14 @@
|
||||
"korean": "Корейский",
|
||||
"portuguese": "Португальский",
|
||||
"russian": "Русский",
|
||||
"spanish": "Испанский"
|
||||
"spanish": "Испанский",
|
||||
"polish": "Польский",
|
||||
"turkish": "Туркменский",
|
||||
"thai": "Тайский",
|
||||
"vietnamese": "Вьетнамский",
|
||||
"indonesian": "Индонезийский",
|
||||
"urdu": "Урду",
|
||||
"malay": "Малайзийский"
|
||||
},
|
||||
"lmstudio": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
@@ -627,6 +630,7 @@
|
||||
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
|
||||
"error.enter.model": "Пожалуйста, выберите модель",
|
||||
"error.enter.name": "Пожалуйста, введите название базы знаний",
|
||||
"error.fetchTopicName": "Не удалось назвать тему",
|
||||
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
|
||||
"error.invalid.api.host": "Неверный API адрес",
|
||||
"error.invalid.api.key": "Неверный API ключ",
|
||||
@@ -941,9 +945,22 @@
|
||||
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
|
||||
},
|
||||
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
|
||||
"image_handle_required": "Пожалуйста, сначала загрузите изображение.",
|
||||
"req_error_text": "Операция не удалась, повторите попытку. Пожалуйста, избегайте защищенных авторским правом терминов и конфиденциальных слов в запросах.",
|
||||
"req_error_token": "Пожалуйста, проверьте действительность токена",
|
||||
"req_error_no_balance": "Пожалуйста, проверьте действительность токена",
|
||||
"auto_create_paint": "Автоматическое создание изображения",
|
||||
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое."
|
||||
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое.",
|
||||
"select_model": "Выбрать модель",
|
||||
"input_parameters": "Ввести параметры",
|
||||
"input_image": "Входное изображение",
|
||||
"generated_image": "Сгенерированное изображение",
|
||||
"pricing": "Цены",
|
||||
"model_and_pricing": "Модель и цены",
|
||||
"per_image": "за изображение",
|
||||
"per_images": "за изображения",
|
||||
"required_field": "Обязательное поле",
|
||||
"uploaded_input": "Загруженный ввод"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Объясните мне этот концепт",
|
||||
@@ -1094,7 +1111,9 @@
|
||||
"token": "Токен Joplin",
|
||||
"token_placeholder": "Введите токен Joplin",
|
||||
"url": "URL Joplin",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "Включить цепочку рассуждений при экспорте",
|
||||
"export_reasoning.help": "Если включено, экспортируемый контент будет содержать цепочку рассуждений, сгенерированную ассистентом."
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "Если включено, при экспорте в Markdown для обозначения формул LaTeX будет принудительно использоваться $$. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.",
|
||||
"markdown_export.force_dollar_math.title": "Принудительно использовать $$ для формул LaTeX",
|
||||
@@ -1103,12 +1122,14 @@
|
||||
"markdown_export.path_placeholder": "Путь экспорта",
|
||||
"markdown_export.select": "Выбрать",
|
||||
"markdown_export.title": "Экспорт в Markdown",
|
||||
"markdown_export.show_model_name.title": "Использовать имя модели при экспорте",
|
||||
"markdown_export.show_model_name.help": "Если включено, для создания заголовков экспортируемых сообщений будет использоваться модель именования темы. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.",
|
||||
"markdown_export.show_model_provider.title": "Показать поставщика модели",
|
||||
"markdown_export.show_model_provider.help": "Показывать поставщика модели (например, OpenAI, Gemini) при экспорте в Markdown",
|
||||
"minute_interval_one": "{{count}} минута",
|
||||
"minute_interval_other": "{{count}} минут",
|
||||
"notion.api_key": "Ключ API Notion",
|
||||
"notion.api_key_placeholder": "Введите ключ API Notion",
|
||||
"notion.auto_split": "Автоматическое разбиение на страницы при экспорте диалога",
|
||||
"notion.auto_split_tip": "Автоматическое разбиение на страницы при экспорте в Notion, если тема слишком длинная",
|
||||
"notion.check": {
|
||||
"button": "Проверить",
|
||||
"empty_api_key": "Не настроен API key",
|
||||
@@ -1122,10 +1143,9 @@
|
||||
"notion.help": "Документация по настройке Notion",
|
||||
"notion.page_name_key": "Название поля заголовка страницы",
|
||||
"notion.page_name_key_placeholder": "Введите название поля заголовка страницы, по умолчанию Name",
|
||||
"notion.split_size": "Размер автоматического разбиения",
|
||||
"notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90",
|
||||
"notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)",
|
||||
"notion.title": "Настройки Notion",
|
||||
"notion.export_reasoning.title": "Включить цепочку рассуждений при экспорте",
|
||||
"notion.export_reasoning.help": "При включении, содержимое цепочки рассуждений будет включено при экспорте в Notion.",
|
||||
"title": "Настройки данных",
|
||||
"webdav": {
|
||||
"autoSync": "Автоматическое резервное копирование",
|
||||
@@ -1486,6 +1506,7 @@
|
||||
"advancedSettings": "Расширенные настройки"
|
||||
},
|
||||
"messages.prompt": "Показывать подсказки",
|
||||
"messages.tokens": "Показать использование токенов",
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||
@@ -1734,7 +1755,9 @@
|
||||
"content_limit": "Ограничение длины текста",
|
||||
"content_limit_tooltip": "Ограничьте длину содержимого результатов поиска, контент, превышающий ограничение, будет обрезан."
|
||||
},
|
||||
"general.auto_check_update.title": "Включить автообновление",
|
||||
"general.auto_check_update.title": "Автоматическое обновление",
|
||||
"general.early_access.title": "Ранний доступ",
|
||||
"general.early_access.tooltip": "Включить для использования последней версии из GitHub, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.",
|
||||
"quickPhrase": {
|
||||
"title": "Быстрые фразы",
|
||||
"add": "Добавить фразу",
|
||||
@@ -1793,6 +1816,8 @@
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Любой язык",
|
||||
"target_language": "Целевой язык",
|
||||
"alter_language": "Альтернативный язык",
|
||||
"button.translate": "Перевести",
|
||||
"close": "Закрыть",
|
||||
"closed": "Перевод закрыт",
|
||||
@@ -1843,6 +1868,13 @@
|
||||
"show_window": "Показать окно",
|
||||
"visualization": "Визуализация"
|
||||
},
|
||||
"update": {
|
||||
"title": "Обновление",
|
||||
"message": "Новая версия {{version}} готова, установить сейчас?",
|
||||
"later": "Позже",
|
||||
"install": "Установить",
|
||||
"noReleaseNotes": "Нет заметок об обновлении"
|
||||
},
|
||||
"selection": {
|
||||
"name": "Помощник выбора",
|
||||
"action": {
|
||||
@@ -1852,7 +1884,8 @@
|
||||
"summary": "Суммаризировать",
|
||||
"search": "Поиск",
|
||||
"refine": "Уточнить",
|
||||
"copy": "Копировать"
|
||||
"copy": "Копировать",
|
||||
"quote": "Цитировать"
|
||||
},
|
||||
"window": {
|
||||
"pin": "Закрепить",
|
||||
@@ -1865,6 +1898,9 @@
|
||||
"esc_stop": "Esc - остановить",
|
||||
"c_copy": "C - копировать",
|
||||
"r_regenerate": "R - перегенерировать"
|
||||
},
|
||||
"translate": {
|
||||
"smart_translate_tips": "Смарт-перевод: содержимое будет переведено на целевой язык; содержимое уже на целевом языке будет переведено на альтернативный язык"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"add.name.placeholder": "输入名称",
|
||||
"add.prompt": "提示词",
|
||||
"add.prompt.placeholder": "输入提示词",
|
||||
"add.prompt.variables.tip": "可用的变量:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "可用的变量",
|
||||
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称"
|
||||
},
|
||||
"add.title": "创建智能体",
|
||||
"import": {
|
||||
"title": "从外部导入",
|
||||
@@ -30,16 +33,7 @@
|
||||
"agent": "导出智能体"
|
||||
},
|
||||
"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": "我的智能体",
|
||||
@@ -83,7 +77,6 @@
|
||||
"settings.tool_use_mode.function": "函数",
|
||||
"settings.tool_use_mode.prompt": "提示词",
|
||||
"settings.model": "模型设置",
|
||||
"settings.preset_messages": "预设消息",
|
||||
"settings.prompt": "提示词设置",
|
||||
"settings.reasoning_effort": "思维链长度",
|
||||
"settings.reasoning_effort.off": "关闭",
|
||||
@@ -325,6 +318,7 @@
|
||||
"topics.export.obsidian_no_vault_selected": "请先选择一个保管库",
|
||||
"topics.export.obsidian_select_vault_first": "请先选择保管库",
|
||||
"topics.export.obsidian_root_directory": "根目录",
|
||||
"topics.export.obsidian_reasoning": "导出思维链",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"topics.export.yuque": "导出到语雀",
|
||||
@@ -427,7 +421,9 @@
|
||||
"pinyin.asc": "按拼音升序",
|
||||
"pinyin.desc": "按拼音降序"
|
||||
},
|
||||
"no_results": "无结果"
|
||||
"no_results": "无结果",
|
||||
"apps": "应用",
|
||||
"mcp": "工具"
|
||||
},
|
||||
"docs": {
|
||||
"title": "帮助文档"
|
||||
@@ -586,7 +582,14 @@
|
||||
"korean": "韩文",
|
||||
"portuguese": "葡萄牙文",
|
||||
"russian": "俄文",
|
||||
"spanish": "西班牙文"
|
||||
"spanish": "西班牙文",
|
||||
"polish": "波兰文",
|
||||
"turkish": "土耳其文",
|
||||
"thai": "泰文",
|
||||
"vietnamese": "越南文",
|
||||
"indonesian": "印尼文",
|
||||
"urdu": "乌尔都文",
|
||||
"malay": "马来文"
|
||||
},
|
||||
"lmstudio": {
|
||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
||||
@@ -627,6 +630,7 @@
|
||||
"error.enter.api.key": "请输入您的 API 密钥",
|
||||
"error.enter.model": "请选择一个模型",
|
||||
"error.enter.name": "请输入知识库名称",
|
||||
"error.fetchTopicName": "话题命名失败",
|
||||
"error.get_embedding_dimensions": "获取嵌入维度失败",
|
||||
"error.invalid.api.host": "无效的 API 地址",
|
||||
"error.invalid.api.key": "无效的 API 密钥",
|
||||
@@ -646,7 +650,7 @@
|
||||
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
|
||||
"group.delete.title": "删除分组消息",
|
||||
"ignore.knowledge.base": "联网模式开启,忽略知识库",
|
||||
"info.notion.block_reach_limit": "对话过长,正在分页导出到Notion",
|
||||
"info.notion.block_reach_limit": "对话过长,正在分段导出到Notion",
|
||||
"loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "正在准备导出到Notion...",
|
||||
"mention.title": "切换模型回答",
|
||||
@@ -941,9 +945,22 @@
|
||||
"magic_prompt_option_tip": "智能优化放大提示词"
|
||||
},
|
||||
"text_desc_required": "请先输入图片描述",
|
||||
"req_error_text" : "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
|
||||
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
|
||||
"req_error_token": "请检查令牌有效性",
|
||||
"req_error_no_balance": "请检查令牌有效性",
|
||||
"image_handle_required": "请先上传图片",
|
||||
"auto_create_paint": "自动新建图片",
|
||||
"auto_create_paint_tip": "在图片生成后,会自动新建图片"
|
||||
"auto_create_paint_tip": "在图片生成后,会自动新建图片",
|
||||
"select_model": "选择模型",
|
||||
"input_parameters": "输入参数",
|
||||
"input_image": "输入图片",
|
||||
"generated_image": "生成图片",
|
||||
"pricing": "定价",
|
||||
"model_and_pricing": "模型与定价",
|
||||
"per_image": "每张图片",
|
||||
"per_images": "每张图片",
|
||||
"required_field": "必填项",
|
||||
"uploaded_input": "已上传输入"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "帮我解释一下这个概念",
|
||||
@@ -1096,7 +1113,9 @@
|
||||
"token": "Joplin 授权令牌",
|
||||
"token_placeholder": "请输入 Joplin 授权令牌",
|
||||
"url": "Joplin 剪裁服务监听 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "导出时包含思维链",
|
||||
"export_reasoning.help": "开启后,导出到Joplin时会包含思维链内容。"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等",
|
||||
"markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式",
|
||||
@@ -1105,14 +1124,16 @@
|
||||
"markdown_export.path_placeholder": "导出路径",
|
||||
"markdown_export.select": "选择",
|
||||
"markdown_export.title": "Markdown 导出",
|
||||
"markdown_export.show_model_name.title": "导出时使用模型名称",
|
||||
"markdown_export.show_model_name.help": "开启后,使用话题命名模型为导出的消息创建标题。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。",
|
||||
"markdown_export.show_model_provider.title": "显示模型供应商",
|
||||
"markdown_export.show_model_provider.help": "在导出Markdown时显示模型供应商,如OpenAI、Gemini等",
|
||||
"message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题",
|
||||
"message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式",
|
||||
"minute_interval_one": "{{count}} 分钟",
|
||||
"minute_interval_other": "{{count}} 分钟",
|
||||
"notion.api_key": "Notion 密钥",
|
||||
"notion.api_key_placeholder": "请输入Notion 密钥",
|
||||
"notion.auto_split": "导出对话时自动分页",
|
||||
"notion.auto_split_tip": "当要导出的话题过长时自动分页导出到Notion",
|
||||
"notion.check": {
|
||||
"button": "检测",
|
||||
"empty_api_key": "未配置 API key",
|
||||
@@ -1126,10 +1147,9 @@
|
||||
"notion.help": "Notion 配置文档",
|
||||
"notion.page_name_key": "页面标题字段名",
|
||||
"notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name",
|
||||
"notion.split_size": "自动分页大小",
|
||||
"notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90",
|
||||
"notion.split_size_placeholder": "请输入每页块数限制(默认90)",
|
||||
"notion.title": "Notion 配置",
|
||||
"notion.title": "Notion 设置",
|
||||
"notion.export_reasoning.title": "导出时包含思维链",
|
||||
"notion.export_reasoning.help": "开启后,导出到Notion时会包含思维链内容。",
|
||||
"title": "数据设置",
|
||||
"webdav": {
|
||||
"autoSync": "自动备份",
|
||||
@@ -1323,6 +1343,8 @@
|
||||
"general.emoji_picker": "表情选择器",
|
||||
"general.image_upload": "图片上传",
|
||||
"general.auto_check_update.title": "自动更新",
|
||||
"general.early_access.title": "抢先体验",
|
||||
"general.early_access.tooltip": "开启后,将使用 GitHub 的最新版本,下载速度可能较慢,请务必提前备份数据",
|
||||
"general.reset.button": "重置",
|
||||
"general.reset.title": "重置数据",
|
||||
"general.restore.button": "恢复",
|
||||
@@ -1490,6 +1512,7 @@
|
||||
"advancedSettings": "高级设置"
|
||||
},
|
||||
"messages.prompt": "显示提示词",
|
||||
"messages.tokens": "显示Token用量",
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
"messages.grid_popover_trigger": "网格详情触发",
|
||||
@@ -1683,7 +1706,7 @@
|
||||
"search_message_in_chat": "在当前对话中搜索消息",
|
||||
"show_app": "显示/隐藏应用",
|
||||
"show_settings": "打开设置",
|
||||
"title": "快捷方式",
|
||||
"title": "快捷键",
|
||||
"toggle_new_context": "清除上下文",
|
||||
"toggle_show_assistants": "切换助手显示",
|
||||
"toggle_show_topics": "切换话题显示",
|
||||
@@ -1791,8 +1814,17 @@
|
||||
"service_tier.flex": "灵活"
|
||||
}
|
||||
},
|
||||
"discover": {
|
||||
"title": "发现",
|
||||
"install": "安装",
|
||||
"uninstall": "卸载",
|
||||
"update": "更新",
|
||||
"update_all": "全部更新"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意语言",
|
||||
"target_language": "目标语言",
|
||||
"alter_language": "备用语言",
|
||||
"button.translate": "翻译",
|
||||
"close": "关闭",
|
||||
"closed": "翻译已关闭",
|
||||
@@ -1843,6 +1875,13 @@
|
||||
"show_window": "显示窗口",
|
||||
"visualization": "可视化"
|
||||
},
|
||||
"update": {
|
||||
"title": "更新提示",
|
||||
"message": "发现新版本 {{version}},是否立即安装?",
|
||||
"later": "稍后",
|
||||
"install": "立即安装",
|
||||
"noReleaseNotes": "暂无更新日志"
|
||||
},
|
||||
"selection": {
|
||||
"name": "划词助手",
|
||||
"action": {
|
||||
@@ -1852,7 +1891,8 @@
|
||||
"summary": "总结",
|
||||
"search": "搜索",
|
||||
"refine": "优化",
|
||||
"copy": "复制"
|
||||
"copy": "复制",
|
||||
"quote": "引用"
|
||||
},
|
||||
"window": {
|
||||
"pin": "置顶",
|
||||
@@ -1865,6 +1905,9 @@
|
||||
"esc_stop": "Esc 停止",
|
||||
"c_copy": "C 复制",
|
||||
"r_regenerate": "R 重新生成"
|
||||
},
|
||||
"translate": {
|
||||
"smart_translate_tips": "智能翻译:内容将优先翻译为目标语言;内容已是目标语言的,将翻译为备选语言"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
"add.name.placeholder": "輸入名稱",
|
||||
"add.prompt": "提示詞",
|
||||
"add.prompt.placeholder": "輸入提示詞",
|
||||
"add.prompt.variables.tip": "可用的變數:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "可用的變數",
|
||||
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱"
|
||||
},
|
||||
"add.title": "建立智慧代理人",
|
||||
"import": {
|
||||
"title": "從外部導入",
|
||||
@@ -30,16 +33,7 @@
|
||||
"agent": "匯出智慧代理人"
|
||||
},
|
||||
"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": "我的智慧代理人",
|
||||
@@ -76,7 +70,6 @@
|
||||
"settings.default_model": "預設模型",
|
||||
"settings.knowledge_base": "知識庫設定",
|
||||
"settings.model": "模型設定",
|
||||
"settings.preset_messages": "預設訊息",
|
||||
"settings.prompt": "提示詞設定",
|
||||
"settings.reasoning_effort": "思維鏈長度",
|
||||
"settings.reasoning_effort.off": "關閉",
|
||||
@@ -322,6 +315,7 @@
|
||||
"translate": "翻譯",
|
||||
"topics.export.siyuan": "匯出到思源筆記",
|
||||
"topics.export.wait_for_title_naming": "正在生成標題...",
|
||||
"topics.export.obsidian_reasoning": "包含思維鏈",
|
||||
"topics.export.title_naming_success": "標題生成成功",
|
||||
"topics.export.title_naming_failed": "標題生成失敗,使用預設標題",
|
||||
"input.translating": "翻譯中...",
|
||||
@@ -427,7 +421,9 @@
|
||||
"pinyin.asc": "按拼音升序",
|
||||
"pinyin.desc": "按拼音降序"
|
||||
},
|
||||
"no_results": "沒有結果"
|
||||
"no_results": "沒有結果",
|
||||
"apps": "應用",
|
||||
"mcp": "工具"
|
||||
},
|
||||
"docs": {
|
||||
"title": "說明文件"
|
||||
@@ -586,7 +582,14 @@
|
||||
"korean": "韓文",
|
||||
"portuguese": "葡萄牙文",
|
||||
"russian": "俄文",
|
||||
"spanish": "西班牙文"
|
||||
"spanish": "西班牙文",
|
||||
"polish": "波蘭文",
|
||||
"turkish": "土耳其文",
|
||||
"thai": "泰文",
|
||||
"vietnamese": "越南文",
|
||||
"indonesian": "印尼文",
|
||||
"urdu": "烏爾都文",
|
||||
"malay": "馬來文"
|
||||
},
|
||||
"lmstudio": {
|
||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)",
|
||||
@@ -627,6 +630,7 @@
|
||||
"error.enter.api.key": "請先輸入您的 API 金鑰",
|
||||
"error.enter.model": "請先選擇一個模型",
|
||||
"error.enter.name": "請先輸入知識庫名稱",
|
||||
"error.fetchTopicName": "話題命名失敗",
|
||||
"error.get_embedding_dimensions": "取得嵌入維度失敗",
|
||||
"error.invalid.api.host": "無效的 API 位址",
|
||||
"error.invalid.api.key": "無效的 API 金鑰",
|
||||
@@ -941,9 +945,22 @@
|
||||
},
|
||||
"rendering_speed": "渲染速度",
|
||||
"text_desc_required": "請先輸入圖片描述",
|
||||
"req_error_text" : "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
|
||||
"image_handle_required": "請先上傳圖片。",
|
||||
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
|
||||
"req_error_token": "請檢查令牌的有效性",
|
||||
"req_error_no_balance": "請檢查令牌的有效性",
|
||||
"auto_create_paint": "自動新增圖片",
|
||||
"auto_create_paint_tip": "圖片生成後,會自動新增圖片"
|
||||
"auto_create_paint_tip": "圖片生成後,會自動新增圖片",
|
||||
"select_model": "選擇模型",
|
||||
"input_parameters": "輸入參數",
|
||||
"input_image": "輸入圖片",
|
||||
"generated_image": "生成圖片",
|
||||
"pricing": "定價",
|
||||
"model_and_pricing": "模型與定價",
|
||||
"per_image": "每張圖片",
|
||||
"per_images": "每張圖片",
|
||||
"required_field": "必填欄位",
|
||||
"uploaded_input": "已上傳輸入"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "幫我解釋一下這個概念",
|
||||
@@ -1096,7 +1113,9 @@
|
||||
"token": "Joplin 授權Token",
|
||||
"token_placeholder": "請輸入 Joplin 授權Token",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"export_reasoning.title": "匯出時包含思維鏈",
|
||||
"export_reasoning.help": "啟用後,匯出內容將包含助手生成的思維鏈(思考過程)。"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$",
|
||||
@@ -1105,12 +1124,14 @@
|
||||
"markdown_export.path_placeholder": "匯出路徑",
|
||||
"markdown_export.select": "選擇",
|
||||
"markdown_export.title": "Markdown 匯出",
|
||||
"markdown_export.show_model_name.title": "匯出時使用模型名稱",
|
||||
"markdown_export.show_model_name.help": "啟用後,將以主題命名模型為匯出的訊息建立標題。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等。",
|
||||
"markdown_export.show_model_provider.title": "顯示模型供應商",
|
||||
"markdown_export.show_model_provider.help": "在匯出Markdown時顯示模型供應商,如OpenAI、Gemini等",
|
||||
"minute_interval_one": "{{count}} 分鐘",
|
||||
"minute_interval_other": "{{count}} 分鐘",
|
||||
"notion.api_key": "Notion 金鑰",
|
||||
"notion.api_key_placeholder": "請輸入 Notion 金鑰",
|
||||
"notion.auto_split": "匯出對話時自動分頁",
|
||||
"notion.auto_split_tip": "當要匯出的話題過長時自動分頁匯出到 Notion",
|
||||
"notion.check": {
|
||||
"button": "檢查",
|
||||
"empty_api_key": "未設定 API key",
|
||||
@@ -1124,10 +1145,9 @@
|
||||
"notion.help": "Notion 設定文件",
|
||||
"notion.page_name_key": "頁面標題欄位名稱",
|
||||
"notion.page_name_key_placeholder": "請輸入頁面標題欄位名稱,預設為 Name",
|
||||
"notion.split_size": "自動分頁大小",
|
||||
"notion.split_size_help": "Notion 免費版使用者建議設定為 90,進階版使用者建議設定為 24990,預設值為 90",
|
||||
"notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"notion.export_reasoning.title": "匯出時包含思維鏈",
|
||||
"notion.export_reasoning.help": "啟用後,匯出到Notion時會包含思維鏈內容。",
|
||||
"title": "資料設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動備份",
|
||||
@@ -1489,6 +1509,7 @@
|
||||
"advancedSettings": "高級設定"
|
||||
},
|
||||
"messages.prompt": "提示詞顯示",
|
||||
"messages.tokens": "Token用量顯示",
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
"messages.grid_popover_trigger": "網格詳細資訊觸發",
|
||||
@@ -1675,7 +1696,7 @@
|
||||
"search_message_in_chat": "在當前對話中搜尋訊息",
|
||||
"show_app": "顯示/隱藏應用程式",
|
||||
"show_settings": "開啟設定",
|
||||
"title": "快速方式",
|
||||
"title": "快捷鍵",
|
||||
"toggle_new_context": "清除上下文",
|
||||
"toggle_show_assistants": "切換助手顯示",
|
||||
"toggle_show_topics": "切換話題顯示",
|
||||
@@ -1737,7 +1758,9 @@
|
||||
"content_limit": "內容長度限制",
|
||||
"content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷"
|
||||
},
|
||||
"general.auto_check_update.title": "啟用自動更新",
|
||||
"general.auto_check_update.title": "自動更新",
|
||||
"general.early_access.title": "搶先體驗",
|
||||
"general.early_access.tooltip": "開啟後,將使用 GitHub 的最新版本,下載速度可能較慢,請務必提前備份數據",
|
||||
"quickPhrase": {
|
||||
"title": "快捷短語",
|
||||
"add": "新增短語",
|
||||
@@ -1793,6 +1816,8 @@
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意語言",
|
||||
"target_language": "目標語言",
|
||||
"alter_language": "備用語言",
|
||||
"button.translate": "翻譯",
|
||||
"close": "關閉",
|
||||
"closed": "翻譯已關閉",
|
||||
@@ -1843,6 +1868,13 @@
|
||||
"show_window": "顯示視窗",
|
||||
"visualization": "視覺化"
|
||||
},
|
||||
"update": {
|
||||
"title": "更新提示",
|
||||
"message": "新版本 {{version}} 已準備就緒,是否立即安裝?",
|
||||
"later": "稍後",
|
||||
"install": "立即安裝",
|
||||
"noReleaseNotes": "暫無更新日誌"
|
||||
},
|
||||
"selection": {
|
||||
"name": "劃詞助手",
|
||||
"action": {
|
||||
@@ -1852,7 +1884,8 @@
|
||||
"summary": "總結",
|
||||
"search": "搜尋",
|
||||
"refine": "優化",
|
||||
"copy": "複製"
|
||||
"copy": "複製",
|
||||
"quote": "引用"
|
||||
},
|
||||
"window": {
|
||||
"pin": "置頂",
|
||||
@@ -1865,6 +1898,9 @@
|
||||
"esc_stop": "Esc 停止",
|
||||
"c_copy": "C 複製",
|
||||
"r_regenerate": "R 重新生成"
|
||||
},
|
||||
"translate": {
|
||||
"smart_translate_tips": "智能翻譯:內容將優先翻譯為目標語言;內容已是目標語言的,將翻譯為備用語言"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@@ -8,18 +8,13 @@
|
||||
"add.name.placeholder": "Εισαγάγετε όνομα",
|
||||
"add.prompt": "Φράση προκαλέσεως",
|
||||
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "Διαθέσιμες μεταβλητές",
|
||||
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου"
|
||||
},
|
||||
"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": "Οι ειδικοί μου",
|
||||
@@ -64,7 +59,6 @@
|
||||
"settings.default_model": "Προεπιλεγμένο μοντέλο",
|
||||
"settings.knowledge_base": "Ρυθμίσεις βάσης γνώσεων",
|
||||
"settings.model": "Ρυθμίσεις μοντέλου",
|
||||
"settings.preset_messages": "Προεπιλεγμένα μηνύματα",
|
||||
"settings.prompt": "Ρυθμίσεις προκαλύμματος",
|
||||
"settings.reasoning_effort": "Μήκος λογισμικού αλυσίδας",
|
||||
"settings.reasoning_effort.high": "Μεγάλο",
|
||||
@@ -557,6 +551,7 @@
|
||||
"error.enter.api.key": "Παρακαλώ εισάγετε το κλειδί API σας",
|
||||
"error.enter.model": "Παρακαλώ επιλέξτε ένα μοντέλο",
|
||||
"error.enter.name": "Παρακαλώ εισάγετε ένα όνομα για τη βάση γνώσεων",
|
||||
"error.fetchTopicName": "Αποτυχία ονοματοδοσίας θέματος",
|
||||
"error.get_embedding_dimensions": "Απέτυχε η πρόσληψη διαστάσεων ενσωμάτωσης",
|
||||
"error.invalid.api.host": "Μη έγκυρη διεύθυνση API",
|
||||
"error.invalid.api.key": "Μη έγκυρο κλειδί API",
|
||||
@@ -1657,6 +1652,13 @@
|
||||
"quit": "Έξοδος",
|
||||
"show_window": "Εμφάνιση Παραθύρου",
|
||||
"visualization": "προβολή"
|
||||
},
|
||||
"update": {
|
||||
"title": "Ενημέρωση",
|
||||
"message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;",
|
||||
"later": "Μετά",
|
||||
"install": "Εγκατάσταση",
|
||||
"noReleaseNotes": "Χωρίς σημειώσεις"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,13 @@
|
||||
"add.name.placeholder": "Ingrese el nombre",
|
||||
"add.prompt": "Palabra clave",
|
||||
"add.prompt.placeholder": "Ingrese la palabra clave",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "Variables disponibles",
|
||||
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo"
|
||||
},
|
||||
"add.title": "Crear agente inteligente",
|
||||
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
|
||||
"edit.message.add.title": "Agregar",
|
||||
"edit.message.assistant.placeholder": "Ingrese el mensaje del asistente",
|
||||
"edit.message.assistant.title": "Asistente",
|
||||
"edit.message.empty.content": "El contenido de la sesión de chat no puede estar vacío",
|
||||
"edit.message.group.title": "Grupo de mensajes",
|
||||
"edit.message.title": "Mensaje predeterminado",
|
||||
"edit.message.user.placeholder": "Ingrese el mensaje del usuario",
|
||||
"edit.message.user.title": "Usuario",
|
||||
"edit.model.select.title": "Seleccionar modelo",
|
||||
"edit.settings.hide_preset_messages": "Ocultar mensajes predeterminados",
|
||||
"edit.title": "Editar agente inteligente",
|
||||
"manage.title": "Administrar agentes inteligentes",
|
||||
"my_agents": "Mis agentes inteligentes",
|
||||
@@ -64,7 +59,6 @@
|
||||
"settings.default_model": "Modelo Predeterminado",
|
||||
"settings.knowledge_base": "Configuración de Base de Conocimientos",
|
||||
"settings.model": "Configuración de Modelo",
|
||||
"settings.preset_messages": "Mensajes Preestablecidos",
|
||||
"settings.prompt": "Configuración de Palabras Clave",
|
||||
"settings.reasoning_effort": "Longitud de Cadena de Razonamiento",
|
||||
"settings.reasoning_effort.high": "Largo",
|
||||
@@ -558,6 +552,7 @@
|
||||
"error.enter.api.key": "Ingrese su clave API",
|
||||
"error.enter.model": "Seleccione un modelo",
|
||||
"error.enter.name": "Ingrese el nombre de la base de conocimiento",
|
||||
"error.fetchTopicName": "Error al nombrar el tema",
|
||||
"error.get_embedding_dimensions": "Fallo al obtener las dimensiones de incrustación",
|
||||
"error.invalid.api.host": "Dirección API inválida",
|
||||
"error.invalid.api.key": "Clave API inválida",
|
||||
@@ -1656,6 +1651,13 @@
|
||||
"quit": "Salir",
|
||||
"show_window": "Mostrar Ventana",
|
||||
"visualization": "Visualización"
|
||||
},
|
||||
"update": {
|
||||
"title": "Actualización",
|
||||
"message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?",
|
||||
"later": "Más tarde",
|
||||
"install": "Instalar",
|
||||
"noReleaseNotes": "Sin notas de la versión"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,13 @@
|
||||
"add.name.placeholder": "Entrer le nom",
|
||||
"add.prompt": "Mot-clé",
|
||||
"add.prompt.placeholder": "Entrer le mot-clé",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "Variables disponibles",
|
||||
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle"
|
||||
},
|
||||
"add.title": "Créer un agent intelligent",
|
||||
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
|
||||
"edit.message.add.title": "Ajouter",
|
||||
"edit.message.assistant.placeholder": "Entrer le message de l'assistant",
|
||||
"edit.message.assistant.title": "Assistant",
|
||||
"edit.message.empty.content": "Le contenu de la session ne peut pas être vide",
|
||||
"edit.message.group.title": "Groupe de messages",
|
||||
"edit.message.title": "Messages prédéfinis",
|
||||
"edit.message.user.placeholder": "Entrer le message de l'utilisateur",
|
||||
"edit.message.user.title": "Utilisateur",
|
||||
"edit.model.select.title": "Sélectionner un modèle",
|
||||
"edit.settings.hide_preset_messages": "Masquer les messages prédéfinis",
|
||||
"edit.title": "Modifier l'agent intelligent",
|
||||
"manage.title": "Gérer les agents intelligents",
|
||||
"my_agents": "Mes agents intelligents",
|
||||
@@ -64,7 +59,6 @@
|
||||
"settings.default_model": "Modèle par défaut",
|
||||
"settings.knowledge_base": "Paramètres de la base de connaissances",
|
||||
"settings.model": "Paramètres du modèle",
|
||||
"settings.preset_messages": "Messages prédéfinis",
|
||||
"settings.prompt": "Paramètres de l'invite",
|
||||
"settings.reasoning_effort": "Longueur de la chaîne de raisonnement",
|
||||
"settings.reasoning_effort.high": "Long",
|
||||
@@ -557,6 +551,7 @@
|
||||
"error.enter.api.key": "Veuillez entrer votre clé API",
|
||||
"error.enter.model": "Veuillez sélectionner un modèle",
|
||||
"error.enter.name": "Veuillez entrer le nom de la base de connaissances",
|
||||
"error.fetchTopicName": "Échec de la dénomination du sujet",
|
||||
"error.get_embedding_dimensions": "Impossible d'obtenir les dimensions d'encodage",
|
||||
"error.invalid.api.host": "Adresse API invalide",
|
||||
"error.invalid.api.key": "Clé API invalide",
|
||||
@@ -1657,6 +1652,13 @@
|
||||
"quit": "Quitter",
|
||||
"show_window": "Afficher la fenêtre",
|
||||
"visualization": "Visualisation"
|
||||
},
|
||||
"update": {
|
||||
"title": "Mise à jour",
|
||||
"message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?",
|
||||
"later": "Plus tard",
|
||||
"install": "Installer",
|
||||
"noReleaseNotes": "Aucune note de version"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,13 @@
|
||||
"add.name.placeholder": "Digite o Nome",
|
||||
"add.prompt": "Prompt",
|
||||
"add.prompt.placeholder": "Digite o Prompt",
|
||||
"add.prompt.variables.tip": {
|
||||
"title": "Variáveis disponíveis",
|
||||
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo"
|
||||
},
|
||||
"add.title": "Criar Agente Inteligente",
|
||||
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
|
||||
"edit.message.add.title": "Adicionar",
|
||||
"edit.message.assistant.placeholder": "Digite a Mensagem do Assistente",
|
||||
"edit.message.assistant.title": "Assistente",
|
||||
"edit.message.empty.content": "O conteúdo da sessão não pode estar vazio",
|
||||
"edit.message.group.title": "Grupo de Mensagens",
|
||||
"edit.message.title": "Mensagens Padrão",
|
||||
"edit.message.user.placeholder": "Digite a Mensagem do Usuário",
|
||||
"edit.message.user.title": "Usuário",
|
||||
"edit.model.select.title": "Selecionar Modelo",
|
||||
"edit.settings.hide_preset_messages": "Ocultar Mensagens Padrão",
|
||||
"edit.title": "Editar Agente Inteligente",
|
||||
"manage.title": "Gerenciar Agentes Inteligentes",
|
||||
"my_agents": "Meus Agentes Inteligentes",
|
||||
@@ -64,7 +59,6 @@
|
||||
"settings.default_model": "Modelo Padrão",
|
||||
"settings.knowledge_base": "Configurações da Base de Conhecimento",
|
||||
"settings.model": "Configurações do Modelo",
|
||||
"settings.preset_messages": "Mensagens Pré-definidas",
|
||||
"settings.prompt": "Configurações de Prompt",
|
||||
"settings.reasoning_effort": "Comprimento da Cadeia de Raciocínio",
|
||||
"settings.reasoning_effort.high": "Longo",
|
||||
@@ -559,6 +553,7 @@
|
||||
"error.enter.api.key": "Insira sua chave API",
|
||||
"error.enter.model": "Selecione um modelo",
|
||||
"error.enter.name": "Insira o nome da base de conhecimento",
|
||||
"error.fetchTopicName": "Falha ao nomear o tópico",
|
||||
"error.get_embedding_dimensions": "Falha ao obter dimensões de incorporação",
|
||||
"error.invalid.api.host": "Endereço API inválido",
|
||||
"error.invalid.api.key": "Chave API inválida",
|
||||
@@ -1659,6 +1654,13 @@
|
||||
"quit": "Sair",
|
||||
"show_window": "Exibir Janela",
|
||||
"visualization": "Visualização"
|
||||
},
|
||||
"update": {
|
||||
"title": "Atualização",
|
||||
"message": "Nova versão {{version}} disponível, deseja instalar agora?",
|
||||
"later": "Mais tarde",
|
||||
"install": "Instalar",
|
||||
"noReleaseNotes": "Sem notas de versão"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@@ -152,27 +151,23 @@ const AgentsPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('agents.title')}
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
allowClear
|
||||
onClear={handleSearchClear}
|
||||
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
|
||||
value={searchInput}
|
||||
maxLength={50}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<div style={{ width: 80 }} />
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
|
||||
<div className="p-4">
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
allowClear
|
||||
onClear={handleSearchClear}
|
||||
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
|
||||
value={searchInput}
|
||||
maxLength={50}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
<div style={{ width: 80 }} />
|
||||
</div>
|
||||
<Main id="content-container">
|
||||
<AgentsGroupList>
|
||||
{Object.entries(agentGroups).map(([group]) => (
|
||||
|
||||
@@ -45,7 +45,7 @@ export function useSystemAgents() {
|
||||
|
||||
// 如果没有远程配置或获取失败,加载本地代理
|
||||
if (resourcesPath && _agents.length === 0) {
|
||||
const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json')
|
||||
const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json', 'utf-8')
|
||||
_agents = JSON.parse(localAgentsData) as Agent[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { Input } from 'antd'
|
||||
import { Search } from 'lucide-react'
|
||||
import React, { FC, useState } from 'react'
|
||||
import { Button, Input } from 'antd'
|
||||
import { Search, SettingsIcon, X } from 'lucide-react'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import App from './App'
|
||||
import MiniAppSettings from './MiniappSettings/MiniAppSettings'
|
||||
import NewAppButton from './NewAppButton'
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
const { minapps } = useMinapps()
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||
const location = useLocation()
|
||||
|
||||
const filteredApps = search
|
||||
? minapps.filter(
|
||||
@@ -31,31 +34,53 @@ const AppsPage: FC = () => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsSettingsOpen(false)
|
||||
}, [location.key])
|
||||
|
||||
return (
|
||||
<Container onContextMenu={handleContextMenu}>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('minapp.title')}
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '30%', height: 28, borderRadius: 15 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
suffix={<Search size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<div style={{ width: 80 }} />
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
{/* <Navbar> */}
|
||||
{/* <NavbarMain> */}
|
||||
{/* {t('minapp.title')} */}
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{
|
||||
width: '30%',
|
||||
height: 28,
|
||||
borderRadius: 15,
|
||||
position: 'absolute',
|
||||
left: '50vw',
|
||||
transform: 'translateX(-50%)'
|
||||
}}
|
||||
size="small"
|
||||
variant="filled"
|
||||
suffix={<Search size={18} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
disabled={isSettingsOpen}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
className="nodrag"
|
||||
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
/>
|
||||
</div>
|
||||
{/* </NavbarMain> */}
|
||||
{/* </Navbar> */}
|
||||
<ContentContainer id="content-container">
|
||||
<AppsContainer style={{ height: containerHeight }}>
|
||||
{filteredApps.map((app) => (
|
||||
<App key={app.id} app={app} />
|
||||
))}
|
||||
<NewAppButton />
|
||||
</AppsContainer>
|
||||
{isSettingsOpen && <MiniAppSettings />}
|
||||
{!isSettingsOpen && (
|
||||
<AppsContainer style={{ height: containerHeight }}>
|
||||
{filteredApps.map((app) => (
|
||||
<App key={app.id} app={app} />
|
||||
))}
|
||||
<NewAppButton />
|
||||
</AppsContainer>
|
||||
)}
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UndoOutlined } from '@ant-design/icons' // 导入重置图标
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
setMaxKeepAliveMinapps,
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
import { Button, message, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDescription, SettingDivider, SettingGroup, SettingRowTitle, SettingTitle } from '..'
|
||||
import MiniAppIconsManager from './MiniAppIconsManager'
|
||||
|
||||
// 默认小程序缓存数量
|
||||
@@ -22,10 +22,10 @@ const DEFAULT_MAX_KEEPALIVE = 3
|
||||
|
||||
const MiniAppSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
|
||||
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
|
||||
const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || [])
|
||||
@@ -72,83 +72,87 @@ const MiniAppSettings: FC = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<Container>
|
||||
{contextHolder} {/* 添加消息上下文 */}
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.miniapps.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingTitle
|
||||
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>{t('settings.miniapps.display_title')}</span>
|
||||
<ResetButtonWrapper>
|
||||
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
|
||||
</ResetButtonWrapper>
|
||||
</SettingTitle>
|
||||
<BorderedContainer>
|
||||
<MiniAppIconsManager
|
||||
visibleMiniApps={visibleMiniApps}
|
||||
disabledMiniApps={disabledMiniApps}
|
||||
setVisibleMiniApps={setVisibleMiniApps}
|
||||
setDisabledMiniApps={setDisabledMiniApps}
|
||||
/>
|
||||
</BorderedContainer>
|
||||
<SettingDivider />
|
||||
<SettingRow style={{ height: 40, alignItems: 'center' }}>
|
||||
<SettingLabelGroup>
|
||||
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
|
||||
</SettingLabelGroup>
|
||||
<Switch
|
||||
checked={minappsOpenLinkExternal}
|
||||
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
{/* 缓存小程序数量设置 */}
|
||||
<SettingRow>
|
||||
<SettingLabelGroup>
|
||||
<SettingRowTitle>{t('settings.miniapps.cache_title')}</SettingRowTitle>
|
||||
<SettingDescription>{t('settings.miniapps.cache_description')}</SettingDescription>
|
||||
</SettingLabelGroup>
|
||||
<CacheSettingControls>
|
||||
<SliderWithResetContainer>
|
||||
<Tooltip title={t('settings.miniapps.reset_tooltip')} placement="top">
|
||||
<ResetButton onClick={handleResetCacheLimit}>
|
||||
<UndoOutlined />
|
||||
</ResetButton>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
min={1}
|
||||
max={10}
|
||||
value={maxKeepAliveMinapps}
|
||||
onChange={handleCacheChange}
|
||||
marks={{
|
||||
1: '1',
|
||||
5: '5',
|
||||
10: 'Max'
|
||||
}}
|
||||
tooltip={{ formatter: (value) => `${value}` }}
|
||||
/>
|
||||
</SliderWithResetContainer>
|
||||
</CacheSettingControls>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingLabelGroup>
|
||||
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
|
||||
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
|
||||
</SettingLabelGroup>
|
||||
<Switch
|
||||
checked={showOpenedMinappsInSidebar}
|
||||
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
<SettingTitle
|
||||
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span>{t('settings.miniapps.display_title')}</span>
|
||||
<ResetButtonWrapper>
|
||||
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
|
||||
</ResetButtonWrapper>
|
||||
</SettingTitle>
|
||||
<BorderedContainer>
|
||||
<MiniAppIconsManager
|
||||
visibleMiniApps={visibleMiniApps}
|
||||
disabledMiniApps={disabledMiniApps}
|
||||
setVisibleMiniApps={setVisibleMiniApps}
|
||||
setDisabledMiniApps={setDisabledMiniApps}
|
||||
/>
|
||||
</BorderedContainer>
|
||||
<SettingDivider />
|
||||
<SettingRow style={{ height: 40, alignItems: 'center' }}>
|
||||
<SettingLabelGroup>
|
||||
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
|
||||
</SettingLabelGroup>
|
||||
<Switch
|
||||
checked={minappsOpenLinkExternal}
|
||||
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
{/* 缓存小程序数量设置 */}
|
||||
<SettingRow>
|
||||
<SettingLabelGroup>
|
||||
<SettingRowTitle>{t('settings.miniapps.cache_title')}</SettingRowTitle>
|
||||
<SettingDescription>{t('settings.miniapps.cache_description')}</SettingDescription>
|
||||
</SettingLabelGroup>
|
||||
<CacheSettingControls>
|
||||
<SliderWithResetContainer>
|
||||
<Tooltip title={t('settings.miniapps.reset_tooltip')} placement="top">
|
||||
<ResetButton onClick={handleResetCacheLimit}>
|
||||
<UndoOutlined />
|
||||
</ResetButton>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
min={1}
|
||||
max={10}
|
||||
value={maxKeepAliveMinapps}
|
||||
onChange={handleCacheChange}
|
||||
marks={{
|
||||
1: '1',
|
||||
5: '5',
|
||||
10: 'Max'
|
||||
}}
|
||||
tooltip={{ formatter: (value) => `${value}` }}
|
||||
/>
|
||||
</SliderWithResetContainer>
|
||||
</CacheSettingControls>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingLabelGroup>
|
||||
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
|
||||
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
|
||||
</SettingLabelGroup>
|
||||
<Switch
|
||||
checked={showOpenedMinappsInSidebar}
|
||||
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow style={{ justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => navigate('/apps')}>{t('common.close')}</Button>
|
||||
</SettingRow>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
// 修改和新增样式
|
||||
const SettingRow = styled.div`
|
||||
display: flex;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Category } from '@renderer/types/cherryStore'
|
||||
import React from 'react'
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
|
||||
|
||||
// 实际的 AgentsPage 组件 - 请确保路径正确
|
||||
import AgentsPage from '../../agents/AgentsPage'
|
||||
import AppsPage from '../../apps/AppsPage'
|
||||
// import AssistantDetailsPage from '../../agents/AssistantDetailsPage'; // 示例详情页
|
||||
|
||||
// 其他分类的页面组件 (如果需要)
|
||||
// const MiniAppPagePlaceholder = ({ categoryId, subcategoryId }: { categoryId?: string; subcategoryId?: string }) => (
|
||||
// <div className="p-4">
|
||||
// MiniApp Placeholder for Category: {categoryId || 'N/A'}, Subcategory: {subcategoryId || 'N/A'}
|
||||
// </div>
|
||||
// )
|
||||
|
||||
export interface DiscoverContentProps {
|
||||
activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant"
|
||||
// selectedSubcategoryId: string
|
||||
currentCategory: Category | undefined
|
||||
}
|
||||
|
||||
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
|
||||
const location = useLocation() // To see the current path for debugging or more complex logic
|
||||
|
||||
if (!currentCategory || !activeTabId) {
|
||||
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
|
||||
}
|
||||
|
||||
if (!activeTabId && !location.pathname.startsWith('/discover/')) {
|
||||
return <Navigate to="/discover/assistant?subcategory=all" replace /> // Fallback redirect, adjust as needed
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
{/* Path for Assistant category */}
|
||||
<Route path="assistant" element={<AgentsPage />} />
|
||||
{/* Path for Mini-App category */}
|
||||
<Route path="mini-app" element={<AppsPage />} />
|
||||
|
||||
<Route path="*" element={<div>Discover Feature Not Found at {location.pathname}</div>} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiscoverContent
|
||||
@@ -0,0 +1,64 @@
|
||||
import { SubCategoryItem } from '@renderer/types/cherryStore'
|
||||
import { Badge } from '@renderer/ui/badge'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider
|
||||
} from '@renderer/ui/sidebar'
|
||||
|
||||
import { InternalCategory } from '../hooks/useDiscoverCategories'
|
||||
|
||||
interface DiscoverSidebarProps {
|
||||
activeCategory: InternalCategory | undefined
|
||||
selectedSubcategory: string
|
||||
onSelectSubcategory: (subcategoryId: string, row?: SubCategoryItem) => void
|
||||
}
|
||||
|
||||
export default function DiscoverSidebar({
|
||||
activeCategory,
|
||||
selectedSubcategory,
|
||||
onSelectSubcategory
|
||||
}: DiscoverSidebarProps) {
|
||||
if (!activeCategory) {
|
||||
return (
|
||||
<Sidebar className="absolute top-0 left-0 h-full border-r">
|
||||
<SidebarContent>
|
||||
<p className="p-4 text-sm text-gray-500">No active category selected.</p>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider className="relative h-full min-h-full w-full">
|
||||
<Sidebar className="absolute top-0 left-0 h-full border-r">
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
{activeCategory.items &&
|
||||
activeCategory.items.length > 0 &&
|
||||
activeCategory.items.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.id}>
|
||||
<SidebarMenuButton
|
||||
isActive={subItem.id === selectedSubcategory}
|
||||
onClick={() => {
|
||||
onSelectSubcategory(subItem.id, subItem)
|
||||
}}
|
||||
size="sm">
|
||||
<span className="truncate">{subItem.name}</span>
|
||||
{typeof subItem.count === 'number' && (
|
||||
<Badge variant="secondary" className="ml-auto shrink-0">
|
||||
{subItem.count}
|
||||
</Badge>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
165
src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts
Normal file
165
src/renderer/src/pages/discover/hooks/useDiscoverCategories.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Category, CherryStoreType } from '@renderer/types/cherryStore'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
// Extended Category type for internal use in hook, including path and sidebar flag
|
||||
// Export this interface so other files can import it
|
||||
export interface InternalCategory extends Category {
|
||||
path: string
|
||||
hasSidebar?: boolean // Optional: defaults to true if not specified, or handle explicitly
|
||||
}
|
||||
|
||||
// Initial category data with path and hasSidebar
|
||||
const initialCategories: InternalCategory[] = [
|
||||
{
|
||||
id: CherryStoreType.ASSISTANT,
|
||||
title: 'Assistants',
|
||||
path: 'assistant',
|
||||
hasSidebar: false,
|
||||
items: []
|
||||
},
|
||||
{
|
||||
id: CherryStoreType.MINI_APP,
|
||||
title: 'Mini Apps',
|
||||
path: 'mini-app',
|
||||
hasSidebar: false,
|
||||
items: []
|
||||
}
|
||||
// Add more categories as needed
|
||||
]
|
||||
|
||||
// Helper to find category by path
|
||||
const findCategoryByPath = (path: string | undefined): InternalCategory | undefined =>
|
||||
initialCategories.find((cat) => cat.path === path)
|
||||
|
||||
// Helper to find category by id (activeTab)
|
||||
const findCategoryById = (id: string | undefined): InternalCategory | undefined =>
|
||||
initialCategories.find((cat) => cat.id === id)
|
||||
|
||||
export function useDiscoverCategories() {
|
||||
const [categories, setCategories] = useState<InternalCategory[]>(initialCategories)
|
||||
const [activeTab, setActiveTab] = useState<string>('')
|
||||
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('all')
|
||||
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// Effect to initialize activeTab from URL path segment or navigate to default
|
||||
useEffect(() => {
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean) // e.g., ["discover", "assistant"]
|
||||
// Expects URL like /discover/:categoryPathSegment/...
|
||||
const currentCategoryPath = pathSegments.length >= 2 && pathSegments[0] === 'discover' ? pathSegments[1] : undefined
|
||||
|
||||
const categoryFromPath = findCategoryByPath(currentCategoryPath)
|
||||
|
||||
// Synchronize active tab with the category determined from the URL path.
|
||||
// If a category is found from the path, update the active tab to match its ID.
|
||||
if (categoryFromPath) {
|
||||
if (activeTab !== categoryFromPath.id) {
|
||||
setActiveTab(categoryFromPath.id)
|
||||
}
|
||||
} else if (location.pathname === '/discover' || location.pathname === '/discover/') {
|
||||
// Handle the case where the URL is the base /discover path.
|
||||
// Redirect to the first category's path to ensure a category is always selected.
|
||||
if (categories.length > 0) {
|
||||
const firstCategory = categories[0]
|
||||
if (firstCategory?.path) {
|
||||
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
|
||||
}
|
||||
}
|
||||
} else if (!currentCategoryPath && categories.length > 0 && !activeTab) {
|
||||
// Fallback for invalid or unmatched /discover/xxx URLs.
|
||||
// If the URL contains a path segment that doesn't correspond to a known category,
|
||||
// and no tab is active, redirect to the first valid category.
|
||||
const firstCategory = categories[0]
|
||||
if (firstCategory?.path) {
|
||||
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
|
||||
}
|
||||
}
|
||||
// If categoryFromPath is undefined, and it's not /discover, it means it's an invalid path like /discover/unknown
|
||||
// In this case, we don't navigate from here; ideally App.tsx has a NotFound route, or DiscoverContent shows a message.
|
||||
}, [location.pathname, categories, activeTab, navigate])
|
||||
|
||||
// Effect to initialize selectedSubcategory from URL query param or default to 'all'
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const subcategoryIdFromQuery = searchParams.get('subcategory')
|
||||
const currentCatDetails = findCategoryById(activeTab) // Use the helper here
|
||||
|
||||
if (subcategoryIdFromQuery && currentCatDetails) {
|
||||
// Check if the subcategory from query is valid for the current active category
|
||||
if (currentCatDetails.items.some((item) => item.id === subcategoryIdFromQuery)) {
|
||||
if (selectedSubcategory !== subcategoryIdFromQuery) {
|
||||
setSelectedSubcategory(subcategoryIdFromQuery)
|
||||
}
|
||||
return // Valid subcategory from URL is set, no further action needed in this effect iteration
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid subcategory in query, or if activeTab has changed and subcategory needs reset/defaulting
|
||||
if (activeTab && currentCatDetails) {
|
||||
const defaultSub = currentCatDetails.items.find((item) => item.id === 'all') || currentCatDetails.items[0]
|
||||
if (defaultSub) {
|
||||
// Ensure defaultSub exists
|
||||
// Set selectedSubcategory state first
|
||||
if (selectedSubcategory !== defaultSub.id) {
|
||||
setSelectedSubcategory(defaultSub.id)
|
||||
}
|
||||
// Then, if URL doesn't match this default, update URL to reflect the default subcategory
|
||||
// This ensures the URL is the source of truth / always consistent.
|
||||
if (!subcategoryIdFromQuery || subcategoryIdFromQuery !== defaultSub.id) {
|
||||
const newSearchParams = new URLSearchParams() // Start with clean params for this path
|
||||
newSearchParams.set('subcategory', defaultSub.id)
|
||||
// Ensure we use the current actual path from currentCatDetails if available for navigation
|
||||
// This avoids issues if location.pathname is briefly out of sync during transitions.
|
||||
const basePath = currentCatDetails.path
|
||||
? `/discover/${currentCatDetails.path}`
|
||||
: location.pathname.split('?')[0]
|
||||
navigate(`${basePath}?${newSearchParams.toString()}`, { replace: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [activeTab, location.search, categories, navigate, selectedSubcategory]) // location.pathname removed as basePath logic handles path part
|
||||
|
||||
const currentCategory = useMemo(() => {
|
||||
return findCategoryById(activeTab) // Use the helper here
|
||||
}, [activeTab]) // categories removed from deps as findCategoryById uses stable initialCategories
|
||||
|
||||
const handleSelectTab = (tabId: string) => {
|
||||
const categoryToSelect = findCategoryById(tabId)
|
||||
if (categoryToSelect && categoryToSelect.path && activeTab !== tabId) {
|
||||
navigate(`/discover/${categoryToSelect.path}?subcategory=all`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectSubcategory = (subcategoryId: string) => {
|
||||
const currentCatDetails = findCategoryById(activeTab)
|
||||
if (selectedSubcategory !== subcategoryId && currentCatDetails?.path) {
|
||||
const newSearchParams = new URLSearchParams()
|
||||
newSearchParams.set('subcategory', subcategoryId)
|
||||
navigate(`/discover/${currentCatDetails.path}?${newSearchParams.toString()}`, { replace: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure each category has an "All" subcategory (runs once on mount)
|
||||
useEffect(() => {
|
||||
setCategories((prev) =>
|
||||
prev.map((cat) => {
|
||||
if (!cat.items.some((item) => item.id === 'all')) {
|
||||
return { ...cat, items: [{ id: 'all', name: `All ${cat.title}` }, ...cat.items] }
|
||||
}
|
||||
return cat
|
||||
})
|
||||
)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
categories,
|
||||
activeTab,
|
||||
selectedSubcategory,
|
||||
currentCategory,
|
||||
handleSelectTab,
|
||||
handleSelectSubcategory,
|
||||
setActiveTab
|
||||
}
|
||||
}
|
||||
83
src/renderer/src/pages/discover/index.tsx
Normal file
83
src/renderer/src/pages/discover/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||
// import { useRuntime } from '@renderer/hooks/useRuntime' // No longer needed if resourcesPath is not used
|
||||
import { Tabs as VercelTabs } from '@renderer/ui/vercel-tabs'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
// Import Context and the main Dialog Manager component
|
||||
import DiscoverContent from './components/DiscoverContent' // Removed DiscoverContent import
|
||||
import DiscoverSidebar from './components/DiscoverSidebar'
|
||||
import { InternalCategory, useDiscoverCategories } from './hooks/useDiscoverCategories'
|
||||
|
||||
// Function to adapt categories for VercelTabs
|
||||
const adaptCategoriesForVercelTabs = (categories: InternalCategory[]) => {
|
||||
return categories.map((category) => ({
|
||||
id: category.id, // VercelTabs expects `id`
|
||||
label: category.title // VercelTabs expects `label`
|
||||
}))
|
||||
}
|
||||
|
||||
export default function DiscoverPage() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
categories,
|
||||
activeTab,
|
||||
selectedSubcategory,
|
||||
currentCategory,
|
||||
handleSelectTab,
|
||||
handleSelectSubcategory,
|
||||
setActiveTab
|
||||
} = useDiscoverCategories()
|
||||
|
||||
// Path like /discover/:categoryIdFromUrl. categoryIdFromUrl is lowercase from URL.
|
||||
const { categoryIdFromUrl } = useParams<{ categoryIdFromUrl: string }>()
|
||||
|
||||
useEffect(() => {
|
||||
const matchedCategory = categories.find((cat) => cat.id.toLowerCase() === categoryIdFromUrl?.toLowerCase())
|
||||
if (matchedCategory && activeTab !== matchedCategory.id) {
|
||||
setActiveTab(matchedCategory.id)
|
||||
}
|
||||
}, [categoryIdFromUrl, categories, activeTab, setActiveTab])
|
||||
|
||||
const vercelTabsData = adaptCategoriesForVercelTabs(categories)
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<NavbarMain className="h-auto flex-shrink-0">
|
||||
<NavbarCenter>{t('discover.title')}</NavbarCenter>
|
||||
</NavbarMain>
|
||||
|
||||
{categories.length > 0 && (
|
||||
<div className="px-4 py-2">
|
||||
<VercelTabs tabs={vercelTabsData} onTabChange={handleSelectTab} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-grow flex-row overflow-auto">
|
||||
{currentCategory?.hasSidebar && (
|
||||
<div className="w-64 flex-shrink-0 border-r">
|
||||
<DiscoverSidebar
|
||||
activeCategory={currentCategory}
|
||||
selectedSubcategory={selectedSubcategory}
|
||||
onSelectSubcategory={handleSelectSubcategory}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* {!currentCategory && categories.length > 0 && (
|
||||
<div className="w-64 flex-shrink-0 border-r p-4 text-muted-foreground">Select a category...</div>
|
||||
)} */}
|
||||
|
||||
<main className="flex-grow overflow-hidden">
|
||||
<DiscoverContent
|
||||
activeTabId={activeTab}
|
||||
// selectedSubcategoryId={selectedSubcategory}
|
||||
currentCategory={currentCategory}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
src/renderer/src/pages/discover/types.ts
Normal file
7
src/renderer/src/pages/discover/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Category } from '@renderer/types/cherryStore'
|
||||
|
||||
export interface DiscoverContextType {
|
||||
selectedSubcategory: string
|
||||
activeTabId: string
|
||||
currentCategory?: Category // currentCategory might be undefined initially
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Logger from '@renderer/config/logger'
|
||||
@@ -207,9 +207,9 @@ const FilesPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarMain>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
</NavbarMain>
|
||||
<ContentContainer id="content-container">
|
||||
<SideNav>
|
||||
{menuItems.map((item) => (
|
||||
|
||||
@@ -15,7 +15,6 @@ import styled from 'styled-components'
|
||||
|
||||
import Inputbar from './Inputbar/Inputbar'
|
||||
import Messages from './Messages/Messages'
|
||||
import Tabs from './Tabs'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -38,7 +37,7 @@ const Chat: FC<Props> = (props) => {
|
||||
const showRightTopics = showTopics && topicPosition === 'right'
|
||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
||||
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
|
||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
return `calc(100vw - ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
@@ -128,24 +127,12 @@ const Chat: FC<Props> = (props) => {
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</QuickPanelProvider>
|
||||
</Main>
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
<Tabs
|
||||
activeAssistant={assistant}
|
||||
activeTopic={props.activeTopic}
|
||||
setActiveAssistant={props.setActiveAssistant}
|
||||
setActiveTopic={props.setActiveTopic}
|
||||
position="right"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const Main = styled(Flex)`
|
||||
|
||||
167
src/renderer/src/pages/home/ChatNavbar.tsx
Normal file
167
src/renderer/src/pages/home/ChatNavbar.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Navbar } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SelectModelButton from './components/SelectModelButton'
|
||||
import UpdateAppButton from './components/UpdateAppButton'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
position: 'left' | 'right'
|
||||
}
|
||||
|
||||
const ChatNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const isFullscreen = useFullscreen()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
const { toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Function to toggle assistants with cooldown
|
||||
const handleToggleShowAssistants = useCallback(() => {
|
||||
if (showAssistants) {
|
||||
// When hiding sidebar, set cooldown
|
||||
toggleShowAssistants()
|
||||
// setTimeout(() => {
|
||||
// setSidebarHideCooldown(false)
|
||||
// }, 10000) // 10 seconds cooldown
|
||||
} else {
|
||||
// When showing sidebar, no cooldown needed
|
||||
toggleShowAssistants()
|
||||
}
|
||||
}, [showAssistants, toggleShowAssistants])
|
||||
|
||||
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
|
||||
|
||||
useShortcut('toggle_show_topics', () => {
|
||||
if (topicPosition === 'right') {
|
||||
toggleShowTopics()
|
||||
} else {
|
||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
const handleNarrowModeToggle = async () => {
|
||||
await modelGenerating()
|
||||
dispatch(setNarrowMode(!narrowMode))
|
||||
}
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
|
||||
<HStack alignItems="center">
|
||||
<NavbarIcon
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
|
||||
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
|
||||
</NavbarIcon>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
{isMac && (
|
||||
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<Search size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
{sidebarIcons.visible.includes('minapp') && (
|
||||
<MinAppsPopover>
|
||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
|
||||
<NarrowIcon>
|
||||
<LayoutGrid size={18} />
|
||||
</NarrowIcon>
|
||||
</Tooltip>
|
||||
</MinAppsPopover>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarContainer>
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
const NavbarContainer = styled.div<{ $isFullscreen: boolean; $showSidebar: boolean }>`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
min-height: var(--navbar-height);
|
||||
justify-content: space-between;
|
||||
padding-left: ${({ $showSidebar }) => (isMac ? ($showSidebar ? '10px' : '75px') : '15px')};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
export const NavbarIcon = styled.div`
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
height: 30px;
|
||||
padding: 0 7px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
&.icon-a-addchat {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-a-darkmode {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.icon-appstore {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.anticon {
|
||||
color: var(--color-icon);
|
||||
font-size: 16px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
color: var(--color-icon-white);
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default ChatNavbar
|
||||
@@ -1,18 +1,14 @@
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useChat } from '@renderer/hooks/useChat'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Chat from './Chat'
|
||||
import Navbar from './Navbar'
|
||||
import HomeTabs from './Tabs'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
import ChatNavbar from './ChatNavbar'
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { assistants } = useAssistants()
|
||||
@@ -21,12 +17,9 @@ const HomePage: FC = () => {
|
||||
const location = useLocation()
|
||||
const state = location.state
|
||||
|
||||
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
|
||||
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
useEffect(() => {
|
||||
NavigationService.setNavigate(navigate)
|
||||
}, [navigate])
|
||||
@@ -61,23 +54,8 @@ const HomePage: FC = () => {
|
||||
|
||||
return (
|
||||
<Container id="home-page">
|
||||
<Navbar
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveTopic={setActiveTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
position="left"
|
||||
/>
|
||||
<ChatNavbar activeAssistant={activeAssistant} position="left" />
|
||||
<ContentContainer id="content-container">
|
||||
{showAssistants && (
|
||||
<HomeTabs
|
||||
activeAssistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
setActiveTopic={setActiveTopic}
|
||||
position="left"
|
||||
/>
|
||||
)}
|
||||
<Chat
|
||||
assistant={activeAssistant}
|
||||
activeTopic={activeTopic}
|
||||
@@ -90,16 +68,13 @@ const HomePage: FC = () => {
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
max-width: calc(100vw - var(--sidebar-width));
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
|
||||
@@ -33,8 +33,10 @@ import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
|
||||
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
||||
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||
import { formatQuotedText } from '@renderer/utils/formats'
|
||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -50,6 +52,7 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
|
||||
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
||||
import MentionModelsInput from './MentionModelsInput'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import SettingButton from './SettingButton'
|
||||
import TokenCount from './TokenCount'
|
||||
|
||||
interface Props {
|
||||
@@ -403,12 +406,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
|
||||
const topic = getDefaultTopic(assistant.id)
|
||||
|
||||
await db.topics.add({ id: topic.id, messages: [] })
|
||||
await addAssistantMessagesToTopic({ assistant, topic })
|
||||
|
||||
// Clear previous state
|
||||
// Reset to assistant default model
|
||||
@@ -420,6 +420,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}, [addTopic, assistant, setActiveTopic, setModel])
|
||||
|
||||
const onQuote = useCallback(
|
||||
(text: string) => {
|
||||
const quotedText = formatQuotedText(text)
|
||||
setText((prevText) => {
|
||||
const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
return newText
|
||||
})
|
||||
textareaRef.current?.focus()
|
||||
},
|
||||
[resizeTextArea]
|
||||
)
|
||||
|
||||
const onPause = async () => {
|
||||
await pauseMessages()
|
||||
}
|
||||
@@ -624,21 +637,25 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
_setEstimateTokenCount(tokensCount)
|
||||
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic),
|
||||
EventEmitter.on(EVENT_NAMES.QUOTE_TEXT, (quotedText: string) => {
|
||||
setText((prevText) => {
|
||||
const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
return newText
|
||||
})
|
||||
textareaRef.current?.focus()
|
||||
})
|
||||
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic)
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [addNewTopic, resizeTextArea])
|
||||
|
||||
// 监听引用事件
|
||||
const quoteFromAnywhereRemover = window.electron?.ipcRenderer.on(
|
||||
IpcChannel.App_QuoteToMain,
|
||||
(_, selectedText: string) => onQuote(selectedText)
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsub) => unsub())
|
||||
quoteFromAnywhereRemover?.()
|
||||
}
|
||||
}, [addNewTopic, onQuote])
|
||||
|
||||
useEffect(() => {
|
||||
textareaRef.current?.focus()
|
||||
if (!document.querySelector('.topview-fullscreen-container')) {
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
}, [assistant, topic])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -723,50 +740,28 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}, [])
|
||||
|
||||
const onToggleExpended = () => {
|
||||
if (textareaHeight) {
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
textArea.style.height = 'auto'
|
||||
setTextareaHeight(undefined)
|
||||
setTimeout(() => {
|
||||
textArea.style.height = `${textArea.scrollHeight}px`
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const isExpended = !expended
|
||||
setExpend(isExpended)
|
||||
const currentlyExpanded = expended || !!textareaHeight
|
||||
const shouldExpand = !currentlyExpanded
|
||||
setExpend(shouldExpand)
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
|
||||
if (textArea) {
|
||||
if (isExpended) {
|
||||
textArea.style.height = '70vh'
|
||||
} else {
|
||||
resetHeight()
|
||||
}
|
||||
if (!textArea) return
|
||||
if (shouldExpand) {
|
||||
textArea.style.height = '70vh'
|
||||
setTextareaHeight(window.innerHeight * 0.7)
|
||||
} else {
|
||||
textArea.style.height = 'auto'
|
||||
setTextareaHeight(undefined)
|
||||
requestAnimationFrame(() => {
|
||||
if (textArea) {
|
||||
const contentHeight = textArea.scrollHeight
|
||||
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
|
||||
const resetHeight = () => {
|
||||
if (expended) {
|
||||
setExpend(false)
|
||||
}
|
||||
|
||||
setTextareaHeight(undefined)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
textArea.style.height = 'auto'
|
||||
const contentHeight = textArea.scrollHeight
|
||||
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isExpended = expended || !!textareaHeight
|
||||
const showThinkingButton = isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)
|
||||
|
||||
@@ -862,6 +857,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
ToolbarButton={ToolbarButton}
|
||||
onClick={onNewContext}
|
||||
/>
|
||||
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
{loading && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||
@@ -916,7 +912,7 @@ const InputBarContainer = styled.div`
|
||||
border: 0.5px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
margin: 14px 20px;
|
||||
margin: 16px 16px;
|
||||
margin-top: 0;
|
||||
border-radius: 15px;
|
||||
padding-top: 6px; // 为拖动手柄留出空间
|
||||
@@ -949,11 +945,11 @@ const Textarea = styled(TextArea)`
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
resize: none !important;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: height 0.2s ease;
|
||||
&.ant-input {
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -968,6 +964,9 @@ const Toolbar = styled.div`
|
||||
margin-bottom: 4px;
|
||||
height: 30px;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
|
||||
@@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
||||
newList.push({
|
||||
label: t('settings.mcp.addServer') + '...',
|
||||
icon: <Plus />,
|
||||
action: () => navigate('/settings/mcp')
|
||||
action: () => navigate('/mcp-servers')
|
||||
})
|
||||
|
||||
newList.unshift({
|
||||
|
||||
@@ -74,29 +74,33 @@ const MentionModelsButton: FC<Props> = ({ ref, mentionModels, onMentionModel, To
|
||||
}
|
||||
|
||||
providers.forEach((p) => {
|
||||
const providerModels = p.models
|
||||
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
||||
.filter((m) => !pinnedModels.includes(getModelUniqId(m)))
|
||||
.map((m) => ({
|
||||
label: (
|
||||
<>
|
||||
<ProviderName>{p.isSystem ? t(`provider.${p.id}`) : p.name}</ProviderName>
|
||||
<span style={{ opacity: 0.8 }}> | {m.name}</span>
|
||||
</>
|
||||
),
|
||||
description: <ModelTagsWithLabel model={m} showLabel={false} size={10} style={{ opacity: 0.8 }} />,
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m.id)} size={20}>
|
||||
{first(m.name)}
|
||||
</Avatar>
|
||||
),
|
||||
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
||||
action: () => onMentionModel(m),
|
||||
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||
}))
|
||||
const providerModels = sortBy(
|
||||
p.models
|
||||
.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
||||
.filter((m) => !pinnedModels.includes(getModelUniqId(m))),
|
||||
['group', 'name']
|
||||
)
|
||||
|
||||
if (providerModels.length > 0) {
|
||||
items.push(...sortBy(providerModels, ['label']))
|
||||
const providerModelItems = providerModels.map((m) => ({
|
||||
label: (
|
||||
<>
|
||||
<ProviderName>{p.isSystem ? t(`provider.${p.id}`) : p.name}</ProviderName>
|
||||
<span style={{ opacity: 0.8 }}> | {m.name}</span>
|
||||
</>
|
||||
),
|
||||
description: <ModelTagsWithLabel model={m} showLabel={false} size={10} style={{ opacity: 0.8 }} />,
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m.id)} size={20}>
|
||||
{first(m.name)}
|
||||
</Avatar>
|
||||
),
|
||||
filterText: (p.isSystem ? t(`provider.${p.id}`) : p.name) + m.name,
|
||||
action: () => onMentionModel(m),
|
||||
isSelected: mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
|
||||
}))
|
||||
|
||||
if (providerModelItems.length > 0) {
|
||||
items.push(...providerModelItems)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
43
src/renderer/src/pages/home/Inputbar/SettingButton.tsx
Normal file
43
src/renderer/src/pages/home/Inputbar/SettingButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Popover } from 'antd'
|
||||
import { Settings } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
|
||||
import SettingsTab from '../Tabs/SettingsTab'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const SettingButton: FC<Props> = ({ assistant, ToolbarButton }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="topLeft"
|
||||
content={<SettingsTab assistant={assistant} onClose={handleClose} />}
|
||||
trigger="click"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
styles={{
|
||||
body: {
|
||||
padding: '4px 2px 4px 2px'
|
||||
}
|
||||
}}>
|
||||
<ToolbarButton type="text">
|
||||
<Settings size={18} />
|
||||
</ToolbarButton>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingButton
|
||||
@@ -1,6 +1,6 @@
|
||||
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||
import { Tooltip } from 'antd'
|
||||
import React from 'react'
|
||||
import React, { memo, useCallback, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface CitationTooltipProps {
|
||||
@@ -13,56 +13,62 @@ interface CitationTooltipProps {
|
||||
}
|
||||
|
||||
const CitationTooltip: React.FC<CitationTooltipProps> = ({ children, citation }) => {
|
||||
let hostname = ''
|
||||
try {
|
||||
hostname = new URL(citation.url).hostname
|
||||
} catch {
|
||||
hostname = citation.url
|
||||
}
|
||||
const hostname = useMemo(() => {
|
||||
try {
|
||||
return new URL(citation.url).hostname
|
||||
} catch {
|
||||
return citation.url
|
||||
}
|
||||
}, [citation.url])
|
||||
|
||||
const sourceTitle = useMemo(() => {
|
||||
return citation.title?.trim() || hostname
|
||||
}, [citation.title, hostname])
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
window.open(citation.url, '_blank', 'noopener,noreferrer')
|
||||
}, [citation.url])
|
||||
|
||||
// 自定义悬浮卡片内容
|
||||
const tooltipContent = (
|
||||
<TooltipContentWrapper>
|
||||
<TooltipHeader onClick={() => window.open(citation.url, '_blank')}>
|
||||
<Favicon hostname={hostname} alt={citation.title || hostname} />
|
||||
<TooltipTitle title={citation.title || hostname}>{citation.title || hostname}</TooltipTitle>
|
||||
</TooltipHeader>
|
||||
{citation.content && <TooltipBody>{citation.content}</TooltipBody>}
|
||||
<TooltipFooter onClick={() => window.open(citation.url, '_blank')}>{hostname}</TooltipFooter>
|
||||
</TooltipContentWrapper>
|
||||
const tooltipContent = useMemo(
|
||||
() => (
|
||||
<div>
|
||||
<TooltipHeader role="button" aria-label={`Open ${sourceTitle} in new tab`} onClick={handleClick}>
|
||||
<Favicon hostname={hostname} alt={sourceTitle} />
|
||||
<TooltipTitle role="heading" aria-level={3} title={sourceTitle}>
|
||||
{sourceTitle}
|
||||
</TooltipTitle>
|
||||
</TooltipHeader>
|
||||
{citation.content?.trim() && (
|
||||
<TooltipBody role="article" aria-label="Citation content">
|
||||
{citation.content}
|
||||
</TooltipBody>
|
||||
)}
|
||||
<TooltipFooter role="button" aria-label={`Visit ${hostname}`} onClick={handleClick}>
|
||||
{hostname}
|
||||
</TooltipFooter>
|
||||
</div>
|
||||
),
|
||||
[citation.content, hostname, handleClick, sourceTitle]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledTooltip
|
||||
title={tooltipContent}
|
||||
<Tooltip
|
||||
overlay={tooltipContent}
|
||||
placement="top"
|
||||
arrow={false}
|
||||
overlayInnerStyle={{
|
||||
backgroundColor: 'var(--color-background-mute)',
|
||||
border: '1px solid var(--color-border)',
|
||||
padding: 0,
|
||||
borderRadius: '8px'
|
||||
color="var(--color-background-mute)"
|
||||
styles={{
|
||||
body: {
|
||||
border: '1px solid var(--color-border)',
|
||||
padding: '12px',
|
||||
borderRadius: '8px'
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
</StyledTooltip>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
// 使用styled-components来自定义Tooltip的样式,包括箭头
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
.ant-tooltip-arrow {
|
||||
.ant-tooltip-arrow-content {
|
||||
background-color: var(--color-background-1);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TooltipContentWrapper = styled.div`
|
||||
padding: 12px;
|
||||
background-color: var(--color-background-soft);
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const TooltipHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -108,4 +114,4 @@ const TooltipFooter = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default CitationTooltip
|
||||
export default memo(CitationTooltip)
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'katex/dist/katex.min.css'
|
||||
import 'katex/dist/contrib/copy-tex'
|
||||
import 'katex/dist/contrib/mhchem'
|
||||
|
||||
import ImageViewer from '@renderer/components/ImageViewer'
|
||||
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@@ -12,7 +13,7 @@ import { findCitationInChildren, getCodeBlockId } from '@renderer/utils/markdown
|
||||
import { isEmpty } from 'lodash'
|
||||
import { type FC, memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown, { type Components } from 'react-markdown'
|
||||
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
// @ts-ignore rehype-mathjax is not typed
|
||||
import rehypeMathjax from 'rehype-mathjax'
|
||||
@@ -22,7 +23,6 @@ import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import CodeBlock from './CodeBlock'
|
||||
import ImagePreview from './ImagePreview'
|
||||
import Link from './Link'
|
||||
|
||||
const ALLOWED_ELEMENTS =
|
||||
@@ -83,11 +83,21 @@ const Markdown: FC<Props> = ({ block }) => {
|
||||
code: (props: any) => (
|
||||
<CodeBlock {...props} id={getCodeBlockId(props?.node?.position?.start)} onSave={onSaveCodeBlock} />
|
||||
),
|
||||
img: ImagePreview,
|
||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />
|
||||
img: (props: any) => <ImageViewer style={{ maxWidth: 500, maxHeight: 500 }} {...props} />,
|
||||
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
|
||||
p: (props) => {
|
||||
const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
|
||||
if (hasImage) return <div {...props} />
|
||||
return <p {...props} />
|
||||
}
|
||||
} as Partial<Components>
|
||||
}, [onSaveCodeBlock])
|
||||
|
||||
const urlTransform = useCallback((value: string) => {
|
||||
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
|
||||
return defaultUrlTransform(value)
|
||||
}, [])
|
||||
|
||||
// if (role === 'user' && !renderInputMessageAsMarkdown) {
|
||||
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
|
||||
// }
|
||||
@@ -103,6 +113,7 @@ const Markdown: FC<Props> = ({ block }) => {
|
||||
className="markdown"
|
||||
components={components}
|
||||
disallowedElements={DISALLOWED_ELEMENTS}
|
||||
urlTransform={urlTransform}
|
||||
remarkRehypeOptions={{
|
||||
footnoteLabel: t('common.footnotes'),
|
||||
footnoteLabelTagName: 'h4',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user