Compare commits
107 Commits
8f2299b875
...
4869898173
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4869898173 | ||
|
|
f062c56de4 | ||
|
|
4c9bd02f8e | ||
|
|
241cb0c0d8 | ||
|
|
6a57973864 | ||
|
|
369f629206 | ||
|
|
09a8f83650 | ||
|
|
40912eaaf4 | ||
|
|
0d236a94ab | ||
|
|
3a936e0f26 | ||
|
|
81a35d129d | ||
|
|
e2d0c3bbce | ||
|
|
12c9d810a2 | ||
|
|
b18b161094 | ||
|
|
32da853f27 | ||
|
|
bf51a0b5c6 | ||
|
|
ae71a7be9e | ||
|
|
0fb6795833 | ||
|
|
dd6d228760 | ||
|
|
8c5999dc82 | ||
|
|
31b0fbf775 | ||
|
|
39fe583030 | ||
|
|
5c19695e21 | ||
|
|
6e4610e337 | ||
|
|
eb2439b90c | ||
|
|
a4a0980cd3 | ||
|
|
7d99765589 | ||
|
|
5f6cf1bd66 | ||
|
|
02930a2793 | ||
|
|
b31b1c7908 | ||
|
|
d0ee764732 | ||
|
|
01cd10b364 | ||
|
|
29ba156b9a | ||
|
|
e541c7b429 | ||
|
|
5f4142f0c4 | ||
|
|
95bbc70c93 | ||
|
|
4add56ae6a | ||
|
|
16f87537a2 | ||
|
|
7f05626a8f | ||
|
|
2094e2201a | ||
|
|
e0fcdf43c5 | ||
|
|
affc866c17 | ||
|
|
799267049f | ||
|
|
cb8d47a17b | ||
|
|
c494288f7b | ||
|
|
2c3f89dbde | ||
|
|
4721a660fa | ||
|
|
6aaa3def0d | ||
|
|
045708d9b3 | ||
|
|
9ffe92d378 | ||
|
|
7159481217 | ||
|
|
d07e136037 | ||
|
|
b38a9c954a | ||
|
|
7139d5093a | ||
|
|
9e283d6930 | ||
|
|
2457c7b818 | ||
|
|
c9a4e12765 | ||
|
|
7bd644451b | ||
|
|
5a00bdcbc6 | ||
|
|
3c958c3d11 | ||
|
|
1d5ace0fb2 | ||
|
|
f8fce871da | ||
|
|
de76d3fedc | ||
|
|
b2c6662192 | ||
|
|
bf8a7c01b0 | ||
|
|
fb8ed35b59 | ||
|
|
7c4d81c108 | ||
|
|
7199f73e06 | ||
|
|
869e56b53c | ||
|
|
f99851fb6b | ||
|
|
c94450db44 | ||
|
|
195ef92acc | ||
|
|
a67370426b | ||
|
|
9d35205681 | ||
|
|
98087e50db | ||
|
|
bedac4f59d | ||
|
|
aba3874797 | ||
|
|
3383280726 | ||
|
|
0c13e708b9 | ||
|
|
bc77c423b3 | ||
|
|
4821756301 | ||
|
|
78290ca70e | ||
|
|
7feeb07624 | ||
|
|
93e28ed916 | ||
|
|
b4aaf052fe | ||
|
|
b37e0389fc | ||
|
|
e1ebe069a5 | ||
|
|
d73912ee3b | ||
|
|
f81c7c7a6c | ||
|
|
5a7bcd5997 | ||
|
|
09a347cae4 | ||
|
|
266f909045 | ||
|
|
bad2f15c1f | ||
|
|
e3115d00bf | ||
|
|
0c0ccf3d11 | ||
|
|
2076e6f998 | ||
|
|
b49d80b78d | ||
|
|
ab5e830ed1 | ||
|
|
e0eca97053 | ||
|
|
d175212d9a | ||
|
|
642ce160a1 | ||
|
|
574d02a8c9 | ||
|
|
7764507d74 | ||
|
|
fa8bf61532 | ||
|
|
30e8cef9cc | ||
|
|
1a2861e81a | ||
|
|
653e5d82ed |
19
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
19
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@ -6,7 +6,8 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thank you for taking the time to fill out this bug report!
|
||||||
|
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@ -15,9 +16,11 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Before submitting an issue, please make sure you have completed the following steps
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
options:
|
options:
|
||||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
- label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out the issue title correctly.
|
- label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
|
||||||
|
required: true
|
||||||
|
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -45,8 +48,8 @@ body:
|
|||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Bug Description
|
label: Bug Description
|
||||||
description: A clear and concise description of what the bug is
|
description: Please be as detailed as possible when describing the problem. Please provide screenshots or screen recordings whenever possible to help us better understand the issue.
|
||||||
placeholder: Tell us what happened...
|
placeholder: Tell us what happened... (Remember to attach screenshots/recordings if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -54,12 +57,14 @@ body:
|
|||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps To Reproduce
|
label: Steps To Reproduce
|
||||||
description: Steps to reproduce the behavior
|
description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately. Please include screenshots or screen recordings for each step when possible.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
|
Remember to attach screenshots/recordings for each step when possible!
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -82,4 +87,4 @@ body:
|
|||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Context
|
label: Additional Context
|
||||||
description: Add any other context about the problem here
|
description: Anything that gives us a better understanding of the problem you're experiencing
|
||||||
|
|||||||
29
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
29
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@ -6,7 +6,8 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for taking the time to suggest a new feature!
|
Thank you for taking the time to submit a feature request!
|
||||||
|
Before submitting this issue, please make sure you have reviewed the [Project Roadmap](https://docs.cherry-ai.com/cherrystudio/planning) and the [Feature Overview](https://docs.cherry-ai.com/cherrystudio/preview).
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@ -15,9 +16,13 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Before submitting an issue, please make sure you have completed the following steps
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
options:
|
options:
|
||||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
- label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out the issue title correctly.
|
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
|
||||||
|
required: true
|
||||||
|
- label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck."
|
||||||
|
required: true
|
||||||
|
- label: The latest version of Cherry Studio does not include the feature I am suggesting.
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -44,28 +49,28 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: Is your feature request related to a problem?
|
label: Is your feature request related to an existing issue?
|
||||||
description: A clear and concise description of what the problem is
|
description: Please briefly describe the problem you are experiencing. If possible, include screenshots or recordings to help illustrate the current situation or pain points.
|
||||||
placeholder: I'm always frustrated when...
|
placeholder: I often feel frustrated because... (Remember to attach screenshots/recordings if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: solution
|
id: solution
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the solution you'd like
|
label: Desired Solution
|
||||||
description: A clear and concise description of what you want to happen
|
description: Please briefly describe what you would like to happen. You can include mockups, screenshots, or screen recordings to better illustrate your proposed solution.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: alternatives
|
id: alternatives
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe alternatives you've considered
|
label: Alternative Solutions
|
||||||
description: A clear and concise description of any alternative solutions or features you've considered
|
description: Please briefly describe any alternative solutions or features you have considered. Feel free to include screenshots or mockups of alternative approaches.
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Context
|
label: Additional Information
|
||||||
description: Add any other context or screenshots about the feature request here
|
description: Add any other context, screenshots, mockups or recordings that can help us better understand your feature request.
|
||||||
|
|||||||
28
.github/ISSUE_TEMPLATE/2_question.yml
vendored
28
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@ -1,12 +1,12 @@
|
|||||||
name: ❓ Question
|
name: ❓ Discussion & Questions
|
||||||
description: Ask a question or seek help
|
description: Seeking help, discussing issues, asking questions, etc...
|
||||||
title: '[Question]: '
|
title: '[Discussion]: '
|
||||||
labels: ['question']
|
labels: ['question']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
|
Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@ -15,9 +15,11 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Before submitting an issue, please make sure you have completed the following steps
|
Before submitting an issue, please make sure you have completed the following steps
|
||||||
options:
|
options:
|
||||||
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar.
|
- label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out the issue title correctly.
|
- label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
|
||||||
|
required: true
|
||||||
|
- label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features.
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -45,8 +47,8 @@ body:
|
|||||||
id: question
|
id: question
|
||||||
attributes:
|
attributes:
|
||||||
label: Your Question
|
label: Your Question
|
||||||
description: Please describe your question in detail
|
description: Please describe your issue in detail. Include screenshots or screen recordings whenever possible to help us better understand your question.
|
||||||
placeholder: Please explain your question as clearly as possible...
|
placeholder: Please explain your issue as clearly as possible...(Remember to attach screenshots/recordings if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@ -54,23 +56,23 @@ body:
|
|||||||
id: context
|
id: context
|
||||||
attributes:
|
attributes:
|
||||||
label: Context
|
label: Context
|
||||||
description: Please provide some background information to help us better understand your question
|
description: Please provide some background information to help us better understand your question. Screenshots or recordings of your current setup or situation can be very helpful.
|
||||||
placeholder: "For example: use case, solutions you've tried, etc."
|
placeholder: "For example: use case, solutions you've tried, etc. Don't forget to include relevant screenshots/recordings!"
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Information
|
label: Additional Information
|
||||||
description: Any other relevant information, screenshots, or code examples
|
description: Any other relevant information, screenshots, recordings, or code examples that can help us better assist you
|
||||||
render: shell
|
render: shell
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: priority
|
id: priority
|
||||||
attributes:
|
attributes:
|
||||||
label: Priority
|
label: Priority
|
||||||
description: How urgent is this question for you?
|
description: How urgent is this issue for you?
|
||||||
options:
|
options:
|
||||||
- Low (Can wait)
|
- Low (Review when available)
|
||||||
- Medium (Would like a response soon)
|
- Medium (Would like a response soon)
|
||||||
- High (Blocking progress)
|
- High (Blocking progress)
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
name: 🐛 错误报告
|
name: 🐛 错误报告 (中文)
|
||||||
description: 创建一个报告以帮助我们改进
|
description: 创建一个报告以帮助我们改进
|
||||||
title: '[错误]: '
|
title: '[错误]: '
|
||||||
labels: ['bug']
|
labels: ['bug']
|
||||||
@ -7,17 +7,20 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
感谢您花时间填写此错误报告!
|
感谢您花时间填写此错误报告!
|
||||||
|
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue 检查清单
|
label: 提交前检查
|
||||||
description: |
|
description: |
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
options:
|
options:
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
required: true
|
required: true
|
||||||
- label: 正确填写了 Issue 标题。
|
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||||
|
required: true
|
||||||
|
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -45,7 +48,7 @@ body:
|
|||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: 错误描述
|
label: 错误描述
|
||||||
description: 清晰简洁地描述错误是什么
|
description: 描述问题时请尽可能详细
|
||||||
placeholder: 告诉我们发生了什么...
|
placeholder: 告诉我们发生了什么...
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@ -54,7 +57,7 @@ body:
|
|||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: 重现步骤
|
label: 重现步骤
|
||||||
description: 重现行为的步骤
|
description: 提供详细的重现步骤,以便于我们可以准确地重现问题
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. 转到 '...'
|
1. 转到 '...'
|
||||||
2. 点击 '....'
|
2. 点击 '....'
|
||||||
@ -82,4 +85,4 @@ body:
|
|||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: 附加信息
|
label: 附加信息
|
||||||
description: 在此添加有关问题的任何其他上下文
|
description: 任何能让我们对你所遇到的问题有更多了解的东西
|
||||||
@ -1,4 +1,4 @@
|
|||||||
name: 💡 功能建议
|
name: 💡 功能建议 (中文)
|
||||||
description: 为项目提出新的想法
|
description: 为项目提出新的想法
|
||||||
title: '[功能]: '
|
title: '[功能]: '
|
||||||
labels: ['enhancement']
|
labels: ['enhancement']
|
||||||
@ -7,17 +7,22 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
感谢您花时间提出新的功能建议!
|
感谢您花时间提出新的功能建议!
|
||||||
|
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue 检查清单
|
label: 提交前检查
|
||||||
description: |
|
description: |
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
options:
|
options:
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
required: true
|
required: true
|
||||||
- label: 正确填写了 Issue 标题。
|
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
|
||||||
|
required: true
|
||||||
|
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||||
|
required: true
|
||||||
|
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
@ -44,7 +49,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: 您的功能建议是否与某个问题相关?
|
label: 您的功能建议是否与某个问题/issue相关?
|
||||||
description: 请简明扼要地描述您遇到的问题
|
description: 请简明扼要地描述您遇到的问题
|
||||||
placeholder: 我总是感到沮丧,因为...
|
placeholder: 我总是感到沮丧,因为...
|
||||||
validations:
|
validations:
|
||||||
@ -1,6 +1,6 @@
|
|||||||
name: ❓ 提问
|
name: ❓ 讨论 & 提问 (中文)
|
||||||
description: 提出一个问题或寻求帮助
|
description: 寻求帮助、讨论问题、提出疑问等...
|
||||||
title: '[问题]: '
|
title: '[讨论]: '
|
||||||
labels: ['question']
|
labels: ['question']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@ -15,9 +15,9 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||||
options:
|
options:
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 Issue,但没有找到类似的问题。
|
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||||
required: true
|
required: true
|
||||||
- label: 正确填写了 Issue 标题。
|
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
14
README.md
14
README.md
@ -60,6 +60,20 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
|
|||||||
- 📝 Complete Markdown Rendering
|
- 📝 Complete Markdown Rendering
|
||||||
- 🤲 Easy Content Sharing
|
- 🤲 Easy Content Sharing
|
||||||
|
|
||||||
|
# 📝 TODO
|
||||||
|
|
||||||
|
- [x] Quick popup (read clipboard, quick question, explain, translate, summarize)
|
||||||
|
- [x] Comparison of multi-model answers
|
||||||
|
- [x] Support login using SSO provided by service providers
|
||||||
|
- [ ] All models support networking (in development...)
|
||||||
|
- [ ] Launch of the first official version
|
||||||
|
- [ ] Plugin functionality (JavaScript)
|
||||||
|
- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base)
|
||||||
|
- [ ] iOS & Android client
|
||||||
|
- [ ] AI notes
|
||||||
|
- [ ] Voice input and output (AI call)
|
||||||
|
- [ ] Data backup supports custom backup content
|
||||||
|
|
||||||
# 🖥️ Develop
|
# 🖥️ Develop
|
||||||
|
|
||||||
## IDE Setup
|
## IDE Setup
|
||||||
|
|||||||
@ -61,6 +61,20 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
|
|||||||
- 📝 完全な Markdown レンダリング
|
- 📝 完全な Markdown レンダリング
|
||||||
- 🤲 簡単な共有機能
|
- 🤲 簡単な共有機能
|
||||||
|
|
||||||
|
# 📝 TODO
|
||||||
|
|
||||||
|
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
|
||||||
|
- [x] 複数モデルの回答の比較
|
||||||
|
- [x] サービスプロバイダーが提供するSSOを使用したログインをサポート
|
||||||
|
- [ ] すべてのモデルがネットワークをサポート(開発中...)
|
||||||
|
- [ ] 最初の公式バージョンのリリース
|
||||||
|
- [ ] プラグイン機能(JavaScript)
|
||||||
|
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
|
||||||
|
- [ ] iOS & Android クライアント
|
||||||
|
- [ ] AIノート
|
||||||
|
- [ ] 音声入出力(AIコール)
|
||||||
|
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
|
||||||
|
|
||||||
# 🖥️ 開発
|
# 🖥️ 開発
|
||||||
|
|
||||||
## IDEの設定
|
## IDEの設定
|
||||||
|
|||||||
@ -61,6 +61,20 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
|
|||||||
- 📝 完整的 Markdown 渲染
|
- 📝 完整的 Markdown 渲染
|
||||||
- 🤲 便捷的内容分享功能
|
- 🤲 便捷的内容分享功能
|
||||||
|
|
||||||
|
# 📝 待辦事項
|
||||||
|
|
||||||
|
- [x] 快捷彈窗 (讀取剪貼簿、快速提問、解釋、翻譯、總結)
|
||||||
|
- [x] 多模型回答對比
|
||||||
|
- [x] 支援使用服務供應商提供的 SSO 進行登入
|
||||||
|
- [ ] 全部模型支援連網(開發中...)
|
||||||
|
- [ ] 推出第一個正式版
|
||||||
|
- [ ] 插件功能(JavaScript)
|
||||||
|
- [ ] 瀏覽器插件(劃詞翻譯、總結、新增至知識庫)
|
||||||
|
- [ ] iOS & Android 客戶端
|
||||||
|
- [ ] AI 筆記
|
||||||
|
- [ ] 語音輸入輸出(AI 通話)
|
||||||
|
- [ ] 資料備份支援自訂備份內容
|
||||||
|
|
||||||
# 🖥️ 开发
|
# 🖥️ 开发
|
||||||
|
|
||||||
## IDE 设置
|
## IDE 设置
|
||||||
|
|||||||
@ -80,11 +80,11 @@ afterPack: scripts/after-pack.js
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
增加服务商 LM Studio、魔搭、Perplexity、无问芯穹、DMXAPI
|
消息分组支持网格模式
|
||||||
提及功能支持上下按键循环选择模型
|
知识库支持多选
|
||||||
小程序增加小艺
|
知识库添加目录支持显示进度
|
||||||
增加Notion连接检测功能
|
知识库支持 DRAFTS, EPUB、代码等
|
||||||
编辑模型弹窗搜索模型时,同时搜索模型的名字和ID
|
知识库支持调节匹配度阈值
|
||||||
编辑模型弹窗增加推理模型筛选按钮
|
添加 NotebookLM, Coze 小程序
|
||||||
修复思考模型思考时间显示错误
|
增加话题提示词
|
||||||
修复部分模型翻译出错
|
OpenRouter 支持 Web 搜索
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "0.9.24",
|
"version": "0.9.27",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@ -94,6 +94,7 @@
|
|||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/node": "^18.19.9",
|
"@types/node": "^18.19.9",
|
||||||
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
@ -137,7 +138,7 @@
|
|||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"rehype-mathjax": "^6.0.0",
|
"rehype-mathjax": "^7.0.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
|||||||
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||||
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||||
|
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||||
export const bookExts = ['.epub']
|
export const bookExts = ['.epub']
|
||||||
export const textExts = [
|
export const textExts = [
|
||||||
'.txt', // 普通文本文件
|
'.txt', // 普通文本文件
|
||||||
@ -89,7 +90,10 @@ export const textExts = [
|
|||||||
'.groovy', // Gradle 构建文件
|
'.groovy', // Gradle 构建文件
|
||||||
'.kts', // Kotlin Script 文件
|
'.kts', // Kotlin Script 文件
|
||||||
'.java', // Java 代码文件
|
'.java', // Java 代码文件
|
||||||
'.cs' // C# 代码文件
|
'.cs', // C# 代码文件
|
||||||
|
'.cpp', // C++ 代码文件
|
||||||
|
'.c', // C++ 代码文件
|
||||||
|
'.h' // C++ 头文件
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ZOOM_SHORTCUTS = [
|
export const ZOOM_SHORTCUTS = [
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import { app, BrowserWindow } from 'electron'
|
import { app } from 'electron'
|
||||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
@ -46,15 +46,13 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
new TrayService()
|
new TrayService()
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
const mainWindow = windowService.getMainWindow()
|
||||||
// dock icon is clicked and there are no other windows open.
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
windowService.createMainWindow()
|
windowService.createMainWindow()
|
||||||
} else {
|
} else {
|
||||||
windowService.showMainWindow()
|
windowService.showMainWindow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
registerShortcuts(mainWindow)
|
registerShortcuts(mainWindow)
|
||||||
|
|
||||||
registerIpc(mainWindow, app)
|
registerIpc(mainWindow, app)
|
||||||
@ -68,12 +66,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
|
|
||||||
// Listen for second instance
|
// Listen for second instance
|
||||||
app.on('second-instance', () => {
|
app.on('second-instance', () => {
|
||||||
const mainWindow = BrowserWindow.getAllWindows()[0]
|
windowService.showMainWindow()
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.isMinimized() && mainWindow.restore()
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
|||||||
22
src/main/loader/draftsExportLoader.ts
Normal file
22
src/main/loader/draftsExportLoader.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as fs from 'node:fs'
|
||||||
|
|
||||||
|
import { JsonLoader } from '@llm-tools/embedjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drafts 应用导出的笔记文件加载器
|
||||||
|
* 原始文件是一个 JSON 数组。每条笔记只保留 content、tags、modified_at 三个字段
|
||||||
|
*/
|
||||||
|
export class DraftsExportLoader extends JsonLoader {
|
||||||
|
constructor(filePath: string) {
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
const rawJson = JSON.parse(fileContent) as any[]
|
||||||
|
const json = rawJson.map((item) => {
|
||||||
|
return {
|
||||||
|
content: item.content?.replace(/\n/g, '<br>'),
|
||||||
|
tags: item.tags,
|
||||||
|
modified_at: item.created_at
|
||||||
|
}
|
||||||
|
})
|
||||||
|
super({ object: json })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,17 +1,18 @@
|
|||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
|
|
||||||
import { LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
|
||||||
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
|
||||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||||
import { LoaderReturn } from '@shared/config/types'
|
import { LoaderReturn } from '@shared/config/types'
|
||||||
import { FileType, KnowledgeBaseParams } from '@types'
|
import { FileType, KnowledgeBaseParams } from '@types'
|
||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
|
import { DraftsExportLoader } from './draftsExportLoader'
|
||||||
import { EpubLoader } from './epubLoader'
|
import { EpubLoader } from './epubLoader'
|
||||||
import { OdLoader, OdType } from './odLoader'
|
import { OdLoader, OdType } from './odLoader'
|
||||||
|
|
||||||
// embedjs内置loader类型
|
// embedjs内置loader类型
|
||||||
const commonExts = ['.pdf', '.csv', '.json', '.docx', '.pptx', '.xlsx', '.md']
|
const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
|
||||||
|
|
||||||
export async function addOdLoader(
|
export async function addOdLoader(
|
||||||
ragApplication: RAGApplication,
|
ragApplication: RAGApplication,
|
||||||
@ -89,7 +90,19 @@ export async function addFileLoader(
|
|||||||
} as LoaderReturn
|
} as LoaderReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DraftsExport类型 (file.ext会自动转换成小写)
|
||||||
|
if (['.draftsexport'].includes(file.ext)) {
|
||||||
|
const loaderReturn = await ragApplication.addLoader(new DraftsExportLoader(file.path) as any, forceReload)
|
||||||
|
return {
|
||||||
|
entriesAdded: loaderReturn.entriesAdded,
|
||||||
|
uniqueId: loaderReturn.uniqueId,
|
||||||
|
uniqueIds: [loaderReturn.uniqueId],
|
||||||
|
loaderType: loaderReturn.loaderType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fileContent = fs.readFileSync(file.path, 'utf-8')
|
const fileContent = fs.readFileSync(file.path, 'utf-8')
|
||||||
|
|
||||||
// HTML类型
|
// HTML类型
|
||||||
if (['.html', '.htm'].includes(file.ext)) {
|
if (['.html', '.htm'].includes(file.ext)) {
|
||||||
const loaderReturn = await ragApplication.addLoader(
|
const loaderReturn = await ragApplication.addLoader(
|
||||||
@ -108,6 +121,27 @@ export async function addFileLoader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON类型
|
||||||
|
if (['.json'].includes(file.ext)) {
|
||||||
|
let jsonObject = {}
|
||||||
|
let jsonParsed = true
|
||||||
|
try {
|
||||||
|
jsonObject = JSON.parse(fileContent)
|
||||||
|
} catch (error) {
|
||||||
|
jsonParsed = false
|
||||||
|
Logger.warn('[KnowledgeBase] failed parsing json file, failling back to text processing:', file.path, error)
|
||||||
|
}
|
||||||
|
if (jsonParsed) {
|
||||||
|
const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject }))
|
||||||
|
return {
|
||||||
|
entriesAdded: loaderReturn.entriesAdded,
|
||||||
|
uniqueId: loaderReturn.uniqueId,
|
||||||
|
uniqueIds: [loaderReturn.uniqueId],
|
||||||
|
loaderType: loaderReturn.loaderType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 文本类型
|
// 文本类型
|
||||||
const loaderReturn = await ragApplication.addLoader(
|
const loaderReturn = await ragApplication.addLoader(
|
||||||
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
class KnowledgeService {
|
class KnowledgeService {
|
||||||
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
|
||||||
|
|
||||||
@ -83,10 +85,23 @@ class KnowledgeService {
|
|||||||
): Promise<LoaderReturn> => {
|
): Promise<LoaderReturn> => {
|
||||||
const ragApplication = await this.getRagApplication(base)
|
const ragApplication = await this.getRagApplication(base)
|
||||||
|
|
||||||
|
const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => {
|
||||||
|
const mainWindow = windowService.getMainWindow()
|
||||||
|
mainWindow?.webContents.send(base.id, (processedFiles / totalFiles) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
if (item.type === 'directory') {
|
if (item.type === 'directory') {
|
||||||
const directory = item.content as string
|
const directory = item.content as string
|
||||||
const files = getAllFiles(directory)
|
const files = getAllFiles(directory)
|
||||||
const loaderPromises = files.map((file) => addFileLoader(ragApplication, file, base, forceReload))
|
const totalFiles = files.length
|
||||||
|
let processedFiles = 0
|
||||||
|
const loaderPromises = files.map(async (file) => {
|
||||||
|
const result = await addFileLoader(ragApplication, file, base, forceReload)
|
||||||
|
processedFiles++
|
||||||
|
|
||||||
|
sendDirectoryProcessingPercent(totalFiles, processedFiles)
|
||||||
|
return result
|
||||||
|
})
|
||||||
const loaderResults = await Promise.all(loaderPromises)
|
const loaderResults = await Promise.all(loaderPromises)
|
||||||
const uniqueIds = loaderResults.map((result) => result.uniqueId)
|
const uniqueIds = loaderResults.map((result) => result.uniqueId)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -47,8 +47,8 @@ function formatShortcutKey(shortcut: string[]): string {
|
|||||||
|
|
||||||
function handleZoom(delta: number) {
|
function handleZoom(delta: number) {
|
||||||
return (window: BrowserWindow) => {
|
return (window: BrowserWindow) => {
|
||||||
const currentZoom = window.webContents.getZoomFactor()
|
const currentZoom = configManager.getZoomFactor()
|
||||||
const newZoom = currentZoom + delta
|
const newZoom = Number((currentZoom + delta).toFixed(1))
|
||||||
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
if (newZoom >= 0.1 && newZoom <= 5.0) {
|
||||||
window.webContents.setZoomFactor(newZoom)
|
window.webContents.setZoomFactor(newZoom)
|
||||||
configManager.setZoomFactor(newZoom)
|
configManager.setZoomFactor(newZoom)
|
||||||
@ -70,10 +70,12 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
|
|||||||
return accelerator
|
return accelerator
|
||||||
.map((key) => {
|
.map((key) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
case 'Command':
|
||||||
|
return 'CommandOrControl'
|
||||||
case 'Control':
|
case 'Control':
|
||||||
return 'CommandOrControl'
|
return 'Control'
|
||||||
case 'Ctrl':
|
case 'Ctrl':
|
||||||
return 'CommandOrControl'
|
return 'Control'
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
return 'Up'
|
return 'Up'
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
@ -110,7 +112,9 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerShortcuts(window: BrowserWindow) {
|
export function registerShortcuts(window: BrowserWindow) {
|
||||||
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
window.once('ready-to-show', () => {
|
||||||
|
window.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||||
|
})
|
||||||
|
|
||||||
const register = () => {
|
const register = () => {
|
||||||
if (window.isDestroyed()) return
|
if (window.isDestroyed()) return
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export class WindowService {
|
|||||||
|
|
||||||
public createMainWindow(): BrowserWindow {
|
public createMainWindow(): BrowserWindow {
|
||||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.show()
|
||||||
return this.mainWindow
|
return this.mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,17 +249,32 @@ export class WindowService {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
mainWindow.hide()
|
mainWindow.hide()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
this.mainWindow = null
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||||
|
this.miniWindow.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public showMainWindow() {
|
public showMainWindow() {
|
||||||
if (this.mainWindow) {
|
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||||
|
this.miniWindow.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
if (this.mainWindow.isMinimized()) {
|
if (this.mainWindow.isMinimized()) {
|
||||||
return this.mainWindow.restore()
|
this.mainWindow.restore()
|
||||||
}
|
}
|
||||||
this.mainWindow.show()
|
this.mainWindow.show()
|
||||||
this.mainWindow.focus()
|
this.mainWindow.focus()
|
||||||
} else {
|
} else {
|
||||||
this.createMainWindow()
|
this.mainWindow = this.createMainWindow()
|
||||||
|
this.mainWindow.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +285,10 @@ export class WindowService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectionMenuWindow) {
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
|
this.mainWindow.hide()
|
||||||
|
}
|
||||||
|
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||||
this.selectionMenuWindow.hide()
|
this.selectionMenuWindow.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,10 @@ export function getAllFiles(dirPath: string, arrayOfFiles: FileType[] = []): Fil
|
|||||||
const files = fs.readdirSync(dirPath)
|
const files = fs.readdirSync(dirPath)
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
if (file.startsWith('.')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const fullPath = path.join(dirPath, file)
|
const fullPath = path.join(dirPath, file)
|
||||||
if (fs.statSync(fullPath).isDirectory()) {
|
if (fs.statSync(fullPath).isDirectory()) {
|
||||||
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)
|
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)
|
||||||
|
|||||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@ -119,6 +119,9 @@ declare global {
|
|||||||
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
||||||
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string>
|
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string>
|
||||||
}
|
}
|
||||||
|
shell: {
|
||||||
|
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
@ -104,6 +104,9 @@ const api = {
|
|||||||
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
||||||
decrypt: (encryptedData: string, iv: string, secretKey: string) =>
|
decrypt: (encryptedData: string, iv: string, secretKey: string) =>
|
||||||
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
|
||||||
|
},
|
||||||
|
shell: {
|
||||||
|
openExternal: shell.openExternal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||||
|
<title>Cherry Studio</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import TopViewContainer from './components/TopView'
|
|||||||
import AntdProvider from './context/AntdProvider'
|
import AntdProvider from './context/AntdProvider'
|
||||||
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
|
||||||
import { ThemeProvider } from './context/ThemeProvider'
|
import { ThemeProvider } from './context/ThemeProvider'
|
||||||
|
import NavigationHandler from './handler/NavigationHandler'
|
||||||
import AgentsPage from './pages/agents/AgentsPage'
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
import FilesPage from './pages/files/FilesPage'
|
import FilesPage from './pages/files/FilesPage'
|
||||||
@ -28,6 +29,8 @@ function App(): JSX.Element {
|
|||||||
<PersistGate loading={null} persistor={persistor}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<TopViewContainer>
|
<TopViewContainer>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
<NavigationHandler />
|
||||||
|
{/* 添加导航处理组件 */}
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
|||||||
BIN
src/renderer/src/assets/images/apps/abacus.webp
Normal file
BIN
src/renderer/src/assets/images/apps/abacus.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/renderer/src/assets/images/apps/dify.webp
Normal file
BIN
src/renderer/src/assets/images/apps/dify.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/renderer/src/assets/images/apps/lambdachat.webp
Normal file
BIN
src/renderer/src/assets/images/apps/lambdachat.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/renderer/src/assets/images/apps/lechat.png
Normal file
BIN
src/renderer/src/assets/images/apps/lechat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/renderer/src/assets/images/apps/wpslingxi.webp
Normal file
BIN
src/renderer/src/assets/images/apps/wpslingxi.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@ -64,6 +64,10 @@
|
|||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(+ ul) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
|||||||
@ -46,23 +46,28 @@ const DragableList: FC<Props<any>> = ({
|
|||||||
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||||
<Droppable droppableId="droppable" {...droppableProps}>
|
<Droppable droppableId="droppable" {...droppableProps}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
|
||||||
{list.map((item, index) => {
|
{list.map((item, index) => {
|
||||||
const id = item.id || item
|
const id = item.id || item
|
||||||
return (
|
return (
|
||||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
|
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div
|
<div
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
style={{
|
||||||
|
...listStyle,
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
marginBottom: 8
|
||||||
|
}}>
|
||||||
{children(item, index)}
|
{children(item, index)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable react/no-unknown-property */
|
/* eslint-disable react/no-unknown-property */
|
||||||
import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
|
import { AppLogo } from '@renderer/config/env'
|
||||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
import { useBridge } from '@renderer/hooks/useBridge'
|
import { useBridge } from '@renderer/hooks/useBridge'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
@ -41,7 +42,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MinApp.onClose = onClose
|
MinApp.onClose = onClose
|
||||||
|
const openDevTools = () => {
|
||||||
|
if (webviewRef.current) {
|
||||||
|
webviewRef.current.openDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
const onReload = () => {
|
const onReload = () => {
|
||||||
if (webviewRef.current) {
|
if (webviewRef.current) {
|
||||||
webviewRef.current.src = app.url
|
webviewRef.current.src = app.url
|
||||||
@ -49,14 +54,17 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onOpenLink = () => {
|
const onOpenLink = () => {
|
||||||
window.api.openWebsite(app.url)
|
if (webviewRef.current) {
|
||||||
|
const currentUrl = webviewRef.current.getURL()
|
||||||
|
window.api.openWebsite(currentUrl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTogglePin = () => {
|
const onTogglePin = () => {
|
||||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
||||||
updatePinnedMinapps(newPinned)
|
updatePinnedMinapps(newPinned)
|
||||||
}
|
}
|
||||||
|
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||||
const Title = () => {
|
const Title = () => {
|
||||||
return (
|
return (
|
||||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||||
@ -75,6 +83,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
<ExportOutlined />
|
<ExportOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{isInDevelopment && (
|
||||||
|
<Button onClick={openDevTools}>
|
||||||
|
<CodeOutlined />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button onClick={() => onClose()}>
|
<Button onClick={() => onClose()}>
|
||||||
<CloseOutlined />
|
<CloseOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
@ -236,6 +249,10 @@ export default class MinApp {
|
|||||||
await delay(0)
|
await delay(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!app.logo) {
|
||||||
|
app.logo = AppLogo
|
||||||
|
}
|
||||||
|
|
||||||
MinApp.app = app
|
MinApp.app = app
|
||||||
store.dispatch(setMinappShow(true))
|
store.dispatch(setMinappShow(true))
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import App from '@renderer/pages/apps/App'
|
|||||||
import { Popover } from 'antd'
|
import { Popover } from 'antd'
|
||||||
import { Empty } from 'antd'
|
import { Empty } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useState, useEffect } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -26,19 +26,19 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 100);
|
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 100)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
setMaxHeight(window.innerHeight - 100);
|
setMaxHeight(window.innerHeight - 100)
|
||||||
};
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize)
|
||||||
};
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<PopoverContent maxHeight={maxHeight}>
|
<PopoverContent maxHeight={maxHeight}>
|
||||||
@ -74,9 +74,9 @@ const PopoverContent = styled(Scrollbar)<{ maxHeight: number }>`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const AppsContainer = styled.div`
|
const AppsContainer = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(90px, 1fr));
|
grid-template-columns: repeat(6, minmax(90px, 1fr));
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export default MinAppsPopover
|
export default MinAppsPopover
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Input, Modal } from 'antd'
|
import { Input, Modal } from 'antd'
|
||||||
import { TextAreaProps } from 'antd/es/input'
|
import { TextAreaProps } from 'antd/es/input'
|
||||||
import { useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Box } from '../Layout'
|
import { Box } from '../Layout'
|
||||||
import { TopView } from '../TopView'
|
import { TopView } from '../TopView'
|
||||||
@ -27,6 +27,7 @@ const PromptPopupContainer: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = useState(defaultValue)
|
const [value, setValue] = useState(defaultValue)
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
|
const textAreaRef = useRef<any>(null)
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@ -41,17 +42,35 @@ const PromptPopupContainer: React.FC<Props> = ({
|
|||||||
resolve(null)
|
resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAfterOpenChange = (visible: boolean) => {
|
||||||
|
if (visible) {
|
||||||
|
const textArea = textAreaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.focus()
|
||||||
|
const length = textArea.value.length
|
||||||
|
textArea.setSelectionRange(length, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PromptPopup.hide = onCancel
|
PromptPopup.hide = onCancel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered>
|
<Modal
|
||||||
|
title={title}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
afterOpenChange={handleAfterOpenChange}
|
||||||
|
centered>
|
||||||
<Box mb={8}>{message}</Box>
|
<Box mb={8}>{message}</Box>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
ref={textAreaRef}
|
||||||
placeholder={inputPlaceholder}
|
placeholder={inputPlaceholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
autoFocus
|
|
||||||
onPressEnter={onOk}
|
onPressEnter={onOk}
|
||||||
rows={1}
|
rows={1}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
|||||||
@ -51,6 +51,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
|||||||
setTimeout(resizeTextArea, 0)
|
setTimeout(resizeTextArea, 0)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleAfterOpenChange = (visible: boolean) => {
|
||||||
|
if (visible) {
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.focus()
|
||||||
|
const length = textArea.value.length
|
||||||
|
textArea.setSelectionRange(length, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextEditPopup.hide = onCancel
|
TextEditPopup.hide = onCancel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -65,6 +76,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
|||||||
onOk={onOk}
|
onOk={onOk}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
|
afterOpenChange={handleAfterOpenChange}
|
||||||
centered>
|
centered>
|
||||||
<TextArea
|
<TextArea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ const Sidebar: FC = () => {
|
|||||||
|
|
||||||
const onOpenDocs = () => {
|
const onOpenDocs = () => {
|
||||||
MinApp.start({
|
MinApp.start({
|
||||||
|
id: 'docs',
|
||||||
name: t('docs.title'),
|
name: t('docs.title'),
|
||||||
url: 'https://docs.cherry-ai.com/',
|
url: 'https://docs.cherry-ai.com/',
|
||||||
logo: AppLogo
|
logo: AppLogo
|
||||||
@ -77,9 +78,11 @@ const Sidebar: FC = () => {
|
|||||||
</AppsContainer>
|
</AppsContainer>
|
||||||
)}
|
)}
|
||||||
</MainMenusContainer>
|
</MainMenusContainer>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus>
|
||||||
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<Icon onClick={onOpenDocs}>
|
<Icon
|
||||||
|
onClick={onOpenDocs}
|
||||||
|
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -93,8 +96,14 @@ const Sidebar: FC = () => {
|
|||||||
</Icon>
|
</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
<StyledLink
|
||||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
onClick={async () => {
|
||||||
|
if (minappShow) {
|
||||||
|
await MinApp.close()
|
||||||
|
}
|
||||||
|
await to(isLocalAi ? '/settings/assistant' : '/settings/provider')
|
||||||
|
}}>
|
||||||
|
<Icon className={pathname.startsWith('/settings') && !minappShow ? 'active' : ''}>
|
||||||
<i className="iconfont icon-setting" />
|
<i className="iconfont icon-setting" />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
@ -108,10 +117,11 @@ const MainMenus: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { sidebarIcons } = useSettings()
|
const { sidebarIcons } = useSettings()
|
||||||
|
const { minappShow } = useRuntime()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
|
||||||
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
assistants: <i className="iconfont icon-chat" />,
|
assistants: <i className="iconfont icon-chat" />,
|
||||||
@ -139,7 +149,13 @@ const MainMenus: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink onClick={() => navigate(path)}>
|
<StyledLink
|
||||||
|
onClick={async () => {
|
||||||
|
if (minappShow) {
|
||||||
|
await MinApp.close()
|
||||||
|
}
|
||||||
|
navigate(path)
|
||||||
|
}}>
|
||||||
<Icon className={isActive}>{iconMap[icon]}</Icon>
|
<Icon className={isActive}>{iconMap[icon]}</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -150,6 +166,7 @@ const MainMenus: FC = () => {
|
|||||||
const PinnedApps: FC = () => {
|
const PinnedApps: FC = () => {
|
||||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { minappShow } = useRuntime()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
||||||
@ -164,11 +181,12 @@ const PinnedApps: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
const isActive = minappShow && MinApp.app?.id === app.id
|
||||||
return (
|
return (
|
||||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink>
|
<StyledLink>
|
||||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||||
<Icon onClick={() => MinApp.start(app)}>
|
<Icon onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
|
||||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
|
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
|
||||||
|
import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url'
|
||||||
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
|
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
|
||||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
||||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
||||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
|
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
|
||||||
|
import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url'
|
||||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
|
import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
|
||||||
|
import DifyAppLogo from '@renderer/assets/images/apps/dify.webp?url'
|
||||||
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
|
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
|
||||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
|
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
|
||||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
|
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
|
||||||
@ -15,9 +18,12 @@ import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
|||||||
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
|
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
|
||||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
||||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url'
|
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url'
|
||||||
|
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
|
||||||
|
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
|
||||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
||||||
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
|
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
|
||||||
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
|
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
|
||||||
|
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
||||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
||||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
||||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
||||||
@ -27,12 +33,11 @@ import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png?url'
|
|||||||
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
||||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
||||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
||||||
|
import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url'
|
||||||
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
||||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url'
|
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url'
|
||||||
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
|
|
||||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
||||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
||||||
import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url'
|
|
||||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
||||||
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url'
|
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url'
|
||||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url'
|
import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url'
|
||||||
@ -171,7 +176,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'perplexity',
|
id: 'perplexity',
|
||||||
name: 'perplexity',
|
name: 'Perplexity',
|
||||||
logo: PerplexityAppLogo,
|
logo: PerplexityAppLogo,
|
||||||
url: 'https://www.perplexity.ai/'
|
url: 'https://www.perplexity.ai/'
|
||||||
},
|
},
|
||||||
@ -306,7 +311,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
id: 'notebooklm',
|
id: 'notebooklm',
|
||||||
name: 'NotebookLM',
|
name: 'NotebookLM',
|
||||||
logo: NotebookLMAppLogo,
|
logo: NotebookLMAppLogo,
|
||||||
url: 'https://notebooklm.google.com/',
|
url: 'https://notebooklm.google.com/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'coze',
|
id: 'coze',
|
||||||
@ -314,6 +319,41 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
logo: CozeAppLogo,
|
logo: CozeAppLogo,
|
||||||
url: 'https://www.coze.com/space',
|
url: 'https://www.coze.com/space',
|
||||||
bodered: true
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dify',
|
||||||
|
name: 'Dify',
|
||||||
|
logo: DifyAppLogo,
|
||||||
|
url: 'https://cloud.dify.ai/apps',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wpslingxi',
|
||||||
|
name: 'WPS灵犀',
|
||||||
|
logo: WPSLingXiLogo,
|
||||||
|
url: 'https://copilot.wps.cn/',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lechat',
|
||||||
|
name: 'LeChat',
|
||||||
|
logo: LeChatLogo,
|
||||||
|
url: 'https://chat.mistral.ai/chat',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'abacus',
|
||||||
|
name: 'Abacus',
|
||||||
|
logo: AbacusLogo,
|
||||||
|
url: 'https://apps.abacus.ai/chatllm',
|
||||||
|
bodered: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lambdachat',
|
||||||
|
name: 'Lambda Chat',
|
||||||
|
logo: LambdaChatLogo,
|
||||||
|
url: 'https://lambda.chat/',
|
||||||
|
bodered: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -150,7 +150,8 @@ const visionAllowedModels = [
|
|||||||
'gpt-4o(?:-[\\w-]+)?',
|
'gpt-4o(?:-[\\w-]+)?',
|
||||||
'chatgpt-4o(?:-[\\w-]+)?',
|
'chatgpt-4o(?:-[\\w-]+)?',
|
||||||
'o1(?:-[\\w-]+)?',
|
'o1(?:-[\\w-]+)?',
|
||||||
'deepseek-vl(?:[\\w-]+)?'
|
'deepseek-vl(?:[\\w-]+)?',
|
||||||
|
'kimi-latest'
|
||||||
]
|
]
|
||||||
|
|
||||||
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
|
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
|
||||||
@ -161,10 +162,9 @@ export const VISION_REGEX = new RegExp(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
|
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|janus/i
|
||||||
export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\breasoner\b.*|.*-[rR]\d+.*)$/i
|
export const REASONING_REGEX = /^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*)$/i
|
||||||
|
|
||||||
export const EMBEDDING_REGEX =
|
export const EMBEDDING_REGEX = /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
|
||||||
/(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
|
|
||||||
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
|
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
|
||||||
|
|
||||||
export function getModelLogo(modelId: string) {
|
export function getModelLogo(modelId: string) {
|
||||||
@ -178,6 +178,7 @@ export function getModelLogo(modelId: string) {
|
|||||||
pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark,
|
pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark,
|
||||||
jina: isLight ? JinaModelLogo : JinaModelLogoDark,
|
jina: isLight ? JinaModelLogo : JinaModelLogoDark,
|
||||||
abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
|
abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
|
||||||
|
minimax: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
|
||||||
o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
|
o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
|
||||||
o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
|
o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
|
||||||
'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark,
|
'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark,
|
||||||
@ -194,13 +195,16 @@ export function getModelLogo(modelId: string) {
|
|||||||
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
|
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
|
||||||
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
|
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
|
||||||
qwen: isLight ? QwenModelLogo : QwenModelLogoDark,
|
qwen: isLight ? QwenModelLogo : QwenModelLogoDark,
|
||||||
qwq: isLight ? QwenModelLogo : QwenModelLogoDark,
|
'qwq-': isLight ? QwenModelLogo : QwenModelLogoDark,
|
||||||
|
'qvq-': isLight ? QwenModelLogo : QwenModelLogoDark,
|
||||||
|
Omni: isLight ? QwenModelLogo : QwenModelLogoDark,
|
||||||
gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,
|
gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,
|
||||||
'yi-': isLight ? YiModelLogo : YiModelLogoDark,
|
'yi-': isLight ? YiModelLogo : YiModelLogoDark,
|
||||||
llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,
|
llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,
|
||||||
mixtral: isLight ? MistralModelLogo : MistralModelLogo,
|
mixtral: isLight ? MistralModelLogo : MistralModelLogo,
|
||||||
mistral: isLight ? MistralModelLogo : MistralModelLogoDark,
|
mistral: isLight ? MistralModelLogo : MistralModelLogoDark,
|
||||||
moonshot: isLight ? MoonshotModelLogo : MoonshotModelLogoDark,
|
moonshot: isLight ? MoonshotModelLogo : MoonshotModelLogoDark,
|
||||||
|
kimi: isLight ? MoonshotModelLogo : MoonshotModelLogoDark,
|
||||||
phi: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,
|
phi: isLight ? MicrosoftModelLogo : MicrosoftModelLogoDark,
|
||||||
baichuan: isLight ? BaichuanModelLogo : BaichuanModelLogoDark,
|
baichuan: isLight ? BaichuanModelLogo : BaichuanModelLogoDark,
|
||||||
claude: isLight ? ClaudeModelLogo : ClaudeModelLogoDark,
|
claude: isLight ? ClaudeModelLogo : ClaudeModelLogoDark,
|
||||||
@ -1326,34 +1330,70 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
|||||||
],
|
],
|
||||||
dmxapi: [
|
dmxapi: [
|
||||||
{
|
{
|
||||||
id: 'gpt-3.5-turbo',
|
id: 'Qwen/Qwen2.5-7B-Instruct',
|
||||||
provider: 'dmxapi',
|
provider: 'dmxapi',
|
||||||
name: 'GPT-3.5-Turbo',
|
name: 'Qwen/Qwen2.5-7B-Instruct',
|
||||||
group: 'OpenAI'
|
group: '免费模型'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ERNIE-Speed-128K',
|
||||||
|
provider: 'dmxapi',
|
||||||
|
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',
|
id: 'gpt-4o',
|
||||||
provider: 'dmxapi',
|
provider: 'dmxapi',
|
||||||
name: 'GPT-4o',
|
name: 'gpt-4o',
|
||||||
group: 'OpenAI'
|
group: 'OpenAI'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4o-mini',
|
id: 'gpt-4o-mini',
|
||||||
provider: 'dmxapi',
|
provider: 'dmxapi',
|
||||||
name: 'GPT-4o-Mini',
|
name: 'gpt-4o-mini',
|
||||||
group: 'OpenAI'
|
group: 'OpenAI'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deepseek-reasoner',
|
id: 'DMXAPI-DeepSeek-R1',
|
||||||
provider: 'dmxapi',
|
provider: 'dmxapi',
|
||||||
name: 'DeepSeek Reasoner',
|
name: 'DMXAPI-DeepSeek-R1',
|
||||||
group: 'DeepSeek'
|
group: 'DeepSeek'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deepseek-chat',
|
id: 'DMXAPI-DeepSeek-V3',
|
||||||
provider: 'dmxapi',
|
provider: 'dmxapi',
|
||||||
name: 'DeepSeek Chat',
|
name: 'DMXAPI-DeepSeek-V3',
|
||||||
group: 'DeepSeek'
|
group: 'DeepSeek'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-3-5-sonnet-20241022',
|
||||||
|
provider: 'dmxapi',
|
||||||
|
name: 'claude-3-5-sonnet-20241022',
|
||||||
|
group: 'Claude'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gemini-2.0-flash',
|
||||||
|
provider: 'dmxapi',
|
||||||
|
name: 'gemini-2.0-flash',
|
||||||
|
group: 'Gemini'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
perplexity: [
|
perplexity: [
|
||||||
@ -1563,6 +1603,10 @@ export function isEmbeddingModel(model: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.provider === 'doubao') {
|
||||||
|
return EMBEDDING_REGEX.test(model.name)
|
||||||
|
}
|
||||||
|
|
||||||
return EMBEDDING_REGEX.test(model.id) || model.type?.includes('embedding') || false
|
return EMBEDDING_REGEX.test(model.id) || model.type?.includes('embedding') || false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1571,6 +1615,10 @@ export function isVisionModel(model: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.provider === 'doubao') {
|
||||||
|
return VISION_REGEX.test(model.name) || model.type?.includes('vision') || false
|
||||||
|
}
|
||||||
|
|
||||||
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1579,6 +1627,10 @@ export function isReasoningModel(model: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.provider === 'doubao') {
|
||||||
|
return REASONING_REGEX.test(model.name) || model.type?.includes('reasoning') || false
|
||||||
|
}
|
||||||
|
|
||||||
return REASONING_REGEX.test(model.id) || model.type?.includes('reasoning') || false
|
return REASONING_REGEX.test(model.id) || model.type?.includes('reasoning') || false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1631,6 +1683,16 @@ export function isWebSearchModel(model: Model): boolean {
|
|||||||
return model?.id?.startsWith('glm-4-')
|
return model?.id?.startsWith('glm-4-')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (provider.id === 'dashscope') {
|
||||||
|
const models = ['qwen-turbo', 'qwen-max', 'qwen-plus']
|
||||||
|
// matches id like qwen-max-0919, qwen-max-latest
|
||||||
|
return models.some((i) => model.id.startsWith(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.id === 'openrouter') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1643,6 +1705,21 @@ export function getOpenAIWebSearchParams(assistant: Assistant, model: Model): Re
|
|||||||
return { enable_enhancement: true }
|
return { enable_enhancement: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (model.provider === 'dashscope') {
|
||||||
|
return {
|
||||||
|
enable_search: true,
|
||||||
|
search_options: {
|
||||||
|
forced_search: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.provider === 'openrouter') {
|
||||||
|
return {
|
||||||
|
plugins: [{ id: 'web' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools: webSearchTools
|
tools: webSearchTools
|
||||||
}
|
}
|
||||||
|
|||||||
@ -212,13 +212,13 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
dmxapi: {
|
dmxapi: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.dmxapi.com'
|
url: 'https://www.dmxapi.cn'
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://dmxapi.com/',
|
official: 'https://www.dmxapi.cn/register?aff=bwwY',
|
||||||
apiKey: 'https://www.dmxapi.com/token',
|
apiKey: 'https://www.dmxapi.cn/register?aff=bwwY',
|
||||||
docs: 'https://dmxapi.com/models.html#code-block',
|
docs: 'https://dmxapi.cn/models.html#code-block',
|
||||||
models: 'https://www.dmxapi.com/pricing'
|
models: 'https://www.dmxapi.cn/pricing'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
perplexity: {
|
perplexity: {
|
||||||
@ -526,7 +526,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://cloud.baidu.com/',
|
official: 'https://cloud.baidu.com/',
|
||||||
apiKey: 'https://cloud.baidu.com/console/qianfan/apikey',
|
apiKey: 'https://console.bce.baidu.com/iam/#/iam/apikey/list',
|
||||||
docs: 'https://cloud.baidu.com/doc/index.html',
|
docs: 'https://cloud.baidu.com/doc/index.html',
|
||||||
models: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu'
|
models: 'https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,5 +55,20 @@ export const TranslateLanguageOptions = [
|
|||||||
value: 'arabic',
|
value: 'arabic',
|
||||||
label: i18n.t('languages.arabic'),
|
label: i18n.t('languages.arabic'),
|
||||||
emoji: '🇸🇦'
|
emoji: '🇸🇦'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'german',
|
||||||
|
label: i18n.t('languages.german'),
|
||||||
|
emoji: '🇩🇪'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const translateLanguageOptions = (): typeof TranslateLanguageOptions => {
|
||||||
|
return TranslateLanguageOptions.map((option) => {
|
||||||
|
return {
|
||||||
|
value: option.value,
|
||||||
|
label: i18n.t(`languages.${option.value}`),
|
||||||
|
emoji: option.emoji
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
|||||||
|
|
||||||
const mappedLanguage = languageMap[language] || language
|
const mappedLanguage = languageMap[language] || language
|
||||||
|
|
||||||
code = code.trimEnd()
|
code = code?.trimEnd() ?? ''
|
||||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FileType, KnowledgeItem, Topic } from '@renderer/types'
|
import { FileType, KnowledgeItem, Topic, TranslateHistory } from '@renderer/types'
|
||||||
import { Dexie, type EntityTable } from 'dexie'
|
import { Dexie, type EntityTable } from 'dexie'
|
||||||
|
|
||||||
// Database declaration (move this to its own module also)
|
// Database declaration (move this to its own module also)
|
||||||
@ -7,6 +7,7 @@ export const db = new Dexie('CherryStudio') as Dexie & {
|
|||||||
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
||||||
settings: EntityTable<{ id: string; value: any }, 'id'>
|
settings: EntityTable<{ id: string; value: any }, 'id'>
|
||||||
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
|
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
|
||||||
|
translate_history: EntityTable<TranslateHistory, 'id'>
|
||||||
}
|
}
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
@ -26,4 +27,12 @@ db.version(3).stores({
|
|||||||
knowledge_notes: '&id, baseId, type, content, created_at, updated_at'
|
knowledge_notes: '&id, baseId, type, content, created_at, updated_at'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
db.version(4).stores({
|
||||||
|
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
|
||||||
|
topics: '&id, messages',
|
||||||
|
settings: '&id, value',
|
||||||
|
knowledge_notes: '&id, baseId, type, content, created_at, updated_at',
|
||||||
|
translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt'
|
||||||
|
})
|
||||||
|
|
||||||
export default db
|
export default db
|
||||||
|
|||||||
17
src/renderer/src/handler/NavigationHandler.tsx
Normal file
17
src/renderer/src/handler/NavigationHandler.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
const NavigationHandler: React.FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
useHotkeys(
|
||||||
|
'meta+, ! ctrl+,',
|
||||||
|
function () {
|
||||||
|
navigate('/settings/provider')
|
||||||
|
},
|
||||||
|
{ splitKey: '!' }
|
||||||
|
)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavigationHandler
|
||||||
@ -44,7 +44,7 @@ export function useAssistant(id: string) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
assistant,
|
assistant,
|
||||||
model: assistant?.model ?? defaultModel,
|
model: assistant?.model ?? assistant?.defaultModel ?? defaultModel,
|
||||||
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
||||||
removeTopic: (topic: Topic) => {
|
removeTopic: (topic: Topic) => {
|
||||||
TopicManager.removeTopic(topic.id)
|
TopicManager.removeTopic(topic.id)
|
||||||
|
|||||||
@ -198,6 +198,27 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || []
|
return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取目录处理进度
|
||||||
|
const getDirectoryProcessingPercent = (itemId?: string) => {
|
||||||
|
const [percent, setPercent] = useState<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!itemId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanup = window.electron.ipcRenderer.on(itemId, (_, progressingPercent: number) => {
|
||||||
|
setPercent(progressingPercent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}, [itemId])
|
||||||
|
|
||||||
|
return percent
|
||||||
|
}
|
||||||
|
|
||||||
// 清除已完成的项目
|
// 清除已完成的项目
|
||||||
const clearCompleted = () => {
|
const clearCompleted = () => {
|
||||||
dispatch(clearCompletedProcessing({ baseId }))
|
dispatch(clearCompletedProcessing({ baseId }))
|
||||||
@ -280,6 +301,7 @@ export const useKnowledge = (baseId: string) => {
|
|||||||
refreshItem,
|
refreshItem,
|
||||||
getProcessingStatus,
|
getProcessingStatus,
|
||||||
getProcessingItemsByType,
|
getProcessingItemsByType,
|
||||||
|
getDirectoryProcessingPercent,
|
||||||
clearCompleted,
|
clearCompleted,
|
||||||
clearAll,
|
clearAll,
|
||||||
removeItem,
|
removeItem,
|
||||||
@ -307,16 +329,22 @@ export const useKnowledgeBases = () => {
|
|||||||
|
|
||||||
// remove assistant knowledge_base
|
// remove assistant knowledge_base
|
||||||
const _assistants = assistants.map((assistant) => {
|
const _assistants = assistants.map((assistant) => {
|
||||||
if (assistant.knowledge_base?.id === baseId) {
|
if (assistant.knowledge_bases?.find((kb) => kb.id === baseId)) {
|
||||||
return { ...assistant, knowledge_base: undefined }
|
return {
|
||||||
|
...assistant,
|
||||||
|
knowledge_bases: assistant.knowledge_bases.filter((kb) => kb.id !== baseId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return assistant
|
return assistant
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove agent knowledge_base
|
// remove agent knowledge_base
|
||||||
const _agents = agents.map((agent) => {
|
const _agents = agents.map((agent) => {
|
||||||
if (agent.knowledge_base?.id === baseId) {
|
if (agent.knowledge_bases?.find((kb) => kb.id === baseId)) {
|
||||||
return { ...agent, knowledge_base: undefined }
|
return {
|
||||||
|
...agent,
|
||||||
|
knowledge_bases: agent.knowledge_bases.filter((kb) => kb.id !== baseId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return agent
|
return agent
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { useProviders } from './useProvider'
|
import { useProviders } from './useProvider'
|
||||||
|
|
||||||
export function useModel(id?: string) {
|
export function useModel(id?: string, providerId?: string) {
|
||||||
const { providers } = useProviders()
|
const { providers } = useProviders()
|
||||||
const allModels = providers.map((p) => p.models).flat()
|
const allModels = providers.map((p) => p.models).flat()
|
||||||
return allModels.find((m) => m.id === id)
|
return allModels.find((m) => {
|
||||||
|
if (providerId) {
|
||||||
|
return m.id === id && m.provider === providerId
|
||||||
|
} else {
|
||||||
|
return m.id === id
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,7 @@
|
|||||||
"settings.reasoning_effort.high": "high",
|
"settings.reasoning_effort.high": "high",
|
||||||
"settings.reasoning_effort.low": "low",
|
"settings.reasoning_effort.low": "low",
|
||||||
"settings.reasoning_effort.medium": "medium",
|
"settings.reasoning_effort.medium": "medium",
|
||||||
|
"settings.reasoning_effort.off": "off",
|
||||||
"settings.reasoning_effort.tip": "Only supports reasoning models",
|
"settings.reasoning_effort.tip": "Only supports reasoning models",
|
||||||
"title": "Assistants"
|
"title": "Assistants"
|
||||||
},
|
},
|
||||||
@ -120,6 +121,8 @@
|
|||||||
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
|
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
|
||||||
"settings.max_tokens.confirm": "Enable max tokens limit",
|
"settings.max_tokens.confirm": "Enable max tokens limit",
|
||||||
"settings.max_tokens.confirm_content": "Enable max tokens limit, affects the length of the result. Need to consider the context limit of the model, otherwise an error will be reported",
|
"settings.max_tokens.confirm_content": "Enable max tokens limit, affects the length of the result. Need to consider the context limit of the model, otherwise an error will be reported",
|
||||||
|
"settings.thought_auto_collapse": "Automatically Collapse Thought Content",
|
||||||
|
"settings.thought_auto_collapse.tip": "Automatically collapse thought content after thinking ends",
|
||||||
"suggestions.title": "Suggested Questions",
|
"suggestions.title": "Suggested Questions",
|
||||||
"thinking": "Thinking",
|
"thinking": "Thinking",
|
||||||
"topics.auto_rename": "Auto Rename",
|
"topics.auto_rename": "Auto Rename",
|
||||||
@ -136,10 +139,13 @@
|
|||||||
"topics.pinned": "Pinned Topics",
|
"topics.pinned": "Pinned Topics",
|
||||||
"topics.title": "Topics",
|
"topics.title": "Topics",
|
||||||
"topics.unpinned": "Unpinned Topics",
|
"topics.unpinned": "Unpinned Topics",
|
||||||
|
"topics.delete.shortcut": "Hold {{key}} to delete directly",
|
||||||
"translate": "Translate",
|
"translate": "Translate",
|
||||||
"topics.prompt": "Topic Prompts",
|
"topics.prompt": "Topic Prompts",
|
||||||
"topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic",
|
"topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic",
|
||||||
"topics.prompt.edit.title": "Edit Topic Prompts"
|
"topics.prompt.edit.title": "Edit Topic Prompts",
|
||||||
|
"artifacts.button.openExternal": "Open in external browser",
|
||||||
|
"artifacts.preview.openExternal.error.content": "Error opening the external browser."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
@ -314,7 +320,8 @@
|
|||||||
"korean": "Korean",
|
"korean": "Korean",
|
||||||
"portuguese": "Portuguese",
|
"portuguese": "Portuguese",
|
||||||
"russian": "Russian",
|
"russian": "Russian",
|
||||||
"spanish": "Spanish"
|
"spanish": "Spanish",
|
||||||
|
"german": "German"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
"mermaid": {
|
||||||
"download": {
|
"download": {
|
||||||
@ -331,6 +338,18 @@
|
|||||||
},
|
},
|
||||||
"title": "Mermaid Diagram"
|
"title": "Mermaid Diagram"
|
||||||
},
|
},
|
||||||
|
"plantuml": {
|
||||||
|
"download": {
|
||||||
|
"png": "Download PNG",
|
||||||
|
"svg": "Download SVG",
|
||||||
|
"failed": "Download failed, please check the network"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"preview": "Preview",
|
||||||
|
"source": "Source"
|
||||||
|
},
|
||||||
|
"title": "PlantUML Diagram"
|
||||||
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"api.check.model.title": "Select the model to use for detection",
|
"api.check.model.title": "Select the model to use for detection",
|
||||||
"api.connection.failed": "Connection failed",
|
"api.connection.failed": "Connection failed",
|
||||||
@ -352,7 +371,7 @@
|
|||||||
"error.invalid.enter.model": "Please select a model",
|
"error.invalid.enter.model": "Please select a model",
|
||||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||||
"error.notion.export": "Notion import failed",
|
"error.notion.export": "Failed to export to Notion. Please check connection status and configuration according to documentation",
|
||||||
"error.notion.no_api_key": "Notion ApiKey or Notion DatabaseID is not configured",
|
"error.notion.no_api_key": "Notion ApiKey or Notion DatabaseID is not configured",
|
||||||
"group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers",
|
"group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers",
|
||||||
"group.delete.title": "Delete Group Message",
|
"group.delete.title": "Delete Group Message",
|
||||||
@ -364,6 +383,7 @@
|
|||||||
"message.multi_model_style.fold": "Fold",
|
"message.multi_model_style.fold": "Fold",
|
||||||
"message.multi_model_style.horizontal": "Horizontal",
|
"message.multi_model_style.horizontal": "Horizontal",
|
||||||
"message.multi_model_style.vertical": "Vertical",
|
"message.multi_model_style.vertical": "Vertical",
|
||||||
|
"message.multi_model_style.grid": "Grid",
|
||||||
"message.style": "Message style",
|
"message.style": "Message style",
|
||||||
"message.style.bubble": "Bubble",
|
"message.style.bubble": "Bubble",
|
||||||
"message.style.plain": "Plain",
|
"message.style.plain": "Plain",
|
||||||
@ -373,13 +393,13 @@
|
|||||||
"reset.double.confirm.title": "DATA LOST !!!",
|
"reset.double.confirm.title": "DATA LOST !!!",
|
||||||
"restore.success": "Restored successfully",
|
"restore.success": "Restored successfully",
|
||||||
"save.success.title": "Saved successfully",
|
"save.success.title": "Saved successfully",
|
||||||
"success.notion.export": "Notion import successful",
|
"success.notion.export": "Successfully exported to Notion",
|
||||||
"switch.disabled": "Please wait for the current reply to complete",
|
"switch.disabled": "Please wait for the current reply to complete",
|
||||||
"topic.added": "New topic added",
|
"topic.added": "New topic added",
|
||||||
"upgrade.success.button": "Restart",
|
"upgrade.success.button": "Restart",
|
||||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||||
"upgrade.success.title": "Upgrade successfully",
|
"upgrade.success.title": "Upgrade successfully",
|
||||||
"warn.notion.exporting": "Notion is importing, please do not import repeatedly",
|
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
||||||
"error.invalid.api.host": "Invalid API Host",
|
"error.invalid.api.host": "Invalid API Host",
|
||||||
"error.invalid.api.key": "Invalid API Key"
|
"error.invalid.api.key": "Invalid API Key"
|
||||||
},
|
},
|
||||||
@ -559,36 +579,45 @@
|
|||||||
},
|
},
|
||||||
"data.title": "Data Directory",
|
"data.title": "Data Directory",
|
||||||
"notion.api_key": "Notion API Key",
|
"notion.api_key": "Notion API Key",
|
||||||
|
"notion.api_key_placeholder": "Enter Notion API Key",
|
||||||
"notion.database_id": "Notion Database ID",
|
"notion.database_id": "Notion Database ID",
|
||||||
|
"notion.database_id_placeholder": "Enter Notion Database ID",
|
||||||
|
"notion.page_name_key": "Page Title Field Name",
|
||||||
|
"notion.page_name_key_placeholder": "Enter page title field name, default is Name",
|
||||||
"notion.title": "Notion Configuration",
|
"notion.title": "Notion Configuration",
|
||||||
|
"notion.help": "Notion Configuration Documentation",
|
||||||
"notion.check": {
|
"notion.check": {
|
||||||
"button": "Check",
|
"button": "Check",
|
||||||
"fail": "Connection failed, please check the configuration",
|
"fail": "Connection failed, please check network and Api_key and Database_id",
|
||||||
"success": "Connection successful",
|
"success": "Connection successful",
|
||||||
"error": "Connection error, please check the network",
|
"error": "Connection error, please check network configuration and Api_key and Database_id",
|
||||||
"empty_api_key": "Api_key is not configured",
|
"empty_api_key": "Api_key is not configured",
|
||||||
"empty_database_id": "Database_id is not configured"
|
"empty_database_id": "Database_id is not configured"
|
||||||
},
|
},
|
||||||
"title": "Data Settings",
|
"title": "Data Settings",
|
||||||
"webdav.autoSync": "Auto Backup",
|
"webdav": {
|
||||||
"webdav.autoSync.off": "Off",
|
"autoSync": "Auto Backup",
|
||||||
"webdav.backup.button": "Backup to WebDAV",
|
"autoSync.off": "Off",
|
||||||
"webdav.host": "WebDAV Host",
|
"backup.button": "Backup to WebDAV",
|
||||||
"webdav.host.placeholder": "http://localhost:8080",
|
"host": "WebDAV Host",
|
||||||
"webdav.hours": "Hours",
|
"host.placeholder": "http://localhost:8080",
|
||||||
"webdav.lastSync": "Last Backup",
|
"minute_interval_one": "{{count}} minute",
|
||||||
"webdav.minutes": "Minutes",
|
"minute_interval_other": "{{count}} minutes",
|
||||||
"webdav.noSync": "Waiting for next backup",
|
"hour_interval_one": "{{count}} hour",
|
||||||
"webdav.password": "WebDAV Password",
|
"hour_interval_other": "{{count}} hours",
|
||||||
"webdav.path": "WebDAV Path",
|
"lastSync": "Last Backup",
|
||||||
"webdav.path.placeholder": "/backup",
|
"noSync": "Waiting for next backup",
|
||||||
"webdav.restore.button": "Restore from WebDAV",
|
"password": "WebDAV Password",
|
||||||
"webdav.restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
"path": "WebDAV Path",
|
||||||
"webdav.restore.title": "Restore from WebDAV",
|
"path.placeholder": "/backup",
|
||||||
"webdav.syncError": "Backup Error",
|
"restore.button": "Restore from WebDAV",
|
||||||
"webdav.syncStatus": "Backup Status",
|
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
||||||
"webdav.title": "WebDAV",
|
"restore.title": "Restore from WebDAV",
|
||||||
"webdav.user": "WebDAV User"
|
"syncError": "Backup Error",
|
||||||
|
"syncStatus": "Backup Status",
|
||||||
|
"title": "WebDAV",
|
||||||
|
"user": "WebDAV User"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"display.custom.css": "Custom CSS",
|
"display.custom.css": "Custom CSS",
|
||||||
"display.custom.css.placeholder": "/* Put custom CSS here */",
|
"display.custom.css.placeholder": "/* Put custom CSS here */",
|
||||||
@ -636,6 +665,10 @@
|
|||||||
"messages.input.title": "Input Settings",
|
"messages.input.title": "Input Settings",
|
||||||
"messages.markdown_rendering_input_message": "Markdown render input message",
|
"messages.markdown_rendering_input_message": "Markdown render input message",
|
||||||
"messages.math_engine": "Math engine",
|
"messages.math_engine": "Math engine",
|
||||||
|
"messages.grid_columns": "Message grid display columns",
|
||||||
|
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||||
|
"messages.grid_popover_trigger.hover": "Hover to display",
|
||||||
|
"messages.grid_popover_trigger.click": "Click to display",
|
||||||
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||||
"messages.model.title": "Model Settings",
|
"messages.model.title": "Model Settings",
|
||||||
"messages.title": "Message Settings",
|
"messages.title": "Message Settings",
|
||||||
@ -708,7 +741,7 @@
|
|||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"alt_warning": "Mac does not support Option + letters as shortcuts",
|
"alt_warning": "On Mac, Option key combinations only work with the Space key",
|
||||||
"clear_shortcut": "Clear Shortcut",
|
"clear_shortcut": "Clear Shortcut",
|
||||||
"clear_topic": "Clear Messages",
|
"clear_topic": "Clear Messages",
|
||||||
"copy_last_message": "Copy Last Message",
|
"copy_last_message": "Copy Last Message",
|
||||||
@ -727,7 +760,8 @@
|
|||||||
"toggle_show_topics": "Toggle Topics",
|
"toggle_show_topics": "Toggle Topics",
|
||||||
"zoom_in": "Zoom In",
|
"zoom_in": "Zoom In",
|
||||||
"zoom_out": "Zoom Out",
|
"zoom_out": "Zoom Out",
|
||||||
"zoom_reset": "Reset Zoom"
|
"zoom_reset": "Reset Zoom",
|
||||||
|
"show_settings": "Open Settings"
|
||||||
},
|
},
|
||||||
"theme.auto": "Auto",
|
"theme.auto": "Auto",
|
||||||
"theme.dark": "Dark",
|
"theme.dark": "Dark",
|
||||||
@ -746,6 +780,7 @@
|
|||||||
"translate": {
|
"translate": {
|
||||||
"any.language": "Any language",
|
"any.language": "Any language",
|
||||||
"button.translate": "Translate",
|
"button.translate": "Translate",
|
||||||
|
"tooltip.newline": "Newline",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"content": "Translation will replace the original text, continue?",
|
"content": "Translation will replace the original text, continue?",
|
||||||
@ -756,7 +791,14 @@
|
|||||||
"input.placeholder": "Enter text to translate",
|
"input.placeholder": "Enter text to translate",
|
||||||
"output.placeholder": "Translation",
|
"output.placeholder": "Translation",
|
||||||
"processing": "Translation in progress...",
|
"processing": "Translation in progress...",
|
||||||
"title": "Translation"
|
"title": "Translation",
|
||||||
|
"history": {
|
||||||
|
"title": "Translation History",
|
||||||
|
"empty": "No translation history",
|
||||||
|
"clear": "Clear History",
|
||||||
|
"delete": "Delete",
|
||||||
|
"clear_description": "Clear history will delete all translation history, continue?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
"settings.reasoning_effort.high": "長い",
|
"settings.reasoning_effort.high": "長い",
|
||||||
"settings.reasoning_effort.low": "短い",
|
"settings.reasoning_effort.low": "短い",
|
||||||
"settings.reasoning_effort.medium": "中程度",
|
"settings.reasoning_effort.medium": "中程度",
|
||||||
|
"settings.reasoning_effort.off": "オフ",
|
||||||
"settings.reasoning_effort.tip": "この設定は推論モデルのみサポートしています"
|
"settings.reasoning_effort.tip": "この設定は推論モデルのみサポートしています"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -120,6 +121,8 @@
|
|||||||
"settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します",
|
"settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します",
|
||||||
"settings.max_tokens.confirm": "最大トークン制限を有効にする",
|
"settings.max_tokens.confirm": "最大トークン制限を有効にする",
|
||||||
"settings.max_tokens.confirm_content": "最大トークン制限を有効にすると、モデルが生成できる最大トークン数が制限されます。これにより、返される結果の長さに影響が出る可能性があります。モデルのコンテキスト制限に基づいて設定する必要があります。そうしないとエラーが発生します",
|
"settings.max_tokens.confirm_content": "最大トークン制限を有効にすると、モデルが生成できる最大トークン数が制限されます。これにより、返される結果の長さに影響が出る可能性があります。モデルのコンテキスト制限に基づいて設定する必要があります。そうしないとエラーが発生します",
|
||||||
|
"settings.thought_auto_collapse": "思考内容を自動的に折りたたむ",
|
||||||
|
"settings.thought_auto_collapse.tip": "思考が終了したら思考内容を自動的に折りたたみます",
|
||||||
"suggestions.title": "提案された質問",
|
"suggestions.title": "提案された質問",
|
||||||
"thinking": "思考中...",
|
"thinking": "思考中...",
|
||||||
"topics.auto_rename": "自動リネーム",
|
"topics.auto_rename": "自動リネーム",
|
||||||
@ -136,10 +139,13 @@
|
|||||||
"topics.pinned": "トピックを固定",
|
"topics.pinned": "トピックを固定",
|
||||||
"topics.title": "トピック",
|
"topics.title": "トピック",
|
||||||
"topics.unpinned": "固定解除",
|
"topics.unpinned": "固定解除",
|
||||||
|
"topics.delete.shortcut": "{{key}}キーを押しながらで直接削除",
|
||||||
"translate": "翻訳",
|
"translate": "翻訳",
|
||||||
"topics.prompt": "トピック提示語",
|
"topics.prompt": "トピック提示語",
|
||||||
"topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供",
|
"topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供",
|
||||||
"topics.prompt.edit.title": "トピック提示語を編集する"
|
"topics.prompt.edit.title": "トピック提示語を編集する",
|
||||||
|
"artifacts.button.openExternal": "外部ブラウザで開く",
|
||||||
|
"artifacts.preview.openExternal.error.content": "外部ブラウザの起動に失敗しました。"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
@ -309,6 +315,7 @@
|
|||||||
"chinese-traditional": "繁体字中国語",
|
"chinese-traditional": "繁体字中国語",
|
||||||
"english": "英語",
|
"english": "英語",
|
||||||
"french": "フランス語",
|
"french": "フランス語",
|
||||||
|
"german": "ドイツ語",
|
||||||
"italian": "イタリア語",
|
"italian": "イタリア語",
|
||||||
"japanese": "日本語",
|
"japanese": "日本語",
|
||||||
"korean": "韓国語",
|
"korean": "韓国語",
|
||||||
@ -351,7 +358,7 @@
|
|||||||
"error.invalid.enter.model": "モデルを選択してください",
|
"error.invalid.enter.model": "モデルを選択してください",
|
||||||
"error.invalid.proxy.url": "無効なプロキシURL",
|
"error.invalid.proxy.url": "無効なプロキシURL",
|
||||||
"error.invalid.webdav": "無効なWebDAV設定",
|
"error.invalid.webdav": "無効なWebDAV設定",
|
||||||
"error.notion.export": "Notion インポートに失敗",
|
"error.notion.export": "Notionへのエクスポートに失敗しました。接続状態と設定を確認してください",
|
||||||
"error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません",
|
"error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません",
|
||||||
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
|
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
|
||||||
"group.delete.title": "分組メッセージを削除",
|
"group.delete.title": "分組メッセージを削除",
|
||||||
@ -363,6 +370,7 @@
|
|||||||
"message.multi_model_style.fold": "折りたたむ",
|
"message.multi_model_style.fold": "折りたたむ",
|
||||||
"message.multi_model_style.horizontal": "水平",
|
"message.multi_model_style.horizontal": "水平",
|
||||||
"message.multi_model_style.vertical": "垂直",
|
"message.multi_model_style.vertical": "垂直",
|
||||||
|
"message.multi_model_style.grid": "グリッド",
|
||||||
"message.style": "メッセージスタイル",
|
"message.style": "メッセージスタイル",
|
||||||
"message.style.bubble": "バブル",
|
"message.style.bubble": "バブル",
|
||||||
"message.style.plain": "プレーン",
|
"message.style.plain": "プレーン",
|
||||||
@ -372,13 +380,13 @@
|
|||||||
"reset.double.confirm.title": "データが失われます!!!",
|
"reset.double.confirm.title": "データが失われます!!!",
|
||||||
"restore.success": "復元に成功しました",
|
"restore.success": "復元に成功しました",
|
||||||
"save.success.title": "保存に成功しました",
|
"save.success.title": "保存に成功しました",
|
||||||
"success.notion.export": "Notion へのインポートに成功",
|
"success.notion.export": "Notionへのエクスポートに成功しました",
|
||||||
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
||||||
"topic.added": "新しいトピックが追加されました",
|
"topic.added": "新しいトピックが追加されました",
|
||||||
"upgrade.success.button": "再起動",
|
"upgrade.success.button": "再起動",
|
||||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||||
"upgrade.success.title": "アップグレードに成功しました",
|
"upgrade.success.title": "アップグレードに成功しました",
|
||||||
"warn.notion.exporting": "Notion 正在インポート中です。重複インポートしないでください。",
|
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||||
"error.enter.name": "ナレッジベース名を入力してください",
|
"error.enter.name": "ナレッジベース名を入力してください",
|
||||||
"error.invalid.api.host": "無効なAPIアドレスです",
|
"error.invalid.api.host": "無効なAPIアドレスです",
|
||||||
"error.invalid.api.key": "無効なAPIキーです"
|
"error.invalid.api.key": "無効なAPIキーです"
|
||||||
@ -559,17 +567,45 @@
|
|||||||
},
|
},
|
||||||
"data.title": "データディレクトリ",
|
"data.title": "データディレクトリ",
|
||||||
"notion.api_key": "Notion APIキー",
|
"notion.api_key": "Notion APIキー",
|
||||||
|
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||||
"notion.database_id": "Notion データベースID",
|
"notion.database_id": "Notion データベースID",
|
||||||
|
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
|
||||||
|
"notion.page_name_key": "ページタイトルフィールド名",
|
||||||
|
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
|
||||||
"notion.title": "Notion 設定",
|
"notion.title": "Notion 設定",
|
||||||
|
"notion.help": "Notion 設定ドキュメント",
|
||||||
"notion.check": {
|
"notion.check": {
|
||||||
"button": "確認",
|
"button": "確認",
|
||||||
"fail": "接続に失敗しました。設定を確認してください。",
|
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||||
"success": "接続に成功しました。",
|
"success": "接続に成功しました。",
|
||||||
"error": "接続エラーが発生しました。ネットワークを確認してください。",
|
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
|
||||||
"empty_api_key": "Api_keyが設定されていません",
|
"empty_api_key": "Api_keyが設定されていません",
|
||||||
"empty_database_id": "Database_idが設定されていません"
|
"empty_database_id": "Database_idが設定されていません"
|
||||||
},
|
},
|
||||||
"title": "データ設定",
|
"title": "データ設定",
|
||||||
|
"webdav": {
|
||||||
|
"autoSync": "自動バックアップ",
|
||||||
|
"autoSync.off": "オフ",
|
||||||
|
"backup.button": "WebDAVにバックアップ",
|
||||||
|
"host": "WebDAVホスト",
|
||||||
|
"host.placeholder": "http://localhost:8080",
|
||||||
|
"minute_interval_one": "{{count}} 分",
|
||||||
|
"minute_interval_other": "{{count}} 分",
|
||||||
|
"hour_interval_one": "{{count}} 時間",
|
||||||
|
"hour_interval_other": "{{count}} 時間",
|
||||||
|
"lastSync": "最終バックアップ",
|
||||||
|
"noSync": "次回のバックアップを待機中",
|
||||||
|
"password": "WebDAVパスワード",
|
||||||
|
"path": "WebDAVパス",
|
||||||
|
"path.placeholder": "/backup",
|
||||||
|
"restore.button": "WebDAVから復元",
|
||||||
|
"restore.content": "WebDAVから復元すると現在のデータが上書きされます。続行しますか?",
|
||||||
|
"restore.title": "WebDAVから復元",
|
||||||
|
"syncError": "バックアップエラー",
|
||||||
|
"syncStatus": "バックアップ状態",
|
||||||
|
"title": "WebDAV",
|
||||||
|
"user": "WebDAVユーザー"
|
||||||
|
},
|
||||||
"webdav.autoSync": "自動バックアップ",
|
"webdav.autoSync": "自動バックアップ",
|
||||||
"webdav.autoSync.off": "オフ",
|
"webdav.autoSync.off": "オフ",
|
||||||
"webdav.backup.button": "WebDAVにバックアップ",
|
"webdav.backup.button": "WebDAVにバックアップ",
|
||||||
@ -588,7 +624,11 @@
|
|||||||
"webdav.syncError": "バックアップエラー",
|
"webdav.syncError": "バックアップエラー",
|
||||||
"webdav.syncStatus": "バックアップ状態",
|
"webdav.syncStatus": "バックアップ状態",
|
||||||
"webdav.title": "WebDAV",
|
"webdav.title": "WebDAV",
|
||||||
"webdav.user": "WebDAVユーザー"
|
"webdav.user": "WebDAVユーザー",
|
||||||
|
"minute_interval_one": "{{count}} 分",
|
||||||
|
"minute_interval_other": "{{count}} 分",
|
||||||
|
"hour_interval_one": "{{count}} 時間",
|
||||||
|
"hour_interval_other": "{{count}} 時間"
|
||||||
},
|
},
|
||||||
"display.custom.css": "カスタムCSS",
|
"display.custom.css": "カスタムCSS",
|
||||||
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
|
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
|
||||||
@ -636,6 +676,10 @@
|
|||||||
"messages.input.title": "入力設定",
|
"messages.input.title": "入力設定",
|
||||||
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
|
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
|
||||||
"messages.math_engine": "数式エンジン",
|
"messages.math_engine": "数式エンジン",
|
||||||
|
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||||
|
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||||
|
"messages.grid_popover_trigger.hover": "ホバーで表示",
|
||||||
|
"messages.grid_popover_trigger.click": "クリックで表示",
|
||||||
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
||||||
"messages.model.title": "モデル設定",
|
"messages.model.title": "モデル設定",
|
||||||
"messages.title": "メッセージ設定",
|
"messages.title": "メッセージ設定",
|
||||||
@ -708,7 +752,7 @@
|
|||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"alt_warning": "MacではOption + 文字をショートカットとして使用できません",
|
"alt_warning": "MacではOptionキーとの組み合わせは、スペースキーのみ使用可能です",
|
||||||
"clear_shortcut": "ショートカットをクリア",
|
"clear_shortcut": "ショートカットをクリア",
|
||||||
"clear_topic": "メッセージを消去",
|
"clear_topic": "メッセージを消去",
|
||||||
"copy_last_message": "最後のメッセージをコピー",
|
"copy_last_message": "最後のメッセージをコピー",
|
||||||
@ -727,7 +771,8 @@
|
|||||||
"toggle_show_topics": "トピックの表示を切り替え",
|
"toggle_show_topics": "トピックの表示を切り替え",
|
||||||
"zoom_in": "ズームイン",
|
"zoom_in": "ズームイン",
|
||||||
"zoom_out": "ズームアウト",
|
"zoom_out": "ズームアウト",
|
||||||
"zoom_reset": "ズームをリセット"
|
"zoom_reset": "ズームをリセット",
|
||||||
|
"show_settings": "設定を開く"
|
||||||
},
|
},
|
||||||
"theme.auto": "自動",
|
"theme.auto": "自動",
|
||||||
"theme.dark": "ダークテーマ",
|
"theme.dark": "ダークテーマ",
|
||||||
@ -746,6 +791,7 @@
|
|||||||
"translate": {
|
"translate": {
|
||||||
"any.language": "任意の言語",
|
"any.language": "任意の言語",
|
||||||
"button.translate": "翻訳",
|
"button.translate": "翻訳",
|
||||||
|
"tooltip.newline": "改行",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"content": "翻訳すると元のテキストが上書きされます。続行しますか?",
|
"content": "翻訳すると元のテキストが上書きされます。続行しますか?",
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
"settings.reasoning_effort.high": "Длинная",
|
"settings.reasoning_effort.high": "Длинная",
|
||||||
"settings.reasoning_effort.low": "Короткая",
|
"settings.reasoning_effort.low": "Короткая",
|
||||||
"settings.reasoning_effort.medium": "Средняя",
|
"settings.reasoning_effort.medium": "Средняя",
|
||||||
|
"settings.reasoning_effort.off": "Выключено",
|
||||||
"settings.reasoning_effort.tip": "Эта настройка поддерживается только моделями с рассуждением"
|
"settings.reasoning_effort.tip": "Эта настройка поддерживается только моделями с рассуждением"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -120,6 +121,8 @@
|
|||||||
"settings.top_p.tip": "Значение по умолчанию 1, чем меньше значение, тем меньше вариативности в ответах, тем проще понять, чем больше значение, тем больше вариативности в ответах, тем больше разнообразие",
|
"settings.top_p.tip": "Значение по умолчанию 1, чем меньше значение, тем меньше вариативности в ответах, тем проще понять, чем больше значение, тем больше вариативности в ответах, тем больше разнообразие",
|
||||||
"settings.max_tokens.confirm": "Включить лимит максимальных токенов",
|
"settings.max_tokens.confirm": "Включить лимит максимальных токенов",
|
||||||
"settings.max_tokens.confirm_content": "Включить лимит максимальных токенов, влияет на длину результата. Нужно учитывать контекст модели, иначе будет ошибка",
|
"settings.max_tokens.confirm_content": "Включить лимит максимальных токенов, влияет на длину результата. Нужно учитывать контекст модели, иначе будет ошибка",
|
||||||
|
"settings.thought_auto_collapse": "Автоматически сворачивать содержание мыслей",
|
||||||
|
"settings.thought_auto_collapse.tip": "Автоматически сворачивать содержание мыслей после завершения размышления",
|
||||||
"suggestions.title": "Предложенные вопросы",
|
"suggestions.title": "Предложенные вопросы",
|
||||||
"thinking": "Мыслим",
|
"thinking": "Мыслим",
|
||||||
"topics.auto_rename": "Автопереименование",
|
"topics.auto_rename": "Автопереименование",
|
||||||
@ -136,10 +139,13 @@
|
|||||||
"topics.pinned": "Закрепленные темы",
|
"topics.pinned": "Закрепленные темы",
|
||||||
"topics.title": "Топики",
|
"topics.title": "Топики",
|
||||||
"topics.unpinned": "Открепленные темы",
|
"topics.unpinned": "Открепленные темы",
|
||||||
|
"topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления",
|
||||||
"translate": "Перевести",
|
"translate": "Перевести",
|
||||||
"topics.prompt": "Тематические подсказки",
|
"topics.prompt": "Тематические подсказки",
|
||||||
"topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы",
|
"topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы",
|
||||||
"topics.prompt.edit.title": "Редактировать подсказки темы"
|
"topics.prompt.edit.title": "Редактировать подсказки темы",
|
||||||
|
"artifacts.button.openExternal": "Открыть во внешнем браузере",
|
||||||
|
"artifacts.preview.openExternal.error.content": "Внешний браузер открылся с ошибкой"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
@ -309,6 +315,7 @@
|
|||||||
"chinese-traditional": "Китайский традиционный",
|
"chinese-traditional": "Китайский традиционный",
|
||||||
"english": "Английский",
|
"english": "Английский",
|
||||||
"french": "Французский",
|
"french": "Французский",
|
||||||
|
"german": "Немецкий",
|
||||||
"italian": "Итальянский",
|
"italian": "Итальянский",
|
||||||
"japanese": "Японский",
|
"japanese": "Японский",
|
||||||
"korean": "Корейский",
|
"korean": "Корейский",
|
||||||
@ -352,7 +359,7 @@
|
|||||||
"error.invalid.enter.model": "Пожалуйста, выберите модель",
|
"error.invalid.enter.model": "Пожалуйста, выберите модель",
|
||||||
"error.invalid.proxy.url": "Неверный URL прокси",
|
"error.invalid.proxy.url": "Неверный URL прокси",
|
||||||
"error.invalid.webdav": "Неверные настройки WebDAV",
|
"error.invalid.webdav": "Неверные настройки WebDAV",
|
||||||
"error.notion.export": "Импорт в Notion не удался",
|
"error.notion.export": "Ошибка экспорта в Notion, пожалуйста, проверьте состояние подключения и настройки в документации",
|
||||||
"error.notion.no_api_key": "Notion ApiKey или Notion DatabaseID не настроен",
|
"error.notion.no_api_key": "Notion ApiKey или Notion DatabaseID не настроен",
|
||||||
"group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника",
|
"group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника",
|
||||||
"group.delete.title": "Удалить группу сообщений",
|
"group.delete.title": "Удалить группу сообщений",
|
||||||
@ -364,6 +371,7 @@
|
|||||||
"message.multi_model_style.fold": "Свернуть",
|
"message.multi_model_style.fold": "Свернуть",
|
||||||
"message.multi_model_style.horizontal": "Горизонтальный",
|
"message.multi_model_style.horizontal": "Горизонтальный",
|
||||||
"message.multi_model_style.vertical": "Вертикальный",
|
"message.multi_model_style.vertical": "Вертикальный",
|
||||||
|
"message.multi_model_style.grid": "клетчатый вид",
|
||||||
"message.style": "Стиль сообщения",
|
"message.style": "Стиль сообщения",
|
||||||
"message.style.bubble": "Пузырь",
|
"message.style.bubble": "Пузырь",
|
||||||
"message.style.plain": "Простой",
|
"message.style.plain": "Простой",
|
||||||
@ -373,13 +381,13 @@
|
|||||||
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
|
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
|
||||||
"restore.success": "Успешно восстановлено",
|
"restore.success": "Успешно восстановлено",
|
||||||
"save.success.title": "Успешно сохранено",
|
"save.success.title": "Успешно сохранено",
|
||||||
"success.notion.export": "Импорт в Notion выполнен успешно",
|
"success.notion.export": "Успешный экспорт в Notion",
|
||||||
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
||||||
"topic.added": "Новый топик добавлен",
|
"topic.added": "Новый топик добавлен",
|
||||||
"upgrade.success.button": "Перезапустить",
|
"upgrade.success.button": "Перезапустить",
|
||||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||||
"upgrade.success.title": "Обновление успешно",
|
"upgrade.success.title": "Обновление успешно",
|
||||||
"warn.notion.exporting": "Идет импорт в Notion, пожалуйста, не повторяйте импорт",
|
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||||
"error.invalid.api.host": "Неверный API адрес",
|
"error.invalid.api.host": "Неверный API адрес",
|
||||||
"error.invalid.api.key": "Неверный API ключ"
|
"error.invalid.api.key": "Неверный API ключ"
|
||||||
},
|
},
|
||||||
@ -559,36 +567,47 @@
|
|||||||
},
|
},
|
||||||
"data.title": "Каталог данных",
|
"data.title": "Каталог данных",
|
||||||
"notion.api_key": "Ключ API Notion",
|
"notion.api_key": "Ключ API Notion",
|
||||||
|
"notion.api_key_placeholder": "Введите ключ API Notion",
|
||||||
"notion.database_id": "ID базы данных Notion",
|
"notion.database_id": "ID базы данных Notion",
|
||||||
|
"notion.database_id_placeholder": "Введите ID базы данных Notion",
|
||||||
|
"notion.page_name_key": "Название поля заголовка страницы",
|
||||||
|
"notion.page_name_key_placeholder": "Введите название поля заголовка страницы, по умолчанию Name",
|
||||||
"notion.title": "Настройки Notion",
|
"notion.title": "Настройки Notion",
|
||||||
|
"notion.help": "Документация по настройке Notion",
|
||||||
"notion.check": {
|
"notion.check": {
|
||||||
"button": "Проверить",
|
"button": "Проверить",
|
||||||
"fail": "Ошибка подключения, проверьте настройки",
|
"fail": "Не удалось подключиться, пожалуйста, проверьте сеть и правильность Api_key и Database_id",
|
||||||
"success": "Подключение успешно",
|
"success": "Подключение успешно",
|
||||||
"error": "Ошибка подключения, проверьте сеть",
|
"error": "Аномалия в подключении, пожалуйста, проверьте настройки сети, а также правильность Api_key и Database_id",
|
||||||
"empty_api_key": "Не настроен Api_key",
|
"empty_api_key": "Не настроен Api_key",
|
||||||
"empty_database_id": "Не настроен Database_id"
|
"empty_database_id": "Не настроен Database_id"
|
||||||
},
|
},
|
||||||
"title": "Настройки данных",
|
"title": "Настройки данных",
|
||||||
"webdav.autoSync": "Автоматическое резервное копирование",
|
"webdav": {
|
||||||
"webdav.autoSync.off": "Выключено",
|
"autoSync": "Автоматическое резервное копирование",
|
||||||
"webdav.backup.button": "Резервное копирование на WebDAV",
|
"autoSync.off": "Выключено",
|
||||||
"webdav.host": "Хост WebDAV",
|
"backup.button": "Резервное копирование на WebDAV",
|
||||||
"webdav.host.placeholder": "http://localhost:8080",
|
"host": "Хост WebDAV",
|
||||||
"webdav.hours": "часов",
|
"host.placeholder": "http://localhost:8080",
|
||||||
"webdav.lastSync": "Последняя синхронизация",
|
"minute_interval_one": "{{count}} минута",
|
||||||
"webdav.minutes": "минут",
|
"minute_interval_few": "{{count}} минуты",
|
||||||
"webdav.noSync": "Ожидание следующего резервного копирования",
|
"minute_interval_many": "{{count}} минут",
|
||||||
"webdav.password": "Пароль WebDAV",
|
"hour_interval_one": "{{count}} час",
|
||||||
"webdav.path": "Путь WebDAV",
|
"hour_interval_few": "{{count}} часа",
|
||||||
"webdav.path.placeholder": "/backup",
|
"hour_interval_many": "{{count}} часов",
|
||||||
"webdav.restore.button": "Восстановление с WebDAV",
|
"lastSync": "Последняя синхронизация",
|
||||||
"webdav.restore.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?",
|
"noSync": "Ожидание следующего резервного копирования",
|
||||||
"webdav.restore.title": "Восстановление с WebDAV",
|
"password": "Пароль WebDAV",
|
||||||
"webdav.syncError": "Ошибка резервного копирования",
|
"path": "Путь WebDAV",
|
||||||
"webdav.syncStatus": "Статус резервного копирования",
|
"path.placeholder": "/backup",
|
||||||
"webdav.title": "WebDAV",
|
"restore.button": "Восстановление с WebDAV",
|
||||||
"webdav.user": "Пользователь WebDAV"
|
"restore.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?",
|
||||||
|
"restore.title": "Восстановление с WebDAV",
|
||||||
|
"syncError": "Ошибка резервного копирования",
|
||||||
|
"syncStatus": "Статус резервного копирования",
|
||||||
|
"title": "WebDAV",
|
||||||
|
"user": "Пользователь WebDAV"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"display.custom.css": "Пользовательский CSS",
|
"display.custom.css": "Пользовательский CSS",
|
||||||
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
|
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
|
||||||
@ -637,6 +656,10 @@
|
|||||||
"messages.math_engine": "Математический движок",
|
"messages.math_engine": "Математический движок",
|
||||||
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
||||||
"messages.model.title": "Настройки модели",
|
"messages.model.title": "Настройки модели",
|
||||||
|
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||||
|
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||||
|
"messages.grid_popover_trigger.hover": "Наведение для отображения",
|
||||||
|
"messages.grid_popover_trigger.click": "Нажатие для отображения",
|
||||||
"messages.title": "Настройки сообщений",
|
"messages.title": "Настройки сообщений",
|
||||||
"messages.use_serif_font": "Использовать serif шрифт",
|
"messages.use_serif_font": "Использовать serif шрифт",
|
||||||
"model": "Модель по умолчанию",
|
"model": "Модель по умолчанию",
|
||||||
@ -707,7 +730,7 @@
|
|||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"action": "Действие",
|
"action": "Действие",
|
||||||
"alt_warning": "Mac не поддерживает Option + буквы как горячие клавиши",
|
"alt_warning": "В Mac сочетания с клавишей Option работают только с пробелом",
|
||||||
"clear_shortcut": "Очистить сочетание клавиш",
|
"clear_shortcut": "Очистить сочетание клавиш",
|
||||||
"clear_topic": "Очистить все сообщения",
|
"clear_topic": "Очистить все сообщения",
|
||||||
"copy_last_message": "Копировать последнее сообщение",
|
"copy_last_message": "Копировать последнее сообщение",
|
||||||
@ -726,7 +749,8 @@
|
|||||||
"toggle_show_topics": "Переключить отображение топиков",
|
"toggle_show_topics": "Переключить отображение топиков",
|
||||||
"zoom_in": "Увеличить",
|
"zoom_in": "Увеличить",
|
||||||
"zoom_out": "Уменьшить",
|
"zoom_out": "Уменьшить",
|
||||||
"zoom_reset": "Сбросить масштаб"
|
"zoom_reset": "Сбросить масштаб",
|
||||||
|
"show_settings": "Открыть настройки"
|
||||||
},
|
},
|
||||||
"theme.auto": "Автоматически",
|
"theme.auto": "Автоматически",
|
||||||
"theme.dark": "Темная",
|
"theme.dark": "Темная",
|
||||||
@ -746,6 +770,7 @@
|
|||||||
"translate": {
|
"translate": {
|
||||||
"any.language": "Любой язык",
|
"any.language": "Любой язык",
|
||||||
"button.translate": "Перевести",
|
"button.translate": "Перевести",
|
||||||
|
"tooltip.newline": "Перевести",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"content": "Перевод заменит исходный текст, продолжить?",
|
"content": "Перевод заменит исходный текст, продолжить?",
|
||||||
|
|||||||
@ -51,6 +51,7 @@
|
|||||||
"settings.reasoning_effort.high": "长",
|
"settings.reasoning_effort.high": "长",
|
||||||
"settings.reasoning_effort.low": "短",
|
"settings.reasoning_effort.low": "短",
|
||||||
"settings.reasoning_effort.medium": "中",
|
"settings.reasoning_effort.medium": "中",
|
||||||
|
"settings.reasoning_effort.off": "关",
|
||||||
"settings.reasoning_effort.tip": "该设置仅支持推理模型",
|
"settings.reasoning_effort.tip": "该设置仅支持推理模型",
|
||||||
"title": "助手"
|
"title": "助手"
|
||||||
},
|
},
|
||||||
@ -73,6 +74,8 @@
|
|||||||
"add.assistant.title": "添加助手",
|
"add.assistant.title": "添加助手",
|
||||||
"artifacts.button.download": "下载",
|
"artifacts.button.download": "下载",
|
||||||
"artifacts.button.preview": "预览",
|
"artifacts.button.preview": "预览",
|
||||||
|
"artifacts.button.openExternal": "外部浏览器打开",
|
||||||
|
"artifacts.preview.openExternal.error.content": "外部浏览器打开出错",
|
||||||
"assistant.search.placeholder": "搜索",
|
"assistant.search.placeholder": "搜索",
|
||||||
"deeply_thought": "已深度思考(用时 {{secounds}} 秒)",
|
"deeply_thought": "已深度思考(用时 {{secounds}} 秒)",
|
||||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||||
@ -120,6 +123,8 @@
|
|||||||
"settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化",
|
"settings.top_p.tip": "默认值为 1,值越小,AI 生成的内容越单调,也越容易理解;值越大,AI 回复的词汇围越大,越多样化",
|
||||||
"settings.max_tokens.confirm": "开启消息长度限制",
|
"settings.max_tokens.confirm": "开启消息长度限制",
|
||||||
"settings.max_tokens.confirm_content": "开启消息长度限制后,单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错",
|
"settings.max_tokens.confirm_content": "开启消息长度限制后,单次交互所用的最大 Token 数, 会影响返回结果的长度。要根据模型上下文限制来设置,否则会报错",
|
||||||
|
"settings.thought_auto_collapse": "思考内容自动折叠",
|
||||||
|
"settings.thought_auto_collapse.tip": "思考结束后思考内容自动折叠",
|
||||||
"suggestions.title": "建议的问题",
|
"suggestions.title": "建议的问题",
|
||||||
"thinking": "思考中",
|
"thinking": "思考中",
|
||||||
"topics.auto_rename": "生成话题名",
|
"topics.auto_rename": "生成话题名",
|
||||||
@ -136,10 +141,10 @@
|
|||||||
"topics.pinned": "固定话题",
|
"topics.pinned": "固定话题",
|
||||||
"topics.title": "话题",
|
"topics.title": "话题",
|
||||||
"topics.unpinned": "取消固定",
|
"topics.unpinned": "取消固定",
|
||||||
|
"topics.delete.shortcut": "按住 {{key}} 可直接删除",
|
||||||
"translate": "翻译",
|
"translate": "翻译",
|
||||||
"topics.prompt": "话题提示词",
|
"topics.prompt": "话题提示词",
|
||||||
"topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词",
|
"topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词"
|
||||||
"topics.prompt.edit.title": "编辑话题提示词"
|
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
@ -314,7 +319,8 @@
|
|||||||
"korean": "韩文",
|
"korean": "韩文",
|
||||||
"portuguese": "葡萄牙文",
|
"portuguese": "葡萄牙文",
|
||||||
"russian": "俄文",
|
"russian": "俄文",
|
||||||
"spanish": "西班牙文"
|
"spanish": "西班牙文",
|
||||||
|
"german": "德文"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
"mermaid": {
|
||||||
"download": {
|
"download": {
|
||||||
@ -331,6 +337,18 @@
|
|||||||
},
|
},
|
||||||
"title": "Mermaid 图表"
|
"title": "Mermaid 图表"
|
||||||
},
|
},
|
||||||
|
"plantuml": {
|
||||||
|
"title": "PlantUML 图表",
|
||||||
|
"download": {
|
||||||
|
"png": "下载 PNG",
|
||||||
|
"svg": "下载 SVG",
|
||||||
|
"failed": "下载失败,请检查网络"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"preview": "预览",
|
||||||
|
"source": "源码"
|
||||||
|
}
|
||||||
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"api.check.model.title": "请选择要检测的模型",
|
"api.check.model.title": "请选择要检测的模型",
|
||||||
"api.connection.failed": "连接失败",
|
"api.connection.failed": "连接失败",
|
||||||
@ -354,7 +372,7 @@
|
|||||||
"error.invalid.enter.model": "请选择一个模型",
|
"error.invalid.enter.model": "请选择一个模型",
|
||||||
"error.invalid.proxy.url": "无效的代理地址",
|
"error.invalid.proxy.url": "无效的代理地址",
|
||||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||||
"error.notion.export": "Notion 导入失败",
|
"error.notion.export": "导出Notion错误,请检查连接状态并对照文档检查配置",
|
||||||
"error.notion.no_api_key": "未配置Notion ApiKey或Notion DatabaseID",
|
"error.notion.no_api_key": "未配置Notion ApiKey或Notion DatabaseID",
|
||||||
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
|
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
|
||||||
"group.delete.title": "删除分组消息",
|
"group.delete.title": "删除分组消息",
|
||||||
@ -366,6 +384,7 @@
|
|||||||
"message.multi_model_style.fold": "折叠",
|
"message.multi_model_style.fold": "折叠",
|
||||||
"message.multi_model_style.horizontal": "水平",
|
"message.multi_model_style.horizontal": "水平",
|
||||||
"message.multi_model_style.vertical": "垂直",
|
"message.multi_model_style.vertical": "垂直",
|
||||||
|
"message.multi_model_style.grid": "网格",
|
||||||
"message.style": "消息样式",
|
"message.style": "消息样式",
|
||||||
"message.style.bubble": "气泡",
|
"message.style.bubble": "气泡",
|
||||||
"message.style.plain": "简洁",
|
"message.style.plain": "简洁",
|
||||||
@ -375,13 +394,13 @@
|
|||||||
"reset.double.confirm.title": "数据丢失!!!",
|
"reset.double.confirm.title": "数据丢失!!!",
|
||||||
"restore.success": "恢复成功",
|
"restore.success": "恢复成功",
|
||||||
"save.success.title": "保存成功",
|
"save.success.title": "保存成功",
|
||||||
"success.notion.export": "导入Notion成功",
|
"success.notion.export": "成功导出到Notion",
|
||||||
"switch.disabled": "请等待当前回复完成后操作",
|
"switch.disabled": "请等待当前回复完成后操作",
|
||||||
"topic.added": "话题添加成功",
|
"topic.added": "话题添加成功",
|
||||||
"upgrade.success.button": "重启",
|
"upgrade.success.button": "重启",
|
||||||
"upgrade.success.content": "重启用以完成升级",
|
"upgrade.success.content": "重启用以完成升级",
|
||||||
"upgrade.success.title": "升级成功",
|
"upgrade.success.title": "升级成功",
|
||||||
"warn.notion.exporting": "Notion正在导入,请勿重复导入"
|
"warn.notion.exporting": "正在导出到Notion, 请勿重复请求导出!"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "添加到侧边栏",
|
"sidebar.add.title": "添加到侧边栏",
|
||||||
@ -559,36 +578,49 @@
|
|||||||
},
|
},
|
||||||
"data.title": "数据目录",
|
"data.title": "数据目录",
|
||||||
"notion.api_key": "Notion 密钥",
|
"notion.api_key": "Notion 密钥",
|
||||||
"notion.database_id": "Notion 数据库ID",
|
"notion.api_key_placeholder": "请输入Notion 密钥",
|
||||||
|
"notion.database_id": "Notion 数据库 ID",
|
||||||
|
"notion.database_id_placeholder": "请输入Notion 数据库 ID",
|
||||||
|
"notion.page_name_key": "页面标题字段名",
|
||||||
|
"notion.page_name_key_placeholder": "请输入页面标题字段名,默认为 Name",
|
||||||
"notion.title": "Notion 配置",
|
"notion.title": "Notion 配置",
|
||||||
|
"notion.help": "Notion 配置文档",
|
||||||
"notion.check": {
|
"notion.check": {
|
||||||
"button": "检查",
|
"button": "检查",
|
||||||
"fail": "连接失败,请检查配置",
|
"fail": "连接失败,请检查网络及Api_key和Database_id是否正确",
|
||||||
"success": "连接成功",
|
"success": "连接成功",
|
||||||
"error": "连接异常,请检查网络",
|
"error": "连接异常,请检查网络及Api_key和Database_id是否正确",
|
||||||
"empty_api_key": "未配置Api_key",
|
"empty_api_key": "未配置Api_key",
|
||||||
"empty_database_id": "未配置Database_id"
|
"empty_database_id": "未配置Database_id"
|
||||||
},
|
},
|
||||||
"title": "数据设置",
|
"title": "数据设置",
|
||||||
"webdav.autoSync": "自动备份",
|
"webdav": {
|
||||||
"webdav.autoSync.off": "关闭",
|
"autoSync": "自动备份",
|
||||||
"webdav.backup.button": "备份到 WebDAV",
|
"autoSync.off": "关闭",
|
||||||
"webdav.host": "WebDAV 地址",
|
"backup.button": "备份到 WebDAV",
|
||||||
"webdav.host.placeholder": "http://localhost:8080",
|
"host": "WebDAV 地址",
|
||||||
"webdav.hours": "小时",
|
"host.placeholder": "http://localhost:8080",
|
||||||
"webdav.lastSync": "上次备份时间",
|
"minute_interval_one": "{{count}} 分钟",
|
||||||
"webdav.minutes": "分钟",
|
"minute_interval_other": "{{count}} 分钟",
|
||||||
"webdav.noSync": "等待下次备份",
|
"hour_interval_one": "{{count}} 小时",
|
||||||
"webdav.password": "WebDAV 密码",
|
"hour_interval_other": "{{count}} 小时",
|
||||||
"webdav.path": "WebDAV 路径",
|
"lastSync": "上次备份时间",
|
||||||
"webdav.path.placeholder": "/backup",
|
"noSync": "等待下次备份",
|
||||||
"webdav.restore.button": "从 WebDAV 恢复",
|
"password": "WebDAV 密码",
|
||||||
"webdav.restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
"path": "WebDAV 路径",
|
||||||
"webdav.restore.title": "从 WebDAV 恢复",
|
"path.placeholder": "/backup",
|
||||||
"webdav.syncError": "备份错误",
|
"restore.button": "从 WebDAV 恢复",
|
||||||
"webdav.syncStatus": "备份状态",
|
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
||||||
"webdav.title": "WebDAV",
|
"restore.title": "从 WebDAV 恢复",
|
||||||
"webdav.user": "WebDAV 用户名"
|
"syncError": "备份错误",
|
||||||
|
"syncStatus": "备份状态",
|
||||||
|
"title": "WebDAV",
|
||||||
|
"user": "WebDAV 用户名"
|
||||||
|
},
|
||||||
|
"minute_interval_one": "{{count}} 分钟",
|
||||||
|
"minute_interval_other": "{{count}} 分钟",
|
||||||
|
"hour_interval_one": "{{count}} 小时",
|
||||||
|
"hour_interval_other": "{{count}} 小时"
|
||||||
},
|
},
|
||||||
"display.custom.css": "自定义 CSS",
|
"display.custom.css": "自定义 CSS",
|
||||||
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
|
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
|
||||||
@ -636,6 +668,10 @@
|
|||||||
"messages.input.title": "输入设置",
|
"messages.input.title": "输入设置",
|
||||||
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
"messages.markdown_rendering_input_message": "Markdown 渲染输入消息",
|
||||||
"messages.math_engine": "数学公式引擎",
|
"messages.math_engine": "数学公式引擎",
|
||||||
|
"messages.grid_columns": "消息网格展示列数",
|
||||||
|
"messages.grid_popover_trigger": "网格详情触发",
|
||||||
|
"messages.grid_popover_trigger.hover": "悬停显示",
|
||||||
|
"messages.grid_popover_trigger.click": "点击显示",
|
||||||
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||||
"messages.model.title": "模型设置",
|
"messages.model.title": "模型设置",
|
||||||
"messages.title": "消息设置",
|
"messages.title": "消息设置",
|
||||||
@ -708,7 +744,7 @@
|
|||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"alt_warning": "Mac 系统不能使用 Option + 字母作为快捷键",
|
"alt_warning": "Mac 系统中 Option 键只能与空格键组合使用",
|
||||||
"clear_shortcut": "清除快捷键",
|
"clear_shortcut": "清除快捷键",
|
||||||
"clear_topic": "清空消息",
|
"clear_topic": "清空消息",
|
||||||
"copy_last_message": "复制上一条消息",
|
"copy_last_message": "复制上一条消息",
|
||||||
@ -721,6 +757,7 @@
|
|||||||
"reset_to_default": "重置为默认",
|
"reset_to_default": "重置为默认",
|
||||||
"search_message": "搜索消息",
|
"search_message": "搜索消息",
|
||||||
"show_app": "显示应用",
|
"show_app": "显示应用",
|
||||||
|
"show_settings": "打开设置",
|
||||||
"title": "快捷方式",
|
"title": "快捷方式",
|
||||||
"toggle_new_context": "清除上下文",
|
"toggle_new_context": "清除上下文",
|
||||||
"toggle_show_assistants": "切换助手显示",
|
"toggle_show_assistants": "切换助手显示",
|
||||||
@ -746,6 +783,7 @@
|
|||||||
"translate": {
|
"translate": {
|
||||||
"any.language": "任意语言",
|
"any.language": "任意语言",
|
||||||
"button.translate": "翻译",
|
"button.translate": "翻译",
|
||||||
|
"tooltip.newline": "换行",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"content": "翻译后将覆盖原文,是否继续?",
|
"content": "翻译后将覆盖原文,是否继续?",
|
||||||
@ -756,7 +794,14 @@
|
|||||||
"input.placeholder": "输入文本进行翻译",
|
"input.placeholder": "输入文本进行翻译",
|
||||||
"output.placeholder": "翻译",
|
"output.placeholder": "翻译",
|
||||||
"processing": "翻译中...",
|
"processing": "翻译中...",
|
||||||
"title": "翻译"
|
"title": "翻译",
|
||||||
|
"history": {
|
||||||
|
"title": "翻译历史",
|
||||||
|
"empty": "暂无翻译历史",
|
||||||
|
"clear": "清空历史",
|
||||||
|
"delete": "删除",
|
||||||
|
"clear_description": "清空历史将删除所有翻译历史记录,是否继续?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
|
|||||||
@ -51,6 +51,7 @@
|
|||||||
"settings.reasoning_effort.high": "長",
|
"settings.reasoning_effort.high": "長",
|
||||||
"settings.reasoning_effort.low": "短",
|
"settings.reasoning_effort.low": "短",
|
||||||
"settings.reasoning_effort.medium": "中",
|
"settings.reasoning_effort.medium": "中",
|
||||||
|
"settings.reasoning_effort.off": "關",
|
||||||
"settings.reasoning_effort.tip": "該設置僅支持推理模型",
|
"settings.reasoning_effort.tip": "該設置僅支持推理模型",
|
||||||
"title": "助手"
|
"title": "助手"
|
||||||
},
|
},
|
||||||
@ -120,6 +121,8 @@
|
|||||||
"settings.top_p.tip": "模型生成文本的隨機程度。值越小,AI 生成的內容越單調,也越容易理解;值越大,AI 回覆的詞彙範圍越大,越多樣化",
|
"settings.top_p.tip": "模型生成文本的隨機程度。值越小,AI 生成的內容越單調,也越容易理解;值越大,AI 回覆的詞彙範圍越大,越多樣化",
|
||||||
"settings.max_tokens.confirm": "啟用消息長度限制",
|
"settings.max_tokens.confirm": "啟用消息長度限制",
|
||||||
"settings.max_tokens.confirm_content": "啟用消息長度限制後,單次交互所用的最大 Token 數, 會影響返回結果的長度。要根據模型上下文限制來設置,否則會報錯",
|
"settings.max_tokens.confirm_content": "啟用消息長度限制後,單次交互所用的最大 Token 數, 會影響返回結果的長度。要根據模型上下文限制來設置,否則會報錯",
|
||||||
|
"settings.thought_auto_collapse": "思考內容自動折疊",
|
||||||
|
"settings.thought_auto_collapse.tip": "思考結束後思考內容自動折疊",
|
||||||
"suggestions.title": "建議的問題",
|
"suggestions.title": "建議的問題",
|
||||||
"thinking": "思考中",
|
"thinking": "思考中",
|
||||||
"topics.auto_rename": "自動重新命名",
|
"topics.auto_rename": "自動重新命名",
|
||||||
@ -136,10 +139,13 @@
|
|||||||
"topics.pinned": "固定話題",
|
"topics.pinned": "固定話題",
|
||||||
"topics.title": "話題",
|
"topics.title": "話題",
|
||||||
"topics.unpinned": "取消固定",
|
"topics.unpinned": "取消固定",
|
||||||
|
"topics.delete.shortcut": "按住 {{key}} 可直接刪除",
|
||||||
"translate": "翻譯",
|
"translate": "翻譯",
|
||||||
"topics.prompt": "話題提示詞",
|
"topics.prompt": "話題提示詞",
|
||||||
"topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞",
|
"topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞",
|
||||||
"topics.prompt.edit.title": "編輯話題提示詞"
|
"topics.prompt.edit.title": "編輯話題提示詞",
|
||||||
|
"artifacts.button.openExternal": "外部瀏覽器打開",
|
||||||
|
"artifacts.preview.openExternal.error.content": "外部瀏覽器打開出錯"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
@ -314,7 +320,8 @@
|
|||||||
"korean": "韓文",
|
"korean": "韓文",
|
||||||
"portuguese": "葡萄牙文",
|
"portuguese": "葡萄牙文",
|
||||||
"russian": "俄文",
|
"russian": "俄文",
|
||||||
"spanish": "西班牙文"
|
"spanish": "西班牙文",
|
||||||
|
"german": "德文"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
"mermaid": {
|
||||||
"download": {
|
"download": {
|
||||||
@ -352,7 +359,7 @@
|
|||||||
"error.invalid.enter.model": "請選擇一個模型",
|
"error.invalid.enter.model": "請選擇一個模型",
|
||||||
"error.invalid.proxy.url": "無效的代理 URL",
|
"error.invalid.proxy.url": "無效的代理 URL",
|
||||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||||
"error.notion.export": "Notion 匯入失敗",
|
"error.notion.export": "導出Notion錯誤,請檢查連接狀態並對照文檔檢查配置",
|
||||||
"error.notion.no_api_key": "未配置 Notion ApiKey 或 Notion DatabaseID",
|
"error.notion.no_api_key": "未配置 Notion ApiKey 或 Notion DatabaseID",
|
||||||
"group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答",
|
"group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答",
|
||||||
"group.delete.title": "刪除分組消息",
|
"group.delete.title": "刪除分組消息",
|
||||||
@ -364,6 +371,7 @@
|
|||||||
"message.multi_model_style.fold": "折疊",
|
"message.multi_model_style.fold": "折疊",
|
||||||
"message.multi_model_style.horizontal": "水平",
|
"message.multi_model_style.horizontal": "水平",
|
||||||
"message.multi_model_style.vertical": "垂直",
|
"message.multi_model_style.vertical": "垂直",
|
||||||
|
"message.multi_model_style.grid": "网格",
|
||||||
"message.style": "消息樣式",
|
"message.style": "消息樣式",
|
||||||
"message.style.bubble": "氣泡",
|
"message.style.bubble": "氣泡",
|
||||||
"message.style.plain": "簡潔",
|
"message.style.plain": "簡潔",
|
||||||
@ -373,13 +381,13 @@
|
|||||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||||
"restore.success": "恢復成功",
|
"restore.success": "恢復成功",
|
||||||
"save.success.title": "保存成功",
|
"save.success.title": "保存成功",
|
||||||
"success.notion.export": "匯入 Notion 成功",
|
"success.notion.export": "成功導出到Notion",
|
||||||
"switch.disabled": "請等待當前回覆完成",
|
"switch.disabled": "請等待當前回覆完成",
|
||||||
"topic.added": "新話題已添加",
|
"topic.added": "新話題已添加",
|
||||||
"upgrade.success.button": "重新啟動",
|
"upgrade.success.button": "重新啟動",
|
||||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||||
"upgrade.success.title": "升級成功",
|
"upgrade.success.title": "升級成功",
|
||||||
"warn.notion.exporting": "Notion 正在匯入,請勿重複匯入",
|
"warn.notion.exporting": "正在導出到Notion,請勿重複請求導出!",
|
||||||
"error.invalid.api.host": "無效的 API 位址",
|
"error.invalid.api.host": "無效的 API 位址",
|
||||||
"error.invalid.api.key": "無效的 API 密鑰"
|
"error.invalid.api.key": "無效的 API 密鑰"
|
||||||
},
|
},
|
||||||
@ -556,39 +564,52 @@
|
|||||||
"title": "清除緩存"
|
"title": "清除緩存"
|
||||||
},
|
},
|
||||||
"data.title": "數據目錄",
|
"data.title": "數據目錄",
|
||||||
"notion.api_key": "Notion 金鑰",
|
"notion.api_key": "Notion 密鑰",
|
||||||
|
"notion.api_key_placeholder": "請輸入Notion 密鑰",
|
||||||
"notion.database_id": "Notion 資料庫 ID",
|
"notion.database_id": "Notion 資料庫 ID",
|
||||||
|
"notion.database_id_placeholder": "請輸入Notion 資料庫 ID",
|
||||||
|
"notion.page_name_key": "頁面標題欄位名稱",
|
||||||
|
"notion.page_name_key_placeholder": "請輸入頁面標題欄位名稱,預設為 Name",
|
||||||
"notion.title": "Notion 配置",
|
"notion.title": "Notion 配置",
|
||||||
|
"notion.help": "Notion 配置文檔",
|
||||||
"notion.check": {
|
"notion.check": {
|
||||||
"button": "檢查",
|
"button": "檢查",
|
||||||
"fail": "連線失敗,請檢查配置",
|
"fail": "連接失敗,請檢查網絡及Api_key和Database_id是否正確",
|
||||||
"success": "連線成功",
|
"success": "連線成功",
|
||||||
"error": "連線異常,請檢查網路",
|
"error": "連接異常,請檢查網絡及Api_key和Database_id是否正確",
|
||||||
"empty_api_key": "未配置Api_key",
|
"empty_api_key": "未配置Api_key",
|
||||||
"empty_database_id": "未配置Database_id"
|
"empty_database_id": "未配置Database_id"
|
||||||
},
|
},
|
||||||
"title": "數據設定",
|
"title": "數據設定",
|
||||||
"webdav.autoSync": "自動備份",
|
"webdav": {
|
||||||
"webdav.autoSync.off": "關閉",
|
"autoSync": "自動備份",
|
||||||
"webdav.backup.button": "從 WebDAV 備份",
|
"autoSync.off": "關閉",
|
||||||
"webdav.host": "WebDAV 主機位址",
|
"backup.button": "備份到 WebDAV",
|
||||||
"webdav.host.placeholder": "http://localhost:8080",
|
"host": "WebDAV 主機位址",
|
||||||
"webdav.hours": "小時",
|
"host.placeholder": "http://localhost:8080",
|
||||||
"webdav.lastSync": "上次同步時間",
|
"minute_interval_one": "{{count}} 分鐘",
|
||||||
"webdav.minutes": "分鐘",
|
"minute_interval_other": "{{count}} 分鐘",
|
||||||
"webdav.noSync": "等待下次備份",
|
"hour_interval_one": "{{count}} 小時",
|
||||||
"webdav.password": "WebDAV 密碼",
|
"hour_interval_other": "{{count}} 小時",
|
||||||
"webdav.path": "WebDAV Path",
|
"lastSync": "上次備份時間",
|
||||||
"webdav.path.placeholder": "/backup",
|
"noSync": "等待下次備份",
|
||||||
"webdav.restore.button": "從 WebDAV 恢復",
|
"password": "WebDAV 密碼",
|
||||||
"webdav.restore.content": "從 WebDAV 恢復將覆蓋當前資料,是否繼續?",
|
"path": "WebDAV 路徑",
|
||||||
"webdav.restore.title": "從 WebDAV 恢復",
|
"path.placeholder": "/backup",
|
||||||
"webdav.syncError": "備份錯誤",
|
"restore.button": "從 WebDAV 恢復",
|
||||||
"webdav.syncStatus": "備份狀態",
|
"restore.content": "從 WebDAV 恢復將覆蓋當前資料,是否繼續?",
|
||||||
"webdav.title": "WebDAV",
|
"restore.title": "從 WebDAV 恢復",
|
||||||
"webdav.user": "WebDAV 使用者名稱",
|
"syncError": "備份錯誤",
|
||||||
|
"syncStatus": "備份狀態",
|
||||||
|
"title": "WebDAV",
|
||||||
|
"user": "WebDAV 使用者名稱"
|
||||||
|
},
|
||||||
"app_data": "應用數據",
|
"app_data": "應用數據",
|
||||||
"app_logs": "應用日誌"
|
"app_logs": "應用日誌",
|
||||||
|
"minute_interval_one": "{{count}} 分鐘",
|
||||||
|
"minute_interval_other": "{{count}} 分鐘",
|
||||||
|
"hour_interval_one": "{{count}} 小時",
|
||||||
|
"hour_interval_other": "{{count}} 小時"
|
||||||
},
|
},
|
||||||
"display.custom.css": "自定義 CSS",
|
"display.custom.css": "自定義 CSS",
|
||||||
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
|
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
|
||||||
@ -635,6 +656,10 @@
|
|||||||
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
|
"messages.input.show_estimated_tokens": "顯示預估 Token 數",
|
||||||
"messages.input.title": "輸入設定",
|
"messages.input.title": "輸入設定",
|
||||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||||
|
"messages.grid_columns": "消息網格展示列數",
|
||||||
|
"messages.grid_popover_trigger": "網格詳情觸發",
|
||||||
|
"messages.grid_popover_trigger.hover": "懸停顯示",
|
||||||
|
"messages.grid_popover_trigger.click": "點擊顯示",
|
||||||
"messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
"messages.metrics": "首字時延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||||
"messages.model.title": "模型設定",
|
"messages.model.title": "模型設定",
|
||||||
"messages.title": "訊息設定",
|
"messages.title": "訊息設定",
|
||||||
@ -707,7 +732,7 @@
|
|||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"alt_warning": "Mac 不能使用 Option + 字母作為快捷鍵",
|
"alt_warning": "Mac 系統中 Option 鍵只能與空白鍵組合使用",
|
||||||
"clear_shortcut": "清除快捷鍵",
|
"clear_shortcut": "清除快捷鍵",
|
||||||
"clear_topic": "清除所有訊息",
|
"clear_topic": "清除所有訊息",
|
||||||
"copy_last_message": "複製上一条消息",
|
"copy_last_message": "複製上一条消息",
|
||||||
@ -726,7 +751,8 @@
|
|||||||
"toggle_show_topics": "切換話題顯示",
|
"toggle_show_topics": "切換話題顯示",
|
||||||
"zoom_in": "放大界面",
|
"zoom_in": "放大界面",
|
||||||
"zoom_out": "縮小界面",
|
"zoom_out": "縮小界面",
|
||||||
"zoom_reset": "重置縮放"
|
"zoom_reset": "重置縮放",
|
||||||
|
"show_settings": "打開設定"
|
||||||
},
|
},
|
||||||
"theme.auto": "自動",
|
"theme.auto": "自動",
|
||||||
"theme.dark": "深色主題",
|
"theme.dark": "深色主題",
|
||||||
@ -746,6 +772,7 @@
|
|||||||
"translate": {
|
"translate": {
|
||||||
"any.language": "任意語言",
|
"any.language": "任意語言",
|
||||||
"button.translate": "翻譯",
|
"button.translate": "翻譯",
|
||||||
|
"tooltip.newline": "換行",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"content": "翻譯後將覆蓋原文,是否繼續?",
|
"content": "翻譯後將覆蓋原文,是否繼續?",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
|||||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent, KnowledgeBase } from '@renderer/types'
|
||||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||||
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
|
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
@ -25,7 +25,7 @@ type FieldType = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
prompt: string
|
prompt: string
|
||||||
knowledge_base_id: string
|
knowledge_base_ids: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
@ -37,8 +37,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const [emoji, setEmoji] = useState('')
|
const [emoji, setEmoji] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const knowledgeState = useAppSelector((state) => state.knowledge)
|
const knowledgeState = useAppSelector((state) => state.knowledge)
|
||||||
const knowledgeOptions: SelectProps['options'] = []
|
|
||||||
const showKnowledgeIcon = useSidebarIconShow('knowledge')
|
const showKnowledgeIcon = useSidebarIconShow('knowledge')
|
||||||
|
const knowledgeOptions: SelectProps['options'] = []
|
||||||
|
|
||||||
knowledgeState.bases.forEach((base) => {
|
knowledgeState.bases.forEach((base) => {
|
||||||
knowledgeOptions.push({
|
knowledgeOptions.push({
|
||||||
@ -57,7 +57,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const _agent: Agent = {
|
const _agent: Agent = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
name: values.name,
|
name: values.name,
|
||||||
knowledge_base: knowledgeState.bases.find((t) => t.id === values.knowledge_base_id),
|
knowledge_bases: values.knowledge_base_ids
|
||||||
|
?.map((id) => knowledgeState.bases.find((t) => t.id === id))
|
||||||
|
?.filter((base): base is KnowledgeBase => base !== undefined),
|
||||||
emoji: _emoji,
|
emoji: _emoji,
|
||||||
prompt: values.prompt,
|
prompt: values.prompt,
|
||||||
defaultModel: getDefaultModel(),
|
defaultModel: getDefaultModel(),
|
||||||
@ -154,12 +156,18 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showKnowledgeIcon && (
|
{showKnowledgeIcon && (
|
||||||
<Form.Item name="knowledge_base_id" label={t('agents.add.knowledge_base')} rules={[{ required: false }]}>
|
<Form.Item name="knowledge_base_ids" label={t('agents.add.knowledge_base')} rules={[{ required: false }]}>
|
||||||
<Select
|
<Select
|
||||||
|
mode="multiple"
|
||||||
allowClear
|
allowClear
|
||||||
placeholder={t('agents.add.knowledge_base.placeholder')}
|
placeholder={t('agents.add.knowledge_base.placeholder')}
|
||||||
menuItemSelectedIcon={<CheckOutlined />}
|
menuItemSelectedIcon={<CheckOutlined />}
|
||||||
options={knowledgeOptions}
|
options={knowledgeOptions}
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
String(option?.label ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
|||||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||||
import { Assistant, FileType, KnowledgeBase, Message, Model, Topic } from '@renderer/types'
|
import { Assistant, FileType, KnowledgeBase, Message, Model, Topic } from '@renderer/types'
|
||||||
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
|
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
|
||||||
|
import { abortCompletion } from '@renderer/utils/abortController'
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
@ -52,7 +53,6 @@ interface Props {
|
|||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
let _files: FileType[] = []
|
let _files: FileType[] = []
|
||||||
let _base: KnowledgeBase | undefined
|
|
||||||
|
|
||||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
@ -83,10 +83,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
|
||||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||||
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
|
const [isMentionPopupOpen, setIsMentionPopupOpen] = useState(false)
|
||||||
|
const currentMessageId = useRef<string>()
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
|
|
||||||
@ -104,7 +104,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
_files = files
|
_files = files
|
||||||
_base = selectedKnowledgeBase
|
|
||||||
|
|
||||||
const sendMessage = useCallback(async () => {
|
const sendMessage = useCallback(async () => {
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
@ -124,8 +123,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedKnowledgeBase) {
|
if (selectedKnowledgeBases) {
|
||||||
message.knowledgeBaseIds = [selectedKnowledgeBase.id]
|
message.knowledgeBaseIds = selectedKnowledgeBases.map((base) => base.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
@ -135,7 +134,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
if (mentionModels.length > 0) {
|
if (mentionModels.length > 0) {
|
||||||
message.mentions = mentionModels
|
message.mentions = mentionModels
|
||||||
}
|
}
|
||||||
|
currentMessageId.current = message.id
|
||||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||||
|
|
||||||
setText('')
|
setText('')
|
||||||
@ -144,7 +143,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
setTimeout(() => resizeTextArea(), 0)
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
|
||||||
setExpend(false)
|
setExpend(false)
|
||||||
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files, mentionModels])
|
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBases, files, mentionModels])
|
||||||
|
|
||||||
const translate = async () => {
|
const translate = async () => {
|
||||||
if (isTranslating) {
|
if (isTranslating) {
|
||||||
@ -243,6 +242,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 0) {
|
||||||
|
setMentionModels((prev) => prev.slice(0, -1))
|
||||||
|
return event.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addNewTopic = useCallback(async () => {
|
const addNewTopic = useCallback(async () => {
|
||||||
@ -271,6 +275,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPause = () => {
|
const onPause = () => {
|
||||||
|
if (currentMessageId.current) {
|
||||||
|
abortCompletion(currentMessageId.current)
|
||||||
|
}
|
||||||
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||||
store.dispatch(setGenerating(false))
|
store.dispatch(setGenerating(false))
|
||||||
}
|
}
|
||||||
@ -458,14 +465,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedKnowledgeBase(showKnowledgeIcon ? assistant.knowledge_base : undefined)
|
// if assistant knowledge bases are undefined return []
|
||||||
}, [assistant.id, assistant.knowledge_base, showKnowledgeIcon])
|
setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : [])
|
||||||
|
}, [assistant.id, assistant.knowledge_bases, showKnowledgeIcon])
|
||||||
|
|
||||||
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
||||||
|
|
||||||
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => {
|
||||||
updateAssistant({ ...assistant, knowledge_base: base })
|
updateAssistant({ ...assistant, knowledge_bases: bases })
|
||||||
setSelectedKnowledgeBase(base)
|
setSelectedKnowledgeBases(bases ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMentionModel = (model: Model) => {
|
const onMentionModel = (model: Model) => {
|
||||||
@ -573,7 +581,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{showKnowledgeIcon && (
|
{showKnowledgeIcon && (
|
||||||
<KnowledgeBaseButton
|
<KnowledgeBaseButton
|
||||||
selectedBase={selectedKnowledgeBase}
|
selectedBases={selectedKnowledgeBases}
|
||||||
onSelect={handleKnowledgeBaseSelect}
|
onSelect={handleKnowledgeBaseSelect}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
disabled={files.length > 0}
|
disabled={files.length > 0}
|
||||||
|
|||||||
@ -1,71 +1,68 @@
|
|||||||
import { FileSearchOutlined } from '@ant-design/icons'
|
import { CheckOutlined, FileSearchOutlined } from '@ant-design/icons'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
import { KnowledgeBase } from '@renderer/types'
|
import { KnowledgeBase } from '@renderer/types'
|
||||||
import { Button, Popover, Tooltip } from 'antd'
|
import { Popover, Select, SelectProps, Tooltip } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedBase?: KnowledgeBase
|
selectedBases?: KnowledgeBase[]
|
||||||
onSelect: (base?: KnowledgeBase) => void
|
onSelect: (bases: KnowledgeBase[]) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
ToolbarButton?: any
|
ToolbarButton?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnowledgeBaseSelector: FC<Props> = ({ selectedBase, onSelect }) => {
|
const KnowledgeBaseSelector: FC<Props> = ({ selectedBases, onSelect }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const knowledgeState = useAppSelector((state) => state.knowledge)
|
const knowledgeState = useAppSelector((state) => state.knowledge)
|
||||||
|
const knowledgeOptions: SelectProps['options'] = knowledgeState.bases.map((base) => ({
|
||||||
|
label: base.name,
|
||||||
|
value: base.id
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectorContainer>
|
<SelectorContainer>
|
||||||
{knowledgeState.bases.length === 0 ? (
|
{knowledgeState.bases.length === 0 ? (
|
||||||
<EmptyMessage>{t('knowledge.no_bases')}</EmptyMessage>
|
<EmptyMessage>{t('knowledge.no_bases')}</EmptyMessage>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Select
|
||||||
{selectedBase && (
|
mode="multiple"
|
||||||
<Button type="link" block onClick={() => onSelect(undefined)} style={{ textAlign: 'left' }}>
|
value={selectedBases?.map((base) => base.id)}
|
||||||
{t('knowledge.clear_selection')}
|
allowClear
|
||||||
</Button>
|
placeholder={t('agents.add.knowledge_base.placeholder')}
|
||||||
)}
|
menuItemSelectedIcon={<CheckOutlined />}
|
||||||
{knowledgeState.bases.map((base) => (
|
options={knowledgeOptions}
|
||||||
<Button
|
filterOption={(input, option) =>
|
||||||
key={base.id}
|
String(option?.label ?? '')
|
||||||
type={selectedBase?.id === base.id ? 'primary' : 'text'}
|
.toLowerCase()
|
||||||
block
|
.includes(input.toLowerCase())
|
||||||
onClick={() => onSelect(base)}
|
}
|
||||||
style={{ textAlign: 'left' }}>
|
onChange={(ids) => {
|
||||||
{base.name}
|
const newSelected = knowledgeState.bases.filter((base) => ids.includes(base.id))
|
||||||
</Button>
|
onSelect(newSelected)
|
||||||
))}
|
}}
|
||||||
</>
|
style={{ width: '200px' }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</SelectorContainer>
|
</SelectorContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, ToolbarButton }) => {
|
const KnowledgeBaseButton: FC<Props> = ({ selectedBases, onSelect, disabled, ToolbarButton }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (selectedBase) {
|
|
||||||
return (
|
|
||||||
<Tooltip placement="top" title={selectedBase.name} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={() => onSelect(undefined)}>
|
|
||||||
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
|
<Tooltip placement="top" title={t('chat.input.knowledge_base')} arrow>
|
||||||
<Popover
|
<Popover
|
||||||
placement="top"
|
placement="top"
|
||||||
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
|
content={<KnowledgeBaseSelector selectedBases={selectedBases} onSelect={onSelect} />}
|
||||||
overlayStyle={{ maxWidth: 400 }}
|
overlayStyle={{ maxWidth: 400 }}
|
||||||
trigger="click">
|
trigger="click">
|
||||||
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
|
<ToolbarButton type="text" disabled={disabled}>
|
||||||
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
<FileSearchOutlined
|
||||||
|
style={{ color: selectedBases && selectedBases?.length > 0 ? 'var(--color-link)' : 'var(--color-icon)' }}
|
||||||
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -28,6 +28,8 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const itemRefs = useRef<Array<HTMLDivElement | null>>([])
|
const itemRefs = useRef<Array<HTMLDivElement | null>>([])
|
||||||
|
// Add a new state to track if menu was dismissed
|
||||||
|
const [menuDismissed, setMenuDismissed] = useState(false)
|
||||||
|
|
||||||
const setItemRef = (index: number, el: HTMLDivElement | null) => {
|
const setItemRef = (index: number, el: HTMLDivElement | null) => {
|
||||||
itemRefs.current[index] = el
|
itemRefs.current[index] = el
|
||||||
@ -44,7 +46,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
|
|
||||||
const handleModelSelect = (model: Model) => {
|
const handleModelSelect = (model: Model) => {
|
||||||
// Check if model is already selected
|
// Check if model is already selected
|
||||||
if (mentionModels.some((selected) => selected.id === model.id)) {
|
if (mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(model))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onSelect(model)
|
onSelect(model)
|
||||||
@ -172,12 +174,13 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
loadPinnedModels()
|
loadPinnedModels()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Scroll to the first menu item when the mode selection menu opens
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (isOpen && flatModelItems.length > 0 && itemRefs.current[0]) {
|
if (isOpen && selectedIndex > -1 && itemRefs.current[selectedIndex]) {
|
||||||
itemRefs.current[0].scrollIntoView({ block: 'nearest' })
|
requestAnimationFrame(() => {
|
||||||
|
itemRefs.current[selectedIndex]?.scrollIntoView({ block: 'nearest' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [isOpen, flatModelItems])
|
}, [isOpen, selectedIndex])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showModelSelector = () => {
|
const showModelSelector = () => {
|
||||||
@ -186,6 +189,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
setSelectedIndex(0)
|
setSelectedIndex(0)
|
||||||
setSearchText('')
|
setSearchText('')
|
||||||
|
setMenuDismissed(false) // Reset dismissed flag when manually showing selector
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@ -209,7 +213,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) {
|
if (selectedIndex >= 0 && selectedIndex < flatModelItems.length) {
|
||||||
const selectedModel = flatModelItems[selectedIndex].model
|
const selectedModel = flatModelItems[selectedIndex].model
|
||||||
if (!mentionModels.some((selected) => selected.id === selectedModel.id)) {
|
if (!mentionModels.some((selected) => getModelUniqId(selected) === getModelUniqId(selectedModel))) {
|
||||||
flatModelItems[selectedIndex].onClick()
|
flatModelItems[selectedIndex].onClick()
|
||||||
}
|
}
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
@ -218,6 +222,7 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
setSearchText('')
|
setSearchText('')
|
||||||
|
setMenuDismissed(true) // Set dismissed flag when Escape is pressed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,10 +235,14 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
|
if (lastAtIndex === -1 || textBeforeCursor.slice(lastAtIndex + 1).includes(' ')) {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
setSearchText('')
|
setSearchText('')
|
||||||
} else if (lastAtIndex !== -1) {
|
setMenuDismissed(false) // Reset dismissed flag when @ is removed
|
||||||
// Get the text after @ for search
|
} else {
|
||||||
const searchStr = textBeforeCursor.slice(lastAtIndex + 1)
|
// Only open menu if it wasn't explicitly dismissed
|
||||||
setSearchText(searchStr)
|
if (!menuDismissed) {
|
||||||
|
setIsOpen(true)
|
||||||
|
const searchStr = textBeforeCursor.slice(lastAtIndex + 1)
|
||||||
|
setSearchText(searchStr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,39 +261,42 @@ const MentionModelsButton: FC<Props> = ({ mentionModels, onMentionModel: onSelec
|
|||||||
textArea.removeEventListener('input', handleTextChange)
|
textArea.removeEventListener('input', handleTextChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isOpen, selectedIndex, flatModelItems, mentionModels])
|
}, [isOpen, selectedIndex, flatModelItems, mentionModels, menuDismissed])
|
||||||
|
|
||||||
// Hide dropdown if no models available
|
|
||||||
if (flatModelItems.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<div ref={menuRef} className="ant-dropdown-menu">
|
<div ref={menuRef} className="ant-dropdown-menu">
|
||||||
{modelMenuItems.map((group, groupIndex) => {
|
{flatModelItems.length > 0 ? (
|
||||||
if (!group) return null
|
modelMenuItems.map((group, groupIndex) => {
|
||||||
|
if (!group) return null
|
||||||
|
|
||||||
// Calculate the starting index for this group's items
|
// Calculate starting index for items in this group
|
||||||
const startIndex = modelMenuItems.slice(0, groupIndex).reduce((acc, g) => acc + (g?.children?.length || 0), 0)
|
const startIndex = modelMenuItems.slice(0, groupIndex).reduce((acc, g) => acc + (g?.children?.length || 0), 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={group.key} className="ant-dropdown-menu-item-group">
|
<div key={group.key} className="ant-dropdown-menu-item-group">
|
||||||
<div className="ant-dropdown-menu-item-group-title">{group.label}</div>
|
<div className="ant-dropdown-menu-item-group-title">{group.label}</div>
|
||||||
<div>
|
<div>
|
||||||
{group.children.map((item, idx) => (
|
{group.children.map((item, idx) => (
|
||||||
<div
|
<div
|
||||||
key={item.key}
|
key={item.key}
|
||||||
ref={(el) => setItemRef(startIndex + idx, el)}
|
ref={(el) => setItemRef(startIndex + idx, el)}
|
||||||
className={`ant-dropdown-menu-item ${selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''}`}
|
className={`ant-dropdown-menu-item ${
|
||||||
onClick={item.onClick}>
|
selectedIndex === startIndex + idx ? 'ant-dropdown-menu-item-selected' : ''
|
||||||
<span className="ant-dropdown-menu-item-icon">{item.icon}</span>
|
}`}
|
||||||
{item.label}
|
onClick={item.onClick}>
|
||||||
</div>
|
<span className="ant-dropdown-menu-item-icon">{item.icon}</span>
|
||||||
))}
|
{item.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
})
|
||||||
})}
|
) : (
|
||||||
|
<div className="ant-dropdown-menu-item-group">
|
||||||
|
<div className="ant-dropdown-menu-item no-results">{t('models.no_matches')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -334,6 +346,17 @@ const DropdownMenuStyle = createGlobalStyle`
|
|||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
cursor: default;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-dropdown-menu-item-group {
|
.ant-dropdown-menu-item-group {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { Flex, Tag } from 'antd'
|
import { Flex, Tag } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@ -13,14 +14,19 @@ const MentionModelsInput: FC<{
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const getProviderName = (model: Model) => {
|
const getProviderName = (model: Model) => {
|
||||||
const provider = providers.find((p) => p.models?.some((m) => m.id === model.id))
|
const provider = providers.find((p) => p.id === model?.provider)
|
||||||
return provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
|
return provider ? (provider.isSystem ? t(`provider.${provider.id}`) : provider.name) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container gap="4px 0" wrap>
|
<Container gap="4px 0" wrap>
|
||||||
{selectedModels.map((model) => (
|
{selectedModels.map((model) => (
|
||||||
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
|
<Tag
|
||||||
|
bordered={false}
|
||||||
|
color="processing"
|
||||||
|
key={getModelUniqId(model)}
|
||||||
|
closable
|
||||||
|
onClose={() => onRemoveModel(model)}>
|
||||||
@{model.name} ({getProviderName(model)})
|
@{model.name} ({getProviderName(model)})
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons'
|
import { DownloadOutlined, ExpandOutlined, LinkOutlined } from '@ant-design/icons'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
import MinApp from '@renderer/components/MinApp'
|
||||||
import { AppLogo } from '@renderer/config/env'
|
import { AppLogo } from '@renderer/config/env'
|
||||||
import { extractTitle } from '@renderer/utils/formats'
|
import { extractTitle } from '@renderer/utils/formats'
|
||||||
@ -13,29 +13,55 @@ interface Props {
|
|||||||
|
|
||||||
const Artifacts: FC<Props> = ({ html }) => {
|
const Artifacts: FC<Props> = ({ html }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const title = extractTitle(html) || 'Artifacts' + ' ' + t('chat.artifacts.button.preview')
|
const title = extractTitle(html) || 'Artifacts ' + t('chat.artifacts.button.preview')
|
||||||
|
|
||||||
const onPreview = async () => {
|
/**
|
||||||
|
* 在应用内打开
|
||||||
|
*/
|
||||||
|
const handleOpenInApp = async () => {
|
||||||
const path = await window.api.file.create('artifacts-preview.html')
|
const path = await window.api.file.create('artifacts-preview.html')
|
||||||
await window.api.file.write(path, html)
|
await window.api.file.write(path, html)
|
||||||
|
const filePath = `file://${path}`
|
||||||
MinApp.start({
|
MinApp.start({
|
||||||
name: title,
|
name: title,
|
||||||
logo: AppLogo,
|
logo: AppLogo,
|
||||||
url: `file://${path}`
|
url: filePath
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部链接打开
|
||||||
|
*/
|
||||||
|
const handleOpenExternal = async () => {
|
||||||
|
const path = await window.api.file.create('artifacts-preview.html')
|
||||||
|
await window.api.file.write(path, html)
|
||||||
|
const filePath = `file://${path}`
|
||||||
|
|
||||||
|
if (window.api.shell && window.api.shell.openExternal) {
|
||||||
|
window.api.shell.openExternal(filePath)
|
||||||
|
} else {
|
||||||
|
console.error(t('artifacts.preview.openExternal.error.content'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
*/
|
||||||
const onDownload = () => {
|
const onDownload = () => {
|
||||||
window.api.file.save(`${title}.html`, html)
|
window.api.file.save(`${title}.html`, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Button type="primary" icon={<ExpandOutlined />} onClick={onPreview} size="small">
|
<Button icon={<ExpandOutlined />} onClick={handleOpenInApp}>
|
||||||
{t('chat.artifacts.button.preview')}
|
{t('chat.artifacts.button.preview')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<DownloadOutlined />} onClick={onDownload} size="small">
|
|
||||||
|
<Button icon={<LinkOutlined />} onClick={handleOpenExternal}>
|
||||||
|
{t('chat.artifacts.button.openExternal')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button icon={<DownloadOutlined />} onClick={onDownload}>
|
||||||
{t('chat.artifacts.button.download')}
|
{t('chat.artifacts.button.download')}
|
||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import Artifacts from './Artifacts'
|
import Artifacts from './Artifacts'
|
||||||
import Mermaid from './Mermaid'
|
import Mermaid from './Mermaid'
|
||||||
|
import { isValidPlantUML, PlantUML } from './PlantUML'
|
||||||
import SvgPreview from './SvgPreview'
|
import SvgPreview from './SvgPreview'
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
@ -62,6 +63,10 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
return <Mermaid chart={children} />
|
return <Mermaid chart={children} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (language === 'plantuml' && isValidPlantUML(children)) {
|
||||||
|
return <PlantUML diagram={children} />
|
||||||
|
}
|
||||||
|
|
||||||
if (language === 'svg') {
|
if (language === 'svg') {
|
||||||
return (
|
return (
|
||||||
<CodeBlockWrapper className="code-block">
|
<CodeBlockWrapper className="code-block">
|
||||||
@ -149,6 +154,7 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
|
if (!text) return
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
|
|||||||
286
src/renderer/src/pages/home/Markdown/PlantUML.tsx
Normal file
286
src/renderer/src/pages/home/Markdown/PlantUML.tsx
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
import { CopyOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||||
|
import { TopView } from '@renderer/components/TopView'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { Button, Modal, Space, Spin, Tabs } from 'antd'
|
||||||
|
import pako from 'pako'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface PlantUMLPopupProps {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
diagram: string
|
||||||
|
}
|
||||||
|
export function isValidPlantUML(diagram: string | null): boolean {
|
||||||
|
if (!diagram || !diagram.trim().startsWith('@start')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const diagramType = diagram.match(/@start(\w+)/)?.[1]
|
||||||
|
|
||||||
|
return diagramType !== undefined && diagram.search(`@end${diagramType}`) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlantUMLServer = 'https://www.plantuml.com/plantuml'
|
||||||
|
function encode64(data: Uint8Array) {
|
||||||
|
let r = ''
|
||||||
|
for (let i = 0; i < data.length; i += 3) {
|
||||||
|
if (i + 2 === data.length) {
|
||||||
|
r += append3bytes(data[i], data[i + 1], 0)
|
||||||
|
} else if (i + 1 === data.length) {
|
||||||
|
r += append3bytes(data[i], 0, 0)
|
||||||
|
} else {
|
||||||
|
r += append3bytes(data[i], data[i + 1], data[i + 2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode6bit(b: number) {
|
||||||
|
if (b < 10) {
|
||||||
|
return String.fromCharCode(48 + b)
|
||||||
|
}
|
||||||
|
b -= 10
|
||||||
|
if (b < 26) {
|
||||||
|
return String.fromCharCode(65 + b)
|
||||||
|
}
|
||||||
|
b -= 26
|
||||||
|
if (b < 26) {
|
||||||
|
return String.fromCharCode(97 + b)
|
||||||
|
}
|
||||||
|
b -= 26
|
||||||
|
if (b === 0) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
if (b === 1) {
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
return '?'
|
||||||
|
}
|
||||||
|
|
||||||
|
function append3bytes(b1: number, b2: number, b3: number) {
|
||||||
|
const c1 = b1 >> 2
|
||||||
|
const c2 = ((b1 & 0x3) << 4) | (b2 >> 4)
|
||||||
|
const c3 = ((b2 & 0xf) << 2) | (b3 >> 6)
|
||||||
|
const c4 = b3 & 0x3f
|
||||||
|
let r = ''
|
||||||
|
r += encode6bit(c1 & 0x3f)
|
||||||
|
r += encode6bit(c2 & 0x3f)
|
||||||
|
r += encode6bit(c3 & 0x3f)
|
||||||
|
r += encode6bit(c4 & 0x3f)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* https://plantuml.com/zh/code-javascript-synchronous
|
||||||
|
* To use PlantUML image generation, a text diagram description have to be :
|
||||||
|
1. Encoded in UTF-8
|
||||||
|
2. Compressed using Deflate algorithm
|
||||||
|
3. Reencoded in ASCII using a transformation _close_ to base64
|
||||||
|
*/
|
||||||
|
function encodeDiagram(diagram: string): string {
|
||||||
|
const utf8text = new TextEncoder().encode(diagram)
|
||||||
|
const compressed = pako.deflateRaw(utf8text)
|
||||||
|
return encode64(compressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlantUMLServerImageProps = {
|
||||||
|
format: 'png' | 'svg'
|
||||||
|
diagram: string
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlantUMLImageUrl(format: 'png' | 'svg', diagram: string, isDark?: boolean) {
|
||||||
|
const encodedDiagram = encodeDiagram(diagram)
|
||||||
|
if (isDark) {
|
||||||
|
return `${PlantUMLServer}/d${format}/${encodedDiagram}`
|
||||||
|
}
|
||||||
|
return `${PlantUMLServer}/${format}/${encodedDiagram}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlantUMLServerImage: React.FC<PlantUMLServerImageProps> = ({ format, diagram, onClick }) => {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const isDark = theme === 'dark'
|
||||||
|
const url = getPlantUMLImageUrl(format, diagram, isDark)
|
||||||
|
return (
|
||||||
|
<StyledPlantUML onClick={onClick}>
|
||||||
|
<Spin
|
||||||
|
spinning={loading}
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined
|
||||||
|
spin
|
||||||
|
style={{
|
||||||
|
fontSize: 32
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
onLoad={() => {
|
||||||
|
setLoading(false)
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
setLoading(false)
|
||||||
|
const target = e.target as HTMLImageElement
|
||||||
|
target.style.opacity = '0.5'
|
||||||
|
target.style.filter = 'blur(2px)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
</StyledPlantUML>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlantUMLPopupCantaier: React.FC<PlantUMLPopupProps> = ({ resolve, diagram }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [downloading, setDownloading] = useState({
|
||||||
|
png: false,
|
||||||
|
svg: false
|
||||||
|
})
|
||||||
|
const [activeTab, setActiveTab] = useState('preview')
|
||||||
|
const { t } = useTranslation()
|
||||||
|
console.log(`plantuml diagram: ${diagram}`)
|
||||||
|
const encodedDiagram = encodeDiagram(diagram)
|
||||||
|
const onOk = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
const handleDownload = (format: 'svg' | 'png') => {
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const url = `${PlantUMLServer}/${format}/${encodedDiagram}`
|
||||||
|
setDownloading((prev) => ({ ...prev, [format]: true }))
|
||||||
|
const filename = `plantuml-diagram-${timestamp}.${format}`
|
||||||
|
downloadUrl(url, filename)
|
||||||
|
.catch(() => {
|
||||||
|
window.message.error(t('plantuml.download.failed'))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setDownloading((prev) => ({ ...prev, [format]: false }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
navigator.clipboard.writeText(diagram)
|
||||||
|
window.message.success(t('message.copy.success'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('plantuml.title')}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
width={1000}
|
||||||
|
centered
|
||||||
|
footer={[
|
||||||
|
<Space key="download-buttons">
|
||||||
|
{activeTab === 'source' && (
|
||||||
|
<Button onClick={handleCopy} icon={<CopyOutlined />}>
|
||||||
|
{t('common.copy')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{activeTab === 'preview' && (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => handleDownload('svg')} loading={downloading.svg}>
|
||||||
|
{t('plantuml.download.svg')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => handleDownload('png')} loading={downloading.png}>
|
||||||
|
{t('plantuml.download.png')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
]}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={(key) => setActiveTab(key)}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'preview',
|
||||||
|
label: t('plantuml.tabs.preview'),
|
||||||
|
children: <PlantUMLServerImage format="svg" diagram={diagram} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'source',
|
||||||
|
label: t('plantuml.tabs.source'),
|
||||||
|
children: (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
maxHeight: 'calc(80vh - 200px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '16px'
|
||||||
|
}}>
|
||||||
|
{diagram}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlantUMLPopupTopView {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide('PlantUMLPopup')
|
||||||
|
}
|
||||||
|
static show(diagram: string) {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PlantUMLPopupCantaier
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
this.hide()
|
||||||
|
}}
|
||||||
|
diagram={diagram}
|
||||||
|
/>,
|
||||||
|
'PlantUMLPopup'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface PlantUMLProps {
|
||||||
|
diagram: string
|
||||||
|
}
|
||||||
|
export const PlantUML: React.FC<PlantUMLProps> = ({ diagram }) => {
|
||||||
|
// const { t } = useTranslation()
|
||||||
|
const onPreview = () => {
|
||||||
|
PlantUMLPopupTopView.show(diagram)
|
||||||
|
}
|
||||||
|
return <PlantUMLServerImage onClick={onPreview} format="svg" diagram={diagram} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledPlantUML = styled.div`
|
||||||
|
max-height: calc(80vh - 100px);
|
||||||
|
text-align: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: 100px;
|
||||||
|
background: var(--color-code-background);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
async function downloadUrl(url: string, filename: string) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
if (!response.ok) {
|
||||||
|
window.message.warning({ content: response.statusText, duration: 1.5 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = filename
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}
|
||||||
@ -6,8 +6,9 @@ import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
|||||||
import { useTopic } from '@renderer/hooks/useTopic'
|
import { useTopic } from '@renderer/hooks/useTopic'
|
||||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
import { getContextCount, getMessageModelId } from '@renderer/services/MessagesService'
|
||||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
|
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/TokenService'
|
||||||
import { Message, Topic } from '@renderer/types'
|
import { Message, Topic } from '@renderer/types'
|
||||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
import { classNames, runAsyncFunction } from '@renderer/utils'
|
||||||
import { Divider } from 'antd'
|
import { Divider } from 'antd'
|
||||||
@ -56,7 +57,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
const [message, setMessage] = useState(_message)
|
const [message, setMessage] = useState(_message)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const model = useModel(getMessageModelId(message)) || message.model
|
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||||
const { isBubbleStyle } = useMessageStyle()
|
const { isBubbleStyle } = useMessageStyle()
|
||||||
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
@ -75,13 +76,22 @@ const MessageItem: FC<Props> = ({
|
|||||||
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
|
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
|
||||||
|
|
||||||
const onEditMessage = useCallback(
|
const onEditMessage = useCallback(
|
||||||
(msg: Message) => {
|
async (msg: Message) => {
|
||||||
|
const usage = await estimateMessageUsage(msg)
|
||||||
|
msg.usage = usage
|
||||||
|
|
||||||
setMessage(msg)
|
setMessage(msg)
|
||||||
const messages = onGetMessages?.()?.map((m) => (m.id === message.id ? msg : m))
|
const messages = onGetMessages?.()?.map((m) => (m.id === message.id ? msg : m))
|
||||||
messages && onSetMessages?.(messages)
|
messages && onSetMessages?.(messages)
|
||||||
topic && db.topics.update(topic.id, { messages })
|
topic && db.topics.update(topic.id, { messages })
|
||||||
|
|
||||||
|
if (messages) {
|
||||||
|
const tokensCount = await estimateHistoryTokens(assistant, messages)
|
||||||
|
const contextCount = getContextCount(assistant, messages)
|
||||||
|
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, { tokensCount, contextCount })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[message.id, onGetMessages, onSetMessages, topic]
|
[message.id, onGetMessages, onSetMessages, topic, assistant]
|
||||||
)
|
)
|
||||||
|
|
||||||
const messageHighlightHandler = (highlight: boolean = true) => {
|
const messageHighlightHandler = (highlight: boolean = true) => {
|
||||||
@ -176,7 +186,7 @@ const MessageItem: FC<Props> = ({
|
|||||||
})}
|
})}
|
||||||
ref={messageContainerRef}
|
ref={messageContainerRef}
|
||||||
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
|
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
|
||||||
<MessageHeader message={message} assistant={assistant} model={model} key={getMessageModelId(message)} />
|
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
|
||||||
<MessageContentContainer
|
<MessageContentContainer
|
||||||
className="message-content-container"
|
className="message-content-container"
|
||||||
style={{ fontFamily, fontSize, background: messageBackground }}>
|
style={{ fontFamily, fontSize, background: messageBackground }}>
|
||||||
@ -240,6 +250,7 @@ const MessageContentContainer = styled.div`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-left: 46px;
|
margin-left: 46px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
overflow-y: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
const MessageFooter = styled.div`
|
const MessageFooter = styled.div`
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { InfoCircleOutlined, SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
import { InfoCircleOutlined, SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { Message, Model } from '@renderer/types'
|
import { Message, Model } from '@renderer/types'
|
||||||
import { getBriefInfo } from '@renderer/utils'
|
import { getBriefInfo } from '@renderer/utils'
|
||||||
import { withMessageThought } from '@renderer/utils/formats'
|
import { withMessageThought } from '@renderer/utils/formats'
|
||||||
@ -77,7 +78,7 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
|
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
|
||||||
{message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)}
|
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<MessageThought message={message} />
|
<MessageThought message={message} />
|
||||||
<Markdown message={{ ...message, content: processedContent }} />
|
<Markdown message={{ ...message, content: processedContent }} />
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
|
import { formatErrorMessage } from '@renderer/utils/error'
|
||||||
import { Alert as AntdAlert } from 'antd'
|
import { Alert as AntdAlert } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -8,8 +9,16 @@ import Markdown from '../Markdown/Markdown'
|
|||||||
const MessageError: FC<{ message: Message }> = ({ message }) => {
|
const MessageError: FC<{ message: Message }> = ({ message }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MessageErrorInfo message={message} />
|
|
||||||
<Markdown message={message} />
|
<Markdown message={message} />
|
||||||
|
{message.error && (
|
||||||
|
<Markdown
|
||||||
|
message={{
|
||||||
|
...message,
|
||||||
|
content: formatErrorMessage(message.error)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MessageErrorInfo message={message} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -27,7 +36,7 @@ const MessageErrorInfo: FC<{ message: Message }> = ({ message }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Alert = styled(AntdAlert)`
|
const Alert = styled(AntdAlert)`
|
||||||
margin-bottom: 15px;
|
margin: 15px 0 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
import { ColumnHeightOutlined, ColumnWidthOutlined, DeleteOutlined, FolderOutlined } from '@ant-design/icons'
|
|
||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
|
||||||
import { HStack } from '@renderer/components/Layout'
|
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||||
import { Message, Model, Topic } from '@renderer/types'
|
import { Message, Topic } from '@renderer/types'
|
||||||
import { Button, Segmented as AntdSegmented } from 'antd'
|
import { Popover } from 'antd'
|
||||||
import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
|
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled, { css } from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
import MessageItem from './Message'
|
import MessageItem from './Message'
|
||||||
|
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: (Message & { index: number })[]
|
messages: (Message & { index: number })[]
|
||||||
@ -32,7 +29,7 @@ const MessageGroup: FC<Props> = ({
|
|||||||
onGetMessages,
|
onGetMessages,
|
||||||
onDeleteGroupMessages
|
onDeleteGroupMessages
|
||||||
}) => {
|
}) => {
|
||||||
const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings()
|
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
||||||
@ -42,8 +39,9 @@ const MessageGroup: FC<Props> = ({
|
|||||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||||
|
|
||||||
const isGrouped = messageLength > 1
|
const isGrouped = messageLength > 1
|
||||||
|
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = useCallback(async () => {
|
||||||
window.modal.confirm({
|
window.modal.confirm({
|
||||||
title: t('message.group.delete.title'),
|
title: t('message.group.delete.title'),
|
||||||
content: t('message.group.delete.content'),
|
content: t('message.group.delete.content'),
|
||||||
@ -57,116 +55,144 @@ const MessageGroup: FC<Props> = ({
|
|||||||
askId && onDeleteGroupMessages?.(askId)
|
askId && onDeleteGroupMessages?.(askId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}, [messages, onDeleteGroupMessages, t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedIndex(messageLength - 1)
|
setSelectedIndex(messageLength - 1)
|
||||||
}, [messageLength])
|
}, [messageLength])
|
||||||
|
|
||||||
const isHorizontal = multiModelMessageStyle === 'horizontal'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
|
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
|
||||||
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
|
<GridContainer $count={messageLength} $layout={multiModelMessageStyle} $gridColumns={gridColumns}>
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => {
|
||||||
<MessageWrapper
|
const isGridGroupMessage = multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped
|
||||||
$layout={multiModelMessageStyle}
|
if (isGridGroupMessage) {
|
||||||
$selected={index === selectedIndex}
|
return (
|
||||||
$isGrouped={isGrouped}
|
<Popover
|
||||||
key={message.id}
|
content={
|
||||||
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
<MessageWrapper
|
||||||
<MessageItem
|
$layout={multiModelMessageStyle}
|
||||||
isGrouped={isGrouped}
|
$selected={index === selectedIndex}
|
||||||
message={message}
|
$isGrouped={isGrouped}
|
||||||
topic={topic}
|
$isInPopover={true}
|
||||||
index={message.index}
|
key={message.id}>
|
||||||
hidePresetMessages={hidePresetMessages}
|
<MessageItem
|
||||||
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }}
|
isGrouped={isGrouped}
|
||||||
onSetMessages={onSetMessages}
|
message={message}
|
||||||
onDeleteMessage={onDeleteMessage}
|
topic={topic}
|
||||||
onGetMessages={onGetMessages}
|
index={message.index}
|
||||||
/>
|
hidePresetMessages={hidePresetMessages}
|
||||||
</MessageWrapper>
|
style={{
|
||||||
))}
|
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
|
||||||
|
}}
|
||||||
|
onSetMessages={onSetMessages}
|
||||||
|
onDeleteMessage={onDeleteMessage}
|
||||||
|
onGetMessages={onGetMessages}
|
||||||
|
/>
|
||||||
|
</MessageWrapper>
|
||||||
|
}
|
||||||
|
trigger={gridPopoverTrigger}
|
||||||
|
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
|
||||||
|
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}
|
||||||
|
key={message.id}>
|
||||||
|
<MessageWrapper
|
||||||
|
$layout={multiModelMessageStyle}
|
||||||
|
$selected={index === selectedIndex}
|
||||||
|
$isGrouped={isGrouped}
|
||||||
|
key={message.id}>
|
||||||
|
<MessageItem
|
||||||
|
isGrouped={isGrouped}
|
||||||
|
message={message}
|
||||||
|
topic={topic}
|
||||||
|
index={message.index}
|
||||||
|
hidePresetMessages={hidePresetMessages}
|
||||||
|
style={
|
||||||
|
gridPopoverTrigger === 'hover' && isGrouped
|
||||||
|
? {
|
||||||
|
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15,
|
||||||
|
overflow: isGrouped ? 'hidden' : 'auto',
|
||||||
|
maxHeight: isGrouped ? '280px' : 'unset'
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onSetMessages={onSetMessages}
|
||||||
|
onDeleteMessage={onDeleteMessage}
|
||||||
|
onGetMessages={onGetMessages}
|
||||||
|
/>
|
||||||
|
</MessageWrapper>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MessageWrapper
|
||||||
|
$layout={multiModelMessageStyle}
|
||||||
|
$selected={index === selectedIndex}
|
||||||
|
$isGrouped={isGrouped}
|
||||||
|
key={message.id}
|
||||||
|
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
||||||
|
<MessageItem
|
||||||
|
isGrouped={isGrouped}
|
||||||
|
message={message}
|
||||||
|
topic={topic}
|
||||||
|
index={message.index}
|
||||||
|
hidePresetMessages={hidePresetMessages}
|
||||||
|
style={{ paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15 }}
|
||||||
|
onSetMessages={onSetMessages}
|
||||||
|
onDeleteMessage={onDeleteMessage}
|
||||||
|
onGetMessages={onGetMessages}
|
||||||
|
/>
|
||||||
|
</MessageWrapper>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</GridContainer>
|
</GridContainer>
|
||||||
{isGrouped && (
|
{isGrouped && (
|
||||||
<GroupMenuBar className="group-menu-bar" $layout={multiModelMessageStyle}>
|
<MessageGroupMenuBar
|
||||||
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
multiModelMessageStyle={multiModelMessageStyle}
|
||||||
<LayoutContainer>
|
setMultiModelMessageStyle={setMultiModelMessageStyle}
|
||||||
{['fold', 'vertical', 'horizontal'].map((layout) => (
|
messages={messages}
|
||||||
<LayoutOption
|
selectedIndex={selectedIndex}
|
||||||
key={layout}
|
setSelectedIndex={setSelectedIndex}
|
||||||
active={multiModelMessageStyle === layout}
|
onDelete={onDelete}
|
||||||
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
|
/>
|
||||||
{layout === 'fold' ? (
|
|
||||||
<FolderOutlined />
|
|
||||||
) : layout === 'horizontal' ? (
|
|
||||||
<ColumnWidthOutlined />
|
|
||||||
) : (
|
|
||||||
<ColumnHeightOutlined />
|
|
||||||
)}
|
|
||||||
</LayoutOption>
|
|
||||||
))}
|
|
||||||
</LayoutContainer>
|
|
||||||
{multiModelMessageStyle === 'fold' && (
|
|
||||||
<ModelsContainer>
|
|
||||||
<Segmented
|
|
||||||
value={selectedIndex.toString()}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSelectedIndex(Number(value))
|
|
||||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
|
|
||||||
}}
|
|
||||||
options={messages.map((message, index) => ({
|
|
||||||
label: (
|
|
||||||
<SegmentedLabel>
|
|
||||||
<ModelAvatar model={message.model as Model} size={20} />
|
|
||||||
<ModelName>{message.model?.name}</ModelName>
|
|
||||||
</SegmentedLabel>
|
|
||||||
),
|
|
||||||
value: index.toString()
|
|
||||||
}))}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</ModelsContainer>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
|
|
||||||
onClick={onDelete}
|
|
||||||
/>
|
|
||||||
</GroupMenuBar>
|
|
||||||
)}
|
)}
|
||||||
</GroupContainer>
|
</GroupContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
|
const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
|
||||||
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && $layout === 'horizontal' ? '15px' : '0')};
|
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && 'horizontal' === $layout ? '15px' : '0')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle }>`
|
const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageStyle; $gridColumns: number }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
|
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
|
||||||
minmax(550px, 1fr)
|
minmax(550px, 1fr)
|
||||||
);
|
);
|
||||||
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
|
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
|
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
|
||||||
minmax(400px, 1fr)
|
minmax(400px, 1fr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
${({ $gridColumns, $layout, $count }) =>
|
||||||
|
$layout === 'grid' &&
|
||||||
|
css`
|
||||||
|
grid-template-columns: repeat(${$count > 1 ? $gridColumns || 2 : 1}, minmax(0, 1fr));
|
||||||
|
grid-template-rows: auto;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
interface MessageWrapperProps {
|
interface MessageWrapperProps {
|
||||||
$layout: 'fold' | 'horizontal' | 'vertical'
|
$layout: 'fold' | 'horizontal' | 'vertical' | 'grid'
|
||||||
$selected: boolean
|
$selected: boolean
|
||||||
$isGrouped: boolean
|
$isGrouped: boolean
|
||||||
|
$isInPopover?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||||
@ -180,6 +206,7 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
|||||||
}
|
}
|
||||||
return 'block'
|
return 'block'
|
||||||
}};
|
}};
|
||||||
|
|
||||||
${({ $layout, $isGrouped }) => {
|
${({ $layout, $isGrouped }) => {
|
||||||
if ($layout === 'horizontal' && $isGrouped) {
|
if ($layout === 'horizontal' && $isGrouped) {
|
||||||
return css`
|
return css`
|
||||||
@ -187,82 +214,26 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow-y: auto;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
${({ $layout, $isInPopover, $isGrouped }) =>
|
||||||
|
$layout === 'grid' && $isGrouped
|
||||||
|
? css`
|
||||||
|
max-height: ${$isInPopover ? '50vh' : '300px'};
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'};
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
|
export default memo(MessageGroup)
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-top: 10px;
|
|
||||||
justify-content: space-between;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 0.5px solid var(--color-border);
|
|
||||||
height: 40px;
|
|
||||||
margin-left: ${({ $layout }) => ($layout === 'horizontal' ? '0' : '40px')};
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
`
|
|
||||||
|
|
||||||
const LayoutContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-direction: row;
|
|
||||||
`
|
|
||||||
|
|
||||||
const LayoutOption = styled.div<{ active: boolean }>`
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'transparent')};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'var(--color-hover)')};
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModelsContainer = styled(Scrollbar)`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Segmented = styled(AntdSegmented)`
|
|
||||||
.ant-segmented-item {
|
|
||||||
background-color: transparent !important;
|
|
||||||
transition: none !important;
|
|
||||||
&:hover {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-segmented-thumb,
|
|
||||||
.ant-segmented-item-selected {
|
|
||||||
background-color: transparent !important;
|
|
||||||
border: 0.5px solid var(--color-border);
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const SegmentedLabel = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 3px 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ModelName = styled.span`
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 12px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default MessageGroup
|
|
||||||
|
|||||||
161
src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx
Normal file
161
src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
ColumnHeightOutlined,
|
||||||
|
ColumnWidthOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
NumberOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||||
|
import { Message, Model } from '@renderer/types'
|
||||||
|
import { Button, Segmented as AntdSegmented } from 'antd'
|
||||||
|
import { FC, memo } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import MessageGroupSettings from './MessageGroupSettings'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
multiModelMessageStyle: MultiModelMessageStyle
|
||||||
|
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
|
||||||
|
messages: Message[]
|
||||||
|
selectedIndex: number
|
||||||
|
setSelectedIndex: (index: number) => void
|
||||||
|
onDelete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageGroupMenuBar: FC<Props> = ({
|
||||||
|
multiModelMessageStyle,
|
||||||
|
setMultiModelMessageStyle,
|
||||||
|
messages,
|
||||||
|
selectedIndex,
|
||||||
|
setSelectedIndex,
|
||||||
|
onDelete
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<GroupMenuBar $layout={multiModelMessageStyle}>
|
||||||
|
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
|
||||||
|
<LayoutContainer>
|
||||||
|
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
|
||||||
|
<LayoutOption
|
||||||
|
key={layout}
|
||||||
|
$active={multiModelMessageStyle === layout}
|
||||||
|
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
|
||||||
|
{layout === 'fold' ? (
|
||||||
|
<FolderOutlined />
|
||||||
|
) : layout === 'horizontal' ? (
|
||||||
|
<ColumnWidthOutlined />
|
||||||
|
) : layout === 'vertical' ? (
|
||||||
|
<ColumnHeightOutlined />
|
||||||
|
) : (
|
||||||
|
<NumberOutlined />
|
||||||
|
)}
|
||||||
|
</LayoutOption>
|
||||||
|
))}
|
||||||
|
</LayoutContainer>
|
||||||
|
{multiModelMessageStyle === 'fold' && (
|
||||||
|
<ModelsContainer>
|
||||||
|
<Segmented
|
||||||
|
value={selectedIndex.toString()}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedIndex(Number(value))
|
||||||
|
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
|
||||||
|
}}
|
||||||
|
options={messages.map((message, index) => ({
|
||||||
|
label: (
|
||||||
|
<SegmentedLabel>
|
||||||
|
<ModelAvatar model={message.model as Model} size={20} />
|
||||||
|
<ModelName>{message.model?.name}</ModelName>
|
||||||
|
</SegmentedLabel>
|
||||||
|
),
|
||||||
|
value: index.toString()
|
||||||
|
}))}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</ModelsContainer>
|
||||||
|
)}
|
||||||
|
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||||
|
</HStack>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
|
</GroupMenuBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
height: 40px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
`
|
||||||
|
|
||||||
|
const LayoutContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-direction: row;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LayoutOption = styled.div<{ $active: boolean }>`
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: ${({ $active }) => ($active ? 'var(--color-background-soft)' : 'transparent')};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ $active }) => ($active ? 'var(--color-background-soft)' : 'var(--color-hover)')};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ModelsContainer = styled(Scrollbar)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Segmented = styled(AntdSegmented)`
|
||||||
|
.ant-segmented-item {
|
||||||
|
background-color: transparent !important;
|
||||||
|
transition: none !important;
|
||||||
|
&:hover {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-segmented-thumb,
|
||||||
|
.ant-segmented-item-selected {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SegmentedLabel = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 3px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ModelName = styled.span`
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(MessageGroupMenuBar)
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { SettingOutlined } from '@ant-design/icons'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { SettingDivider } from '@renderer/pages/settings'
|
||||||
|
import { SettingRow } from '@renderer/pages/settings'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setGridColumns, setGridPopoverTrigger } from '@renderer/store/settings'
|
||||||
|
import { Col, Row, Select, Slider } from 'antd'
|
||||||
|
import { Popover } from 'antd'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const MessageGroupSettings: FC = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { gridColumns, gridPopoverTrigger } = useSettings()
|
||||||
|
const [gridColumnsValue, setGridColumnsValue] = useState(gridColumns)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
trigger={undefined}
|
||||||
|
showArrow
|
||||||
|
content={
|
||||||
|
<div style={{ padding: 10 }}>
|
||||||
|
<SettingRow>
|
||||||
|
<div style={{ marginRight: 10 }}>{t('settings.messages.grid_popover_trigger')}</div>
|
||||||
|
<Select
|
||||||
|
value={gridPopoverTrigger || 'hover'}
|
||||||
|
onChange={(value) => dispatch(setGridPopoverTrigger(value as 'hover' | 'click'))}
|
||||||
|
size="small">
|
||||||
|
<Select.Option value="hover">{t('settings.messages.grid_popover_trigger.hover')}</Select.Option>
|
||||||
|
<Select.Option value="click">{t('settings.messages.grid_popover_trigger.click')}</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<div>{t('settings.messages.grid_columns')}</div>
|
||||||
|
</SettingRow>
|
||||||
|
<Row align="middle" gutter={10}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Slider
|
||||||
|
value={gridColumnsValue}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={(value) => setGridColumnsValue(value)}
|
||||||
|
onChangeComplete={(value) => dispatch(setGridColumns(value))}
|
||||||
|
min={2}
|
||||||
|
max={6}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<SettingOutlined style={{ marginLeft: 15, cursor: 'pointer' }} />
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageGroupSettings
|
||||||
@ -60,12 +60,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
|
|
||||||
const isUserMessage = message.role === 'user'
|
const isUserMessage = message.role === 'user'
|
||||||
|
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCallback(
|
||||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
(e: React.MouseEvent) => {
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
e.stopPropagation()
|
||||||
setCopied(true)
|
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
||||||
setTimeout(() => setCopied(false), 2000)
|
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||||
}, [message.content, t])
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
},
|
||||||
|
[message.content, t]
|
||||||
|
)
|
||||||
|
|
||||||
const onNewBranch = useCallback(async () => {
|
const onNewBranch = useCallback(async () => {
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
@ -195,14 +199,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
[message, onEdit, onNewBranch, t]
|
[message, onEdit, onNewBranch, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onRegenerate = async () => {
|
const onRegenerate = async (e: React.MouseEvent | undefined) => {
|
||||||
|
e?.stopPropagation?.()
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
const selectedModel = isGrouped ? model : assistantModel
|
const selectedModel = isGrouped ? model : assistantModel
|
||||||
const _message = resetAssistantMessage(message, selectedModel)
|
const _message = resetAssistantMessage(message, selectedModel)
|
||||||
onEditMessage?.(_message)
|
onEditMessage?.(_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMentionModel = async () => {
|
const onMentionModel = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
const selectedModel = await SelectModelPopup.show({ model })
|
const selectedModel = await SelectModelPopup.show({ model })
|
||||||
if (!selectedModel) return
|
if (!selectedModel) return
|
||||||
@ -216,9 +222,13 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
onEditMessage?.(_message)
|
onEditMessage?.(_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUseful = useCallback(() => {
|
const onUseful = useCallback(
|
||||||
onEditMessage?.({ ...message, useful: !message.useful })
|
(e: React.MouseEvent) => {
|
||||||
}, [message, onEditMessage])
|
e.stopPropagation()
|
||||||
|
onEditMessage?.({ ...message, useful: !message.useful })
|
||||||
|
},
|
||||||
|
[message, onEditMessage]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||||
@ -270,13 +280,14 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
key: 'translate-close',
|
key: 'translate-close',
|
||||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
onClick: (e) => e.domEvent.stopPropagation()
|
||||||
}}
|
}}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
placement="topRight"
|
placement="topRight"
|
||||||
arrow>
|
arrow>
|
||||||
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
||||||
<ActionButton className="message-action-button">
|
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||||
<TranslationOutlined />
|
<TranslationOutlined />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -298,14 +309,25 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className="message-action-button"
|
className="message-action-button"
|
||||||
onClick={isGrouped ? () => onDeleteMessage?.(message) : undefined}>
|
onClick={
|
||||||
|
isGrouped
|
||||||
|
? (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onDeleteMessage?.(message)
|
||||||
|
}
|
||||||
|
: (e) => e.stopPropagation()
|
||||||
|
}>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
{!isUserMessage && (
|
{!isUserMessage && (
|
||||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
<Dropdown
|
||||||
<ActionButton className="message-action-button">
|
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
|
||||||
|
trigger={['click']}
|
||||||
|
placement="topRight"
|
||||||
|
arrow>
|
||||||
|
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||||
<MenuOutlined />
|
<MenuOutlined />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
import { CheckOutlined } from '@ant-design/icons'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { Collapse } from 'antd'
|
import { Collapse, message as antdMessage, Tooltip } from 'antd'
|
||||||
import { FC, useEffect, useState, useMemo } from 'react'
|
import { FC, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import BarLoader from 'react-spinners/BarLoader'
|
import BarLoader from 'react-spinners/BarLoader'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Markdown from '../Markdown/Markdown'
|
import Markdown from '../Markdown/Markdown'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@ -13,9 +15,10 @@ interface Props {
|
|||||||
|
|
||||||
const MessageThought: FC<Props> = ({ message }) => {
|
const MessageThought: FC<Props> = ({ message }) => {
|
||||||
const [activeKey, setActiveKey] = useState<'thought' | ''>('thought')
|
const [activeKey, setActiveKey] = useState<'thought' | ''>('thought')
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
const isThinking = !message.content
|
const isThinking = !message.content
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { messageFont, fontSize } = useSettings()
|
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings()
|
||||||
const fontFamily = useMemo(() => {
|
const fontFamily = useMemo(() => {
|
||||||
return messageFont === 'serif'
|
return messageFont === 'serif'
|
||||||
? 'serif'
|
? 'serif'
|
||||||
@ -23,19 +26,30 @@ const MessageThought: FC<Props> = ({ message }) => {
|
|||||||
}, [messageFont])
|
}, [messageFont])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isThinking) setActiveKey('')
|
if (!isThinking && thoughtAutoCollapse) setActiveKey('')
|
||||||
}, [isThinking])
|
}, [isThinking, thoughtAutoCollapse])
|
||||||
|
|
||||||
if (!message.reasoning_content) {
|
if (!message.reasoning_content) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyThought = () => {
|
||||||
|
if (message.reasoning_content) {
|
||||||
|
navigator.clipboard.writeText(message.reasoning_content)
|
||||||
|
antdMessage.success({ content: t('message.copied'), key: 'copy-message' })
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const thinkingTime = message.metrics?.time_thinking_millsec || 0
|
const thinkingTime = message.metrics?.time_thinking_millsec || 0
|
||||||
const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1)
|
const thinkingTimeSeconds = (thinkingTime / 1000).toFixed(1)
|
||||||
|
const isPaused = message.status === 'paused'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseContainer
|
<CollapseContainer
|
||||||
activeKey={activeKey}
|
activeKey={activeKey}
|
||||||
|
size="small"
|
||||||
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
onChange={() => setActiveKey((key) => (key ? '' : 'thought'))}
|
||||||
className="message-thought-container"
|
className="message-thought-container"
|
||||||
items={[
|
items={[
|
||||||
@ -46,7 +60,21 @@ const MessageThought: FC<Props> = ({ message }) => {
|
|||||||
<TinkingText>
|
<TinkingText>
|
||||||
{isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSeconds })}
|
{isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSeconds })}
|
||||||
</TinkingText>
|
</TinkingText>
|
||||||
{isThinking && <BarLoader color="#9254de" />}
|
{isThinking && !isPaused && <BarLoader color="#9254de" />}
|
||||||
|
{(!isThinking || isPaused) && (
|
||||||
|
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||||
|
<ActionButton
|
||||||
|
className="message-action-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
copyThought()
|
||||||
|
}}
|
||||||
|
aria-label={t('common.copy')}>
|
||||||
|
{!copied && <i className="iconfont icon-copy"></i>}
|
||||||
|
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</MessageTitleLabel>
|
</MessageTitleLabel>
|
||||||
),
|
),
|
||||||
children: (
|
children: (
|
||||||
@ -76,4 +104,32 @@ const TinkingText = styled.span`
|
|||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const ActionButton = styled.button`
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default MessageThought
|
export default MessageThought
|
||||||
|
|||||||
@ -166,7 +166,8 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
setMessages([])
|
setMessages([])
|
||||||
setDisplayMessages([])
|
setDisplayMessages([])
|
||||||
const defaultTopic = getDefaultTopic(assistant.id)
|
const defaultTopic = getDefaultTopic(assistant.id)
|
||||||
updateTopic({ ...topic, name: defaultTopic.name, messages: [] })
|
const _topic = getTopic(assistant, topic.id)
|
||||||
|
_topic && updateTopic({ ..._topic, name: defaultTopic.name, messages: [] })
|
||||||
TopicManager.clearTopicMessages(topic.id)
|
TopicManager.clearTopicMessages(topic.id)
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {
|
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@ -11,26 +12,30 @@ interface Props {
|
|||||||
|
|
||||||
const Prompt: FC<Props> = ({ assistant, topic }) => {
|
const Prompt: FC<Props> = ({ assistant, topic }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const prompt = assistant.prompt || t('chat.default.description')
|
const prompt = assistant.prompt || t('chat.default.description')
|
||||||
const topicPrompt = topic?.prompt || ''
|
const topicPrompt = topic?.prompt || ''
|
||||||
|
const isDark = theme === 'dark'
|
||||||
|
|
||||||
if (!prompt && !topicPrompt) {
|
if (!prompt && !topicPrompt) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })}>
|
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })} $isDark={isDark}>
|
||||||
<Text>{prompt}</Text>
|
<Text>{prompt}</Text>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div<{ $isDark: boolean }>`
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
margin: 4px 20px 0 20px;
|
margin: 4px 20px 0 20px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 0.5px solid var(--color-border);
|
border: 0.5px solid var(--color-border);
|
||||||
|
background-color: ${({ $isDark }) => ($isDark ? 'var(--color-background-soft)' : 'transparent')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const Text = styled.div`
|
const Text = styled.div`
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
|||||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||||
@ -47,6 +48,11 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
|||||||
SearchPopup.show()
|
SearchPopup.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleNarrowModeToggle = async () => {
|
||||||
|
await modelGenerating()
|
||||||
|
dispatch(setNarrowMode(!narrowMode))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar className="home-navbar">
|
<Navbar className="home-navbar">
|
||||||
{showAssistants && (
|
{showAssistants && (
|
||||||
@ -80,7 +86,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
|||||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
</NarrowIcon>
|
</NarrowIcon>
|
||||||
<NarrowIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
|
<NarrowIcon onClick={handleNarrowModeToggle}>
|
||||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||||
</NarrowIcon>
|
</NarrowIcon>
|
||||||
{sidebarIcons.visible.includes('minapp') && (
|
{sidebarIcons.visible.includes('minapp') && (
|
||||||
|
|||||||
@ -27,7 +27,8 @@ import {
|
|||||||
setPasteLongTextThreshold,
|
setPasteLongTextThreshold,
|
||||||
setRenderInputMessageAsMarkdown,
|
setRenderInputMessageAsMarkdown,
|
||||||
setShowInputEstimatedTokens,
|
setShowInputEstimatedTokens,
|
||||||
setShowMessageDivider
|
setShowMessageDivider,
|
||||||
|
setThoughtAutoCollapse
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { Assistant, AssistantSettings, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
import { Assistant, AssistantSettings, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||||
import { modalConfirm } from '@renderer/utils'
|
import { modalConfirm } from '@renderer/utils'
|
||||||
@ -69,7 +70,8 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
mathEngine,
|
mathEngine,
|
||||||
autoTranslateWithSpace,
|
autoTranslateWithSpace,
|
||||||
pasteLongTextThreshold,
|
pasteLongTextThreshold,
|
||||||
multiModelMessageStyle
|
multiModelMessageStyle,
|
||||||
|
thoughtAutoCollapse
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
|
|
||||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
@ -261,6 +263,20 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.thought_auto_collapse')}
|
||||||
|
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
|
||||||
|
<QuestionIcon style={{ marginLeft: 4 }} />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={thoughtAutoCollapse}
|
||||||
|
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
|
||||||
<Select
|
<Select
|
||||||
@ -283,6 +299,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
|
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
|
||||||
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
|
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
|
||||||
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
|
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
|
||||||
|
<Select.Option value="grid">{t('message.message.multi_model_style.grid')}</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
import DragableList from '@renderer/components/DragableList'
|
import DragableList from '@renderer/components/DragableList'
|
||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -24,7 +25,7 @@ import { exportTopicAsMarkdown, exportTopicToNotion, topicToMarkdown } from '@re
|
|||||||
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
import { Dropdown, MenuProps, Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import { FC, useCallback } from 'react'
|
import { FC, useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -42,6 +43,42 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
|
|
||||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||||
|
|
||||||
|
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||||
|
const deleteTimerRef = useRef<NodeJS.Timeout>()
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if (deleteTimerRef.current) {
|
||||||
|
clearTimeout(deleteTimerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeletingTopicId(topicId)
|
||||||
|
|
||||||
|
deleteTimerRef.current = setTimeout(() => setDeletingTopicId(null), 2000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onClearMessages = useCallback(() => {
|
||||||
|
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||||
|
store.dispatch(setGenerating(false))
|
||||||
|
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleConfirmDelete = useCallback(
|
||||||
|
async (topic: Topic, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (assistant.topics.length === 1) {
|
||||||
|
return onClearMessages()
|
||||||
|
}
|
||||||
|
await modelGenerating()
|
||||||
|
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||||
|
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1])
|
||||||
|
removeTopic(topic)
|
||||||
|
setDeletingTopicId(null)
|
||||||
|
},
|
||||||
|
[assistant.topics, onClearMessages, removeTopic, setActiveTopic]
|
||||||
|
)
|
||||||
|
|
||||||
const onPinTopic = useCallback(
|
const onPinTopic = useCallback(
|
||||||
(topic: Topic) => {
|
(topic: Topic) => {
|
||||||
const updatedTopic = { ...topic, pinned: !topic.pinned }
|
const updatedTopic = { ...topic, pinned: !topic.pinned }
|
||||||
@ -54,7 +91,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
async (topic: Topic) => {
|
async (topic: Topic) => {
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||||
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
|
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? index - 1 : index + 1])
|
||||||
removeTopic(topic)
|
removeTopic(topic)
|
||||||
},
|
},
|
||||||
[assistant.topics, removeTopic, setActiveTopic]
|
[assistant.topics, removeTopic, setActiveTopic]
|
||||||
@ -78,12 +115,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
[setActiveTopic]
|
[setActiveTopic]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onClearMessages = useCallback(() => {
|
|
||||||
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
|
||||||
store.dispatch(setGenerating(false))
|
|
||||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const getTopicMenuItems = useCallback(
|
const getTopicMenuItems = useCallback(
|
||||||
(topic: Topic) => {
|
(topic: Topic) => {
|
||||||
const menus: MenuProps['items'] = [
|
const menus: MenuProps['items'] = [
|
||||||
@ -219,7 +250,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
|
|
||||||
return menus
|
return menus
|
||||||
},
|
},
|
||||||
[assistant, assistants, onClearMessages, onPinTopic, onDeleteTopic, onMoveTopic, t, updateTopic]
|
[assistant, assistants, onClearMessages, onDeleteTopic, onPinTopic, onMoveTopic, t, updateTopic]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -244,17 +275,34 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
)}
|
)}
|
||||||
<MenuButton className="pin">{topic.pinned && <PushpinOutlined />}</MenuButton>
|
<MenuButton className="pin">{topic.pinned && <PushpinOutlined />}</MenuButton>
|
||||||
{isActive && !topic.pinned && (
|
{isActive && !topic.pinned && (
|
||||||
<MenuButton
|
<Tooltip
|
||||||
className="menu"
|
placement="bottom"
|
||||||
onClick={(e) => {
|
mouseEnterDelay={0.7}
|
||||||
e.stopPropagation()
|
title={
|
||||||
if (assistant.topics.length === 1) {
|
<div>
|
||||||
return onClearMessages()
|
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
|
||||||
}
|
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
|
||||||
onDeleteTopic(topic)
|
</div>
|
||||||
}}>
|
</div>
|
||||||
<CloseOutlined />
|
}>
|
||||||
</MenuButton>
|
<MenuButton
|
||||||
|
className="menu"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
handleConfirmDelete(topic, e)
|
||||||
|
} else if (deletingTopicId === topic.id) {
|
||||||
|
handleConfirmDelete(topic, e)
|
||||||
|
} else {
|
||||||
|
handleDeleteClick(topic.id, e)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{deletingTopicId === topic.id ? (
|
||||||
|
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
||||||
|
) : (
|
||||||
|
<CloseOutlined />
|
||||||
|
)}
|
||||||
|
</MenuButton>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</TopicListItem>
|
</TopicListItem>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
|||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { getProviderName } from '@renderer/services/ProviderService'
|
import { getProviderName } from '@renderer/services/ProviderService'
|
||||||
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
|
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
|
||||||
import { bookExts, documentExts, textExts } from '@shared/config/constant'
|
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||||
import { Alert, Button, Card, Divider, message, Tag, Tooltip, Typography, Upload } from 'antd'
|
import { Alert, Button, Card, Divider, message, Tag, Tooltip, Typography, Upload } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -35,7 +35,7 @@ interface KnowledgeContentProps {
|
|||||||
selectedBase: KnowledgeBase
|
selectedBase: KnowledgeBase
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileTypes = [...bookExts, ...documentExts, ...textExts]
|
const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts]
|
||||||
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -53,6 +53,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
addSitemap,
|
addSitemap,
|
||||||
removeItem,
|
removeItem,
|
||||||
getProcessingStatus,
|
getProcessingStatus,
|
||||||
|
getDirectoryProcessingPercent,
|
||||||
addNote,
|
addNote,
|
||||||
addDirectory
|
addDirectory
|
||||||
} = useKnowledge(selectedBase.id || '')
|
} = useKnowledge(selectedBase.id || '')
|
||||||
@ -64,6 +65,8 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const progressingPercent = getDirectoryProcessingPercent(base?.id)
|
||||||
|
|
||||||
const handleAddFile = () => {
|
const handleAddFile = () => {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return
|
return
|
||||||
@ -217,7 +220,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
style={{ marginTop: 10, background: 'transparent' }}>
|
style={{ marginTop: 10, background: 'transparent' }}>
|
||||||
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
|
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
|
||||||
<p className="ant-upload-hint">
|
<p className="ant-upload-hint">
|
||||||
{t('knowledge.file_hint', { file_types: fileTypes.slice(0, 6).join(', ').replaceAll('.', '') })}
|
{t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })}
|
||||||
</p>
|
</p>
|
||||||
</Dragger>
|
</Dragger>
|
||||||
</FileSection>
|
</FileSection>
|
||||||
@ -239,7 +242,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} type="file" />
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
@ -271,7 +274,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
<StatusIcon
|
||||||
|
sourceId={item.id}
|
||||||
|
base={base}
|
||||||
|
getProcessingStatus={getProcessingStatus}
|
||||||
|
progressingPercent={progressingPercent}
|
||||||
|
type="directory"
|
||||||
|
/>
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
@ -303,7 +312,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} type="url" />
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
@ -335,7 +344,12 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
|
<StatusIcon
|
||||||
|
sourceId={item.id}
|
||||||
|
base={base}
|
||||||
|
getProcessingStatus={getProcessingStatus}
|
||||||
|
type="sitemap"
|
||||||
|
/>
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
@ -362,7 +376,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
|||||||
<FlexAlignCenter>
|
<FlexAlignCenter>
|
||||||
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
||||||
<StatusIconWrapper>
|
<StatusIconWrapper>
|
||||||
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} />
|
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} type="note" />
|
||||||
</StatusIconWrapper>
|
</StatusIconWrapper>
|
||||||
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
||||||
</FlexAlignCenter>
|
</FlexAlignCenter>
|
||||||
|
|||||||
@ -65,6 +65,7 @@ const KnowledgePage: FC = () => {
|
|||||||
title: t('knowledge.delete_confirm'),
|
title: t('knowledge.delete_confirm'),
|
||||||
centered: true,
|
centered: true,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
|
setSelectedBase(undefined)
|
||||||
deleteKnowledgeBase(base.id)
|
deleteKnowledgeBase(base.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
|
||||||
import { KnowledgeBase, ProcessingStatus } from '@renderer/types'
|
import { KnowledgeBase, ProcessingStatus } from '@renderer/types'
|
||||||
import { Tooltip } from 'antd'
|
import { Progress, Tooltip } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -9,9 +9,11 @@ interface StatusIconProps {
|
|||||||
sourceId: string
|
sourceId: string
|
||||||
base: KnowledgeBase
|
base: KnowledgeBase
|
||||||
getProcessingStatus: (sourceId: string) => ProcessingStatus | undefined
|
getProcessingStatus: (sourceId: string) => ProcessingStatus | undefined
|
||||||
|
progressingPercent?: number
|
||||||
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }) => {
|
const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus, progressingPercent, type }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const status = getProcessingStatus(sourceId)
|
const status = getProcessingStatus(sourceId)
|
||||||
const item = base.items.find((item) => item.id === sourceId)
|
const item = base.items.find((item) => item.id === sourceId)
|
||||||
@ -39,12 +41,16 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
|
|||||||
<StatusDot $status="pending" />
|
<StatusDot $status="pending" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
case 'processing':
|
|
||||||
return (
|
case 'processing': {
|
||||||
|
return type === 'directory' ? (
|
||||||
|
<Progress type="circle" size={14} percent={Number(progressingPercent?.toFixed(0))} />
|
||||||
|
) : (
|
||||||
<Tooltip title={t('knowledge.status_processing')} placement="left">
|
<Tooltip title={t('knowledge.status_processing')} placement="left">
|
||||||
<StatusDot $status="processing" />
|
<StatusDot $status="processing" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t('knowledge.status_completed')} placement="left">
|
<Tooltip title={t('knowledge.status_completed')} placement="left">
|
||||||
|
|||||||
@ -16,18 +16,14 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const knowledgeState = useAppSelector((state) => state.knowledge)
|
const knowledgeState = useAppSelector((state) => state.knowledge)
|
||||||
const knowledgeOptions: SelectProps['options'] = []
|
const knowledgeOptions: SelectProps['options'] = knowledgeState.bases.map((base) => ({
|
||||||
|
label: base.name,
|
||||||
knowledgeState.bases.forEach((base) => {
|
value: base.id
|
||||||
knowledgeOptions.push({
|
}))
|
||||||
label: base.name,
|
|
||||||
value: base.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const onUpdate = (value) => {
|
const onUpdate = (value) => {
|
||||||
const knowledge_base = knowledgeState.bases.find((t) => t.id === value)
|
const knowledge_bases = value.map((id) => knowledgeState.bases.find((b) => b.id === id))
|
||||||
const _assistant = { ...assistant, knowledge_base }
|
const _assistant = { ...assistant, knowledge_bases }
|
||||||
updateAssistant(_assistant)
|
updateAssistant(_assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,12 +33,18 @@ const AssistantKnowledgeBaseSettings: React.FC<Props> = ({ assistant, updateAssi
|
|||||||
{t('common.knowledge_base')}
|
{t('common.knowledge_base')}
|
||||||
</Box>
|
</Box>
|
||||||
<Select
|
<Select
|
||||||
|
mode="multiple"
|
||||||
allowClear
|
allowClear
|
||||||
defaultValue={assistant.knowledge_base?.id}
|
value={assistant.knowledge_bases?.map((b) => b.id)}
|
||||||
placeholder={t('agents.add.knowledge_base.placeholder')}
|
placeholder={t('agents.add.knowledge_base.placeholder')}
|
||||||
menuItemSelectedIcon={<CheckOutlined />}
|
menuItemSelectedIcon={<CheckOutlined />}
|
||||||
options={knowledgeOptions}
|
options={knowledgeOptions}
|
||||||
onChange={(value) => onUpdate(value)}
|
onChange={(value) => onUpdate(value)}
|
||||||
|
filterOption={(input, option) =>
|
||||||
|
String(option?.label ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(input.toLowerCase())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
|
||||||
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||||
const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort ?? 'medium')
|
const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort)
|
||||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||||
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
||||||
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
||||||
@ -172,6 +172,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
setDefaultModel(selectedModel)
|
setDefaultModel(selectedModel)
|
||||||
updateAssistant({
|
updateAssistant({
|
||||||
...assistant,
|
...assistant,
|
||||||
|
model: selectedModel,
|
||||||
defaultModel: selectedModel
|
defaultModel: selectedModel
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -391,6 +392,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
|||||||
<Radio.Button value="low">{t('assistants.settings.reasoning_effort.low')}</Radio.Button>
|
<Radio.Button value="low">{t('assistants.settings.reasoning_effort.low')}</Radio.Button>
|
||||||
<Radio.Button value="medium">{t('assistants.settings.reasoning_effort.medium')}</Radio.Button>
|
<Radio.Button value="medium">{t('assistants.settings.reasoning_effort.medium')}</Radio.Button>
|
||||||
<Radio.Button value="high">{t('assistants.settings.reasoning_effort.high')}</Radio.Button>
|
<Radio.Button value="high">{t('assistants.settings.reasoning_effort.high')}</Radio.Button>
|
||||||
|
<Radio.Button value={undefined}>{t('assistants.settings.reasoning_effort.off')}</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
import 'emoji-picker-element'
|
||||||
|
|
||||||
|
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||||
import { Box, HStack } from '@renderer/components/Layout'
|
import { Box, HStack } from '@renderer/components/Layout'
|
||||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||||
import { Button, Input } from 'antd'
|
import { getLeadingEmoji } from '@renderer/utils'
|
||||||
|
import { Button, Input, Popover } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -14,12 +18,19 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant, onOk }) => {
|
const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant, onOk }) => {
|
||||||
const [name, setName] = useState(assistant.name)
|
const [emoji, setEmoji] = useState(getLeadingEmoji(assistant.name) || '⭐️')
|
||||||
|
const [name, setName] = useState(assistant.name.replace(getLeadingEmoji(assistant.name) || '', '').trim())
|
||||||
const [prompt, setPrompt] = useState(assistant.prompt)
|
const [prompt, setPrompt] = useState(assistant.prompt)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onUpdate = () => {
|
const onUpdate = () => {
|
||||||
const _assistant = { ...assistant, name, prompt }
|
const _assistant = { ...assistant, name: `${emoji} ${name}`.trim(), prompt }
|
||||||
|
updateAssistant(_assistant)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmojiSelect = (selectedEmoji: string) => {
|
||||||
|
setEmoji(selectedEmoji)
|
||||||
|
const _assistant = { ...assistant, name: `${selectedEmoji} ${name}`.trim(), prompt }
|
||||||
updateAssistant(_assistant)
|
updateAssistant(_assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,12 +39,18 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant,
|
|||||||
<Box mb={8} style={{ fontWeight: 'bold' }}>
|
<Box mb={8} style={{ fontWeight: 'bold' }}>
|
||||||
{t('common.name')}
|
{t('common.name')}
|
||||||
</Box>
|
</Box>
|
||||||
<Input
|
<HStack gap={8} alignItems="center">
|
||||||
placeholder={t('common.assistant') + t('common.name')}
|
<Popover content={<EmojiPicker onEmojiClick={handleEmojiSelect} />} arrow>
|
||||||
value={name}
|
<Button style={{ fontSize: 20, padding: '4px 8px', minWidth: '42px', height: '32px' }}>{emoji}</Button>
|
||||||
onChange={(e) => setName(e.target.value)}
|
</Popover>
|
||||||
onBlur={onUpdate}
|
<Input
|
||||||
/>
|
placeholder={t('common.assistant') + t('common.name')}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onBlur={onUpdate}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
<Box mt={8} mb={8} style={{ fontWeight: 'bold' }}>
|
<Box mt={8} mb={8} style={{ fontWeight: 'bold' }}>
|
||||||
{t('common.prompt')}
|
{t('common.prompt')}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
import { FileSearchOutlined, FolderOpenOutlined, InfoCircleOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
import { Client } from '@notionhq/client'
|
import { Client } from '@notionhq/client'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import MinApp from '@renderer/components/MinApp'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { backup, reset, restore } from '@renderer/services/BackupService'
|
import { backup, reset, restore } from '@renderer/services/BackupService'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import { setNotionApiKey, setNotionDatabaseID } from '@renderer/store/settings'
|
import { setNotionApiKey, setNotionDatabaseID, setNotionPageNameKey } from '@renderer/store/settings'
|
||||||
import { AppInfo } from '@renderer/types'
|
import { AppInfo } from '@renderer/types'
|
||||||
import { Button, Modal, Typography } from 'antd'
|
import { Button, Modal, Tooltip, Typography } from 'antd'
|
||||||
import Input from 'antd/es/input/Input'
|
import Input from 'antd/es/input/Input'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -22,10 +23,9 @@ const NotionSettings: FC = () => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
// 这里可以添加 Notion 相关的状态和逻辑
|
|
||||||
// 例如:
|
|
||||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||||
|
const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey)
|
||||||
|
|
||||||
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
dispatch(setNotionApiKey(e.target.value))
|
dispatch(setNotionApiKey(e.target.value))
|
||||||
@ -34,6 +34,11 @@ const NotionSettings: FC = () => {
|
|||||||
const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
dispatch(setNotionDatabaseID(e.target.value))
|
dispatch(setNotionDatabaseID(e.target.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNotionPageNameKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(setNotionPageNameKey(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
const handleNotionConnectionCheck = () => {
|
const handleNotionConnectionCheck = () => {
|
||||||
if (notionApiKey === null) {
|
if (notionApiKey === null) {
|
||||||
window.message.error(t('settings.data.notion.check.empty_api_key'))
|
window.message.error(t('settings.data.notion.check.empty_api_key'))
|
||||||
@ -60,36 +65,66 @@ const NotionSettings: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNotionTitleClick = () => {
|
||||||
|
MinApp.start({
|
||||||
|
id: 'notion-help',
|
||||||
|
name: 'Notion Help',
|
||||||
|
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingGroup theme={theme}>
|
<SettingGroup theme={theme}>
|
||||||
<SettingTitle>{t('settings.data.notion.title')}</SettingTitle>
|
<SettingTitle style={{ justifyContent: 'flex-start', gap: 10 }}>
|
||||||
|
{t('settings.data.notion.title')}
|
||||||
|
<Tooltip title={t('settings.data.notion.help')} placement="right">
|
||||||
|
<InfoCircleOutlined
|
||||||
|
style={{ color: 'var(--color-text-2)', cursor: 'pointer' }}
|
||||||
|
onClick={handleNotionTitleClick}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</SettingTitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
||||||
<HStack alignItems="center" gap="5px">
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={notionDatabaseID || ''}
|
value={notionDatabaseID || ''}
|
||||||
onChange={handleNotionDatabaseIdChange}
|
onChange={handleNotionDatabaseIdChange}
|
||||||
onBlur={handleNotionDatabaseIdChange}
|
onBlur={handleNotionDatabaseIdChange}
|
||||||
style={{ width: 315 }}
|
style={{ width: 315 }}
|
||||||
|
placeholder={t('settings.data.notion.database_id_placeholder')}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.data.notion.page_name_key')}</SettingRowTitle>
|
||||||
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={notionPageNameKey || ''}
|
||||||
|
onChange={handleNotionPageNameKeyChange}
|
||||||
|
onBlur={handleNotionPageNameKeyChange}
|
||||||
|
style={{ width: 315 }}
|
||||||
|
placeholder={t('settings.data.notion.page_name_key_placeholder')}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
|
||||||
<HStack alignItems="center" gap="5px">
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
value={notionApiKey || ''}
|
value={notionApiKey || ''}
|
||||||
onChange={handleNotionTokenChange}
|
onChange={handleNotionTokenChange}
|
||||||
onBlur={handleNotionTokenChange}
|
onBlur={handleNotionTokenChange}
|
||||||
style={{ width: 250 }}
|
style={{ width: 250 }}
|
||||||
|
placeholder={t('settings.data.notion.api_key_placeholder')}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleNotionConnectionCheck} style={{ width: 60 }}>
|
<Button onClick={handleNotionConnectionCheck}>{t('settings.data.notion.check.button')}</Button>
|
||||||
{t('settings.data.notion.check.button')}
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider /> {/* 添加分割线 */}
|
<SettingDivider /> {/* 添加分割线 */}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const WebDavSettings: FC = () => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const { t, i18n } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { webdavSync } = useRuntime()
|
const { webdavSync } = useRuntime()
|
||||||
|
|
||||||
@ -163,7 +163,6 @@ const WebDavSettings: FC = () => {
|
|||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
<HStack gap="5px" justifyContent="space-between">
|
<HStack gap="5px" justifyContent="space-between">
|
||||||
{/* 添加 在线备份 在线还原 按钮 */}
|
|
||||||
<Button onClick={onBackup} icon={<SaveOutlined />} loading={backuping}>
|
<Button onClick={onBackup} icon={<SaveOutlined />} loading={backuping}>
|
||||||
{t('settings.data.webdav.backup.button')}
|
{t('settings.data.webdav.backup.button')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -177,19 +176,15 @@ const WebDavSettings: FC = () => {
|
|||||||
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
|
||||||
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
|
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
|
||||||
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
|
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
|
||||||
<Select.Option value={1}>
|
<Select.Option value={1}>{t('settings.data.webdav.minute_interval', { count: 1 })}</Select.Option>
|
||||||
1 {i18n.language === 'en-US' ? t('settings.data.webdav.minute') : t('settings.data.webdav.minutes')}
|
<Select.Option value={5}>{t('settings.data.webdav.minute_interval', { count: 5 })}</Select.Option>
|
||||||
</Select.Option>
|
<Select.Option value={15}>{t('settings.data.webdav.minute_interval', { count: 15 })}</Select.Option>
|
||||||
<Select.Option value={5}>5 {t('settings.data.webdav.minutes')}</Select.Option>
|
<Select.Option value={30}>{t('settings.data.webdav.minute_interval', { count: 30 })}</Select.Option>
|
||||||
<Select.Option value={15}>15 {t('settings.data.webdav.minutes')}</Select.Option>
|
<Select.Option value={60}>{t('settings.data.webdav.hour_interval', { count: 1 })}</Select.Option>
|
||||||
<Select.Option value={30}>30 {t('settings.data.webdav.minutes')}</Select.Option>
|
<Select.Option value={120}>{t('settings.data.webdav.hour_interval', { count: 2 })}</Select.Option>
|
||||||
<Select.Option value={60}>
|
<Select.Option value={360}>{t('settings.data.webdav.hour_interval', { count: 6 })}</Select.Option>
|
||||||
1 {i18n.language === 'en-US' ? t('settings.data.webdav.hour') : t('settings.data.webdav.hours')}
|
<Select.Option value={720}>{t('settings.data.webdav.hour_interval', { count: 12 })}</Select.Option>
|
||||||
</Select.Option>
|
<Select.Option value={1440}>{t('settings.data.webdav.hour_interval', { count: 24 })}</Select.Option>
|
||||||
<Select.Option value={120}>2 {t('settings.data.webdav.hours')}</Select.Option>
|
|
||||||
<Select.Option value={360}>6 {t('settings.data.webdav.hours')}</Select.Option>
|
|
||||||
<Select.Option value={720}>12 {t('settings.data.webdav.hours')}</Select.Option>
|
|
||||||
<Select.Option value={1440}>24 {t('settings.data.webdav.hours')}</Select.Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{webdavSync && syncInterval > 0 && (
|
{webdavSync && syncInterval > 0 && (
|
||||||
|
|||||||
@ -23,50 +23,6 @@ interface MiniAppManagerProps {
|
|||||||
|
|
||||||
type ListType = 'visible' | 'disabled'
|
type ListType = 'visible' | 'disabled'
|
||||||
|
|
||||||
// 添加 reorderLists 函数的接口定义
|
|
||||||
interface ReorderListsParams {
|
|
||||||
sourceList: MinAppType[]
|
|
||||||
destList: MinAppType[]
|
|
||||||
sourceIndex: number
|
|
||||||
destIndex: number
|
|
||||||
isSameList: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReorderListsResult {
|
|
||||||
sourceList: MinAppType[]
|
|
||||||
destList: MinAppType[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 reorderLists 函数
|
|
||||||
const reorderLists = ({
|
|
||||||
sourceList,
|
|
||||||
destList,
|
|
||||||
sourceIndex,
|
|
||||||
destIndex,
|
|
||||||
isSameList
|
|
||||||
}: ReorderListsParams): ReorderListsResult => {
|
|
||||||
if (isSameList) {
|
|
||||||
// 在同一列表内重新排序
|
|
||||||
const newList = [...sourceList]
|
|
||||||
const [removed] = newList.splice(sourceIndex, 1)
|
|
||||||
newList.splice(destIndex, 0, removed)
|
|
||||||
return {
|
|
||||||
sourceList: newList,
|
|
||||||
destList: destList
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 在不同列表间移动
|
|
||||||
const newSourceList = [...sourceList]
|
|
||||||
const [removed] = newSourceList.splice(sourceIndex, 1)
|
|
||||||
const newDestList = [...destList]
|
|
||||||
newDestList.splice(destIndex, 0, removed)
|
|
||||||
return {
|
|
||||||
sourceList: newSourceList,
|
|
||||||
destList: newDestList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
||||||
visibleMiniApps,
|
visibleMiniApps,
|
||||||
disabledMiniApps,
|
disabledMiniApps,
|
||||||
@ -92,25 +48,35 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
|||||||
if (!result.destination) return
|
if (!result.destination) return
|
||||||
|
|
||||||
const { source, destination } = result
|
const { source, destination } = result
|
||||||
const sourceList = source.droppableId as ListType
|
|
||||||
const destList = destination.droppableId as ListType
|
|
||||||
|
|
||||||
if (source.droppableId === destination.droppableId) return
|
if (source.droppableId === destination.droppableId) {
|
||||||
|
// 在同一列表内重新排序
|
||||||
|
const list = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
|
||||||
|
const [removed] = list.splice(source.index, 1)
|
||||||
|
list.splice(destination.index, 0, removed)
|
||||||
|
|
||||||
const newLists = reorderLists({
|
if (source.droppableId === 'visible') {
|
||||||
sourceList: sourceList === 'visible' ? visibleMiniApps : disabledMiniApps,
|
handleListUpdate(list, disabledMiniApps)
|
||||||
destList: destList === 'visible' ? visibleMiniApps : disabledMiniApps,
|
} else {
|
||||||
sourceIndex: source.index,
|
handleListUpdate(visibleMiniApps, list)
|
||||||
destIndex: destination.index,
|
}
|
||||||
isSameList: sourceList === destList
|
return
|
||||||
})
|
}
|
||||||
|
|
||||||
handleListUpdate(
|
// 在不同列表间移动
|
||||||
sourceList === 'visible' ? newLists.sourceList : newLists.destList,
|
const sourceList = source.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
|
||||||
sourceList === 'visible' ? newLists.destList : newLists.sourceList
|
const destList = destination.droppableId === 'visible' ? [...visibleMiniApps] : [...disabledMiniApps]
|
||||||
)
|
|
||||||
|
const [removed] = sourceList.splice(source.index, 1)
|
||||||
|
const targetList = destList.filter((app) => app.id !== removed.id)
|
||||||
|
targetList.splice(destination.index, 0, removed)
|
||||||
|
|
||||||
|
const newVisibleMiniApps = destination.droppableId === 'visible' ? targetList : sourceList
|
||||||
|
const newDisabledMiniApps = destination.droppableId === 'disabled' ? targetList : sourceList
|
||||||
|
|
||||||
|
handleListUpdate(newVisibleMiniApps, newDisabledMiniApps)
|
||||||
},
|
},
|
||||||
[disabledMiniApps, handleListUpdate, visibleMiniApps]
|
[visibleMiniApps, disabledMiniApps, handleListUpdate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onMoveMiniApp = useCallback(
|
const onMoveMiniApp = useCallback(
|
||||||
@ -153,17 +119,15 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
|||||||
<Droppable droppableId={listType}>
|
<Droppable droppableId={listType}>
|
||||||
{(provided: DroppableProvided) => (
|
{(provided: DroppableProvided) => (
|
||||||
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
|
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
<ScrollContainer>
|
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
|
||||||
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
|
<Draggable key={program.id} draggableId={String(program.id)} index={index}>
|
||||||
<Draggable key={program.id} draggableId={String(program.id)} index={index}>
|
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
|
||||||
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
|
</Draggable>
|
||||||
</Draggable>
|
))}
|
||||||
))}
|
{disabledMiniApps.length === 0 && listType === 'disabled' && (
|
||||||
{disabledMiniApps.length === 0 && listType === 'disabled' && (
|
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
|
||||||
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
|
)}
|
||||||
)}
|
{provided.placeholder}
|
||||||
{provided.placeholder}
|
|
||||||
</ScrollContainer>
|
|
||||||
</ProgramList>
|
</ProgramList>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
@ -181,12 +145,6 @@ const AppLogo = styled.img`
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ScrollContainer = styled.div`
|
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
|
||||||
padding-right: 5px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ProgramSection = styled.div`
|
const ProgramSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
@ -208,13 +166,29 @@ const ProgramList = styled.div`
|
|||||||
height: 365px;
|
height: 365px;
|
||||||
min-height: 365px;
|
min-height: 365px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
padding-right: 5px;
|
|
||||||
background: var(--color-background-soft);
|
background: var(--color-background-soft);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
display: flex;
|
overflow-y: auto;
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: hidden;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-border-hover);
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ProgramItem = styled.div`
|
const ProgramItem = styled.div`
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import ModelTags from '@renderer/components/ModelTags'
|
import ModelTags from '@renderer/components/ModelTags'
|
||||||
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
|
import OAuthButton from '@renderer/components/OAuth/OAuthButton'
|
||||||
import { EMBEDDING_REGEX, getModelLogo, REASONING_REGEX, VISION_REGEX } from '@renderer/config/models'
|
import { getModelLogo, isEmbeddingModel, isReasoningModel, isVisionModel } from '@renderer/config/models'
|
||||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
@ -192,9 +192,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
const modelTypeContent = (model: Model) => {
|
const modelTypeContent = (model: Model) => {
|
||||||
// 获取默认选中的类型
|
// 获取默认选中的类型
|
||||||
const defaultTypes = [
|
const defaultTypes = [
|
||||||
...(VISION_REGEX.test(model.id) ? ['vision'] : []),
|
...(isVisionModel(model) ? ['vision'] : []),
|
||||||
...(EMBEDDING_REGEX.test(model.id) ? ['embedding'] : []),
|
...(isEmbeddingModel(model) ? ['embedding'] : []),
|
||||||
...(REASONING_REGEX.test(model.id) ? ['reasoning'] : [])
|
...(isReasoningModel(model) ? ['reasoning'] : [])
|
||||||
] as ModelType[]
|
] as ModelType[]
|
||||||
|
|
||||||
// 合并现有选择和默认类型
|
// 合并现有选择和默认类型
|
||||||
@ -206,9 +206,21 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
value={selectedTypes}
|
value={selectedTypes}
|
||||||
onChange={(types) => onUpdateModelTypes(model, types as ModelType[])}
|
onChange={(types) => onUpdateModelTypes(model, types as ModelType[])}
|
||||||
options={[
|
options={[
|
||||||
{ label: t('models.type.vision'), value: 'vision', disabled: VISION_REGEX.test(model.id) },
|
{
|
||||||
{ label: t('models.type.embedding'), value: 'embedding', disabled: EMBEDDING_REGEX.test(model.id) },
|
label: t('models.type.vision'),
|
||||||
{ label: t('models.type.reasoning'), value: 'reasoning', disabled: REASONING_REGEX.test(model.id) }
|
value: 'vision',
|
||||||
|
disabled: isVisionModel(model)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('models.type.embedding'),
|
||||||
|
value: 'embedding',
|
||||||
|
disabled: isEmbeddingModel(model)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('models.type.reasoning'),
|
||||||
|
value: 'reasoning',
|
||||||
|
disabled: isReasoningModel(model)
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -59,7 +59,8 @@ const ShortcutSettings: FC = () => {
|
|||||||
const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
const hasModifier = keys.some((key) => ['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
||||||
const hasNonModifier = keys.some((key) => !['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
const hasNonModifier = keys.some((key) => !['Control', 'Ctrl', 'Command', 'Alt', 'Shift'].includes(key))
|
||||||
|
|
||||||
if (isMac && keys.includes('Alt')) {
|
// only allows option + space
|
||||||
|
if (isMac && keys[0] === 'Alt' && !['Space', undefined].includes(keys[1])) {
|
||||||
window.message.warning({
|
window.message.warning({
|
||||||
content: t('settings.shortcuts.alt_warning'),
|
content: t('settings.shortcuts.alt_warning'),
|
||||||
key: 'shortcut-alt-warning'
|
key: 'shortcut-alt-warning'
|
||||||
|
|||||||
@ -1,18 +1,27 @@
|
|||||||
import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
|
import {
|
||||||
|
CheckOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
HistoryOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
WarningOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
import { translateLanguageOptions } from '@renderer/config/translate'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||||
import { Assistant, Message } from '@renderer/types'
|
import { Assistant, Message, TranslateHistory } from '@renderer/types'
|
||||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||||
import { Button, Select, Space } from 'antd'
|
import { Button, Dropdown, Empty, Flex, Popconfirm, Select, Space, Tooltip } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -29,11 +38,42 @@ const TranslatePage: FC = () => {
|
|||||||
const { translateModel } = useDefaultModel()
|
const { translateModel } = useDefaultModel()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
||||||
|
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const textAreaRef = useRef<TextAreaRef>(null)
|
||||||
|
|
||||||
|
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
_result = result
|
_result = result
|
||||||
_targetLanguage = targetLanguage
|
_targetLanguage = targetLanguage
|
||||||
|
|
||||||
|
const saveTranslateHistory = async (
|
||||||
|
sourceText: string,
|
||||||
|
targetText: string,
|
||||||
|
sourceLanguage: string,
|
||||||
|
targetLanguage: string
|
||||||
|
) => {
|
||||||
|
const history: TranslateHistory = {
|
||||||
|
id: uuid(),
|
||||||
|
sourceText,
|
||||||
|
targetText,
|
||||||
|
sourceLanguage,
|
||||||
|
targetLanguage,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
console.log('🌟TEO🌟 ~ saveTranslateHistory ~ history:', history)
|
||||||
|
await db.translate_history.add(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteHistory = async (id: string) => {
|
||||||
|
db.translate_history.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearHistory = async () => {
|
||||||
|
db.translate_history.clear()
|
||||||
|
}
|
||||||
|
|
||||||
const onTranslate = async () => {
|
const onTranslate = async () => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
return
|
return
|
||||||
@ -62,7 +102,17 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
await fetchTranslate({ message, assistant, onResponse: (text) => setResult(text) })
|
let translatedText = ''
|
||||||
|
await fetchTranslate({
|
||||||
|
message,
|
||||||
|
assistant,
|
||||||
|
onResponse: (text) => {
|
||||||
|
translatedText = text
|
||||||
|
setResult(text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await saveTranslateHistory(text, translatedText, 'any', targetLanguage)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +122,12 @@ const TranslatePage: FC = () => {
|
|||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onHistoryItemClick = (history: TranslateHistory) => {
|
||||||
|
setText(history.sourceText)
|
||||||
|
setResult(history.targetText)
|
||||||
|
setTargetLanguage(history.targetLanguage)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isEmpty(text) && setResult('')
|
isEmpty(text) && setResult('')
|
||||||
}, [text])
|
}, [text])
|
||||||
@ -83,6 +139,13 @@ const TranslatePage: FC = () => {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
onTranslate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SettingButton = () => {
|
const SettingButton = () => {
|
||||||
if (isLocalAi) {
|
if (isLocalAi) {
|
||||||
return null
|
return null
|
||||||
@ -109,104 +172,163 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container id="translate-page">
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}>
|
||||||
|
{t('translate.title')}
|
||||||
|
<Button
|
||||||
|
className="nodrag"
|
||||||
|
color="default"
|
||||||
|
variant={historyDrawerVisible ? 'filled' : 'text'}
|
||||||
|
type="text"
|
||||||
|
icon={<HistoryOutlined />}
|
||||||
|
onClick={() => setHistoryDrawerVisible(!historyDrawerVisible)}
|
||||||
|
/>
|
||||||
|
</NavbarCenter>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container" ref={contentContainerRef} $historyDrawerVisible={historyDrawerVisible}>
|
||||||
<MenuContainer>
|
<HistoryContainner $historyDrawerVisible={historyDrawerVisible}>
|
||||||
<Select
|
<OperationBar>
|
||||||
showSearch
|
<span style={{ fontSize: 16 }}>{t('translate.history.title')}</span>
|
||||||
value="any"
|
{!isEmpty(translateHistory) && (
|
||||||
style={{ width: 180 }}
|
<Popconfirm
|
||||||
optionFilterProp="label"
|
title={t('translate.history.clear')}
|
||||||
disabled
|
description={t('translate.history.clear_description')}
|
||||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
onConfirm={clearHistory}>
|
||||||
/>
|
<Button type="text" size="small" danger icon={<DeleteOutlined />}>
|
||||||
<SwapOutlined />
|
{t('translate.history.clear')}
|
||||||
<Select
|
</Button>
|
||||||
showSearch
|
</Popconfirm>
|
||||||
value={targetLanguage}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
optionFilterProp="label"
|
|
||||||
options={TranslateLanguageOptions}
|
|
||||||
onChange={(value) => {
|
|
||||||
setTargetLanguage(value)
|
|
||||||
db.settings.put({ id: 'translate:target:language', value })
|
|
||||||
}}
|
|
||||||
optionRender={(option) => (
|
|
||||||
<Space>
|
|
||||||
<span role="img" aria-label={option.data.label}>
|
|
||||||
{option.data.emoji}
|
|
||||||
</span>
|
|
||||||
{option.label}
|
|
||||||
</Space>
|
|
||||||
)}
|
)}
|
||||||
|
</OperationBar>
|
||||||
|
{translateHistory && translateHistory.length ? (
|
||||||
|
<HistoryList>
|
||||||
|
{translateHistory.map((item) => (
|
||||||
|
<Dropdown
|
||||||
|
key={item.id}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: t('translate.history.delete'),
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
danger: true,
|
||||||
|
onClick: () => deleteHistory(item.id)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}>
|
||||||
|
<HistoryListItem onClick={() => onHistoryItemClick(item)}>
|
||||||
|
<Flex justify="space-between" vertical gap={4} style={{ width: '100%' }}>
|
||||||
|
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
|
||||||
|
<HistoryListItemTitle>{item.targetText}</HistoryListItemTitle>
|
||||||
|
<HistoryListItemDate>{dayjs(item.createdAt).format('MM/DD HH:mm')}</HistoryListItemDate>
|
||||||
|
</Flex>
|
||||||
|
</HistoryListItem>
|
||||||
|
</Dropdown>
|
||||||
|
))}
|
||||||
|
</HistoryList>
|
||||||
|
) : (
|
||||||
|
<Flex justify="center" align="center" style={{ flex: 1 }}>
|
||||||
|
<Empty description={t('translate.history.empty')} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</HistoryContainner>
|
||||||
|
|
||||||
|
<InputContainer>
|
||||||
|
<OperationBar>
|
||||||
|
<Flex align="center" gap={20}>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
value="any"
|
||||||
|
style={{ width: 180 }}
|
||||||
|
optionFilterProp="label"
|
||||||
|
disabled
|
||||||
|
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
||||||
|
/>
|
||||||
|
<SettingButton />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
mouseEnterDelay={0.5}
|
||||||
|
styles={{ body: { fontSize: '12px' } }}
|
||||||
|
title={
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
Enter: {t('translate.button.translate')}
|
||||||
|
<br />
|
||||||
|
Shift + Enter: {t('translate.tooltip.newline')}
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<TranslateButton
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={onTranslate}
|
||||||
|
disabled={!text.trim()}
|
||||||
|
icon={<SendOutlined />}>
|
||||||
|
{t('translate.button.translate')}
|
||||||
|
</TranslateButton>
|
||||||
|
</Tooltip>
|
||||||
|
</OperationBar>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
ref={textAreaRef}
|
||||||
|
variant="borderless"
|
||||||
|
placeholder={t('translate.input.placeholder')}
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
disabled={loading}
|
||||||
|
spellCheck={false}
|
||||||
|
allowClear
|
||||||
/>
|
/>
|
||||||
<SettingButton />
|
</InputContainer>
|
||||||
</MenuContainer>
|
|
||||||
<TranslateInputWrapper>
|
<OutputContainer>
|
||||||
<InputContainer>
|
<OperationBar>
|
||||||
<Textarea
|
<Select
|
||||||
variant="borderless"
|
showSearch
|
||||||
placeholder={t('translate.input.placeholder')}
|
value={targetLanguage}
|
||||||
value={text}
|
style={{ width: 180 }}
|
||||||
onChange={(e) => setText(e.target.value)}
|
optionFilterProp="label"
|
||||||
disabled={loading}
|
options={translateLanguageOptions()}
|
||||||
spellCheck={false}
|
onChange={(value) => {
|
||||||
allowClear
|
setTargetLanguage(value)
|
||||||
|
db.settings.put({ id: 'translate:target:language', value })
|
||||||
|
}}
|
||||||
|
optionRender={(option) => (
|
||||||
|
<Space>
|
||||||
|
<span role="img" aria-label={option.data.label}>
|
||||||
|
{option.data.emoji}
|
||||||
|
</span>
|
||||||
|
{option.label}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<TranslateButton
|
|
||||||
type="primary"
|
|
||||||
loading={loading}
|
|
||||||
onClick={onTranslate}
|
|
||||||
disabled={!text.trim()}
|
|
||||||
icon={<SendOutlined />}>
|
|
||||||
{t('translate.button.translate')}
|
|
||||||
</TranslateButton>
|
|
||||||
</InputContainer>
|
|
||||||
<OutputContainer>
|
|
||||||
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
|
|
||||||
<CopyButton
|
<CopyButton
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
disabled={!result}
|
disabled={!result}
|
||||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
||||||
/>
|
/>
|
||||||
</OutputContainer>
|
</OperationBar>
|
||||||
</TranslateInputWrapper>
|
|
||||||
|
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
|
||||||
|
</OutputContainer>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div<{ $historyDrawerVisible: boolean }>`
|
||||||
display: flex;
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr 1fr;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
padding: 20px 15px;
|
||||||
height: 100%;
|
position: relative;
|
||||||
padding: 20px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const MenuContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
gap: 20px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TranslateInputWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
min-height: 350px;
|
|
||||||
gap: 20px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const InputContainer = styled.div`
|
const InputContainer = styled.div`
|
||||||
@ -214,52 +336,136 @@ const InputContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
border: 1px solid var(--color-border-soft);
|
border: 1px solid var(--color-border-soft);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-right: 2px;
|
||||||
|
margin-right: 15px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const OperationBar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 10px 8px 10px 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Textarea = styled(TextArea)`
|
const Textarea = styled(TextArea)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
overflow: auto;
|
border-radius: 0;
|
||||||
.ant-input {
|
.ant-input {
|
||||||
resize: none;
|
resize: none;
|
||||||
padding: 15px 20px;
|
padding: 5px 16px;
|
||||||
|
}
|
||||||
|
.ant-input-clear-icon {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const OutputContainer = styled.div`
|
const OutputContainer = styled.div`
|
||||||
|
min-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-right: 2px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const OutputText = styled.div`
|
const OutputText = styled.div`
|
||||||
padding: 5px 10px;
|
min-height: 0;
|
||||||
max-height: calc(100vh - var(--navbar-height) - 120px);
|
flex: 1;
|
||||||
overflow: auto;
|
padding: 5px 16px;
|
||||||
|
overflow-y: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TranslateButton = styled(Button)`
|
const TranslateButton = styled(Button)``
|
||||||
position: absolute;
|
|
||||||
right: 15px;
|
const CopyButton = styled(Button)``
|
||||||
bottom: 15px;
|
|
||||||
z-index: 10;
|
const HistoryContainner = styled.div<{ $historyDrawerVisible: boolean }>`
|
||||||
|
width: ${({ $historyDrawerVisible }) => ($historyDrawerVisible ? '300px' : '0')};
|
||||||
|
height: calc(100vh - var(--navbar-height) - 40px);
|
||||||
|
transition:
|
||||||
|
width 0.2s,
|
||||||
|
opacity 0.2s;
|
||||||
|
border: 1px solid var(--color-border-soft);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-right: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 2px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
${({ $historyDrawerVisible }) =>
|
||||||
|
!$historyDrawerVisible &&
|
||||||
|
`
|
||||||
|
border: none;
|
||||||
|
margin-right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
const CopyButton = styled(Button)`
|
const HistoryList = styled.div`
|
||||||
position: absolute;
|
flex: 1;
|
||||||
right: 15px;
|
display: flex;
|
||||||
bottom: 15px;
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryListItem = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: var(--list-item-border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
button {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
border-bottom: 1px dashed var(--color-border-soft);
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryListItemTitle = styled.div`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const HistoryListItemDate = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
`
|
`
|
||||||
|
|
||||||
export default TranslatePage
|
export default TranslatePage
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import OpenAI from 'openai'
|
|||||||
|
|
||||||
import { CompletionsParams } from '.'
|
import { CompletionsParams } from '.'
|
||||||
import BaseProvider from './BaseProvider'
|
import BaseProvider from './BaseProvider'
|
||||||
|
|
||||||
export default class AnthropicProvider extends BaseProvider {
|
export default class AnthropicProvider extends BaseProvider {
|
||||||
private sdk: Anthropic
|
private sdk: Anthropic
|
||||||
|
|
||||||
@ -108,9 +107,14 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastUserMessage = _messages.findLast((m) => m.role === 'user')
|
||||||
|
|
||||||
|
const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id)
|
||||||
|
const { signal } = abortController
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const stream = this.sdk.messages
|
const stream = this.sdk.messages
|
||||||
.stream({ ...body, stream: true })
|
.stream({ ...body, stream: true }, { signal })
|
||||||
.on('text', (text) => {
|
.on('text', (text) => {
|
||||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||||
stream.controller.abort()
|
stream.controller.abort()
|
||||||
@ -146,7 +150,7 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
.on('error', (error) => reject(error))
|
.on('error', (error) => reject(error))
|
||||||
})
|
}).finally(cleanup)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {
|
public async translate(message: Message, assistant: Assistant, onResponse?: (text: string) => void) {
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import { getLMStudioKeepAliveTime } from '@renderer/hooks/useLMStudio'
|
|||||||
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
||||||
import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
import type { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||||
import { delay, isJSON, parseJSON } from '@renderer/utils'
|
import { delay, isJSON, parseJSON } from '@renderer/utils'
|
||||||
import OpenAI from 'openai'
|
import { addAbortController, removeAbortController } from '@renderer/utils/abortController'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import type OpenAI from 'openai'
|
||||||
|
|
||||||
import { CompletionsParams } from '.'
|
import type { CompletionsParams } from '.'
|
||||||
|
|
||||||
export default abstract class BaseProvider {
|
export default abstract class BaseProvider {
|
||||||
protected provider: Provider
|
protected provider: Provider
|
||||||
@ -83,21 +85,35 @@ export default abstract class BaseProvider {
|
|||||||
return message.content
|
return message.content
|
||||||
}
|
}
|
||||||
|
|
||||||
const knowledgeId = message.knowledgeBaseIds[0]
|
const bases = store.getState().knowledge.bases.filter((kb) => message.knowledgeBaseIds?.includes(kb.id))
|
||||||
const base = store.getState().knowledge.bases.find((kb) => kb.id === knowledgeId)
|
|
||||||
|
|
||||||
if (!base) {
|
if (!bases || bases.length === 0) {
|
||||||
return message.content
|
return message.content
|
||||||
}
|
}
|
||||||
|
|
||||||
const { referencesContent, referencesCount } = await getKnowledgeReferences(base, message)
|
const allReferencesPromises = bases.map(async (base) => {
|
||||||
|
const references = await getKnowledgeReferences(base, message)
|
||||||
|
|
||||||
// 如果知识库中未检索到内容则使用通用逻辑
|
return {
|
||||||
if (referencesCount === 0) {
|
knowledgeBaseId: base.id,
|
||||||
|
references
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const allReferences = (await Promise.all(allReferencesPromises))
|
||||||
|
.filter((result) => result.references && result.references.length > 0)
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
if (allReferences.length === 0) {
|
||||||
|
window.message.info({
|
||||||
|
content: t('knowledge.no_match'),
|
||||||
|
duration: 4,
|
||||||
|
key: 'knowledge-base-no-match-info'
|
||||||
|
})
|
||||||
return message.content
|
return message.content
|
||||||
}
|
}
|
||||||
|
const allReferencesContent = `\`\`\`json\n${JSON.stringify(allReferences, null, 2)}\n\`\`\``
|
||||||
|
|
||||||
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', referencesContent)
|
return REFERENCE_PROMPT.replace('{question}', message.content).replace('{references}', allReferencesContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getCustomParameters(assistant: Assistant) {
|
protected getCustomParameters(assistant: Assistant) {
|
||||||
@ -120,4 +136,21 @@ export default abstract class BaseProvider {
|
|||||||
}, {}) || {}
|
}, {}) || {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected createAbortController(messageId?: string) {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
addAbortController(messageId, () => abortController.abort())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
abortController,
|
||||||
|
cleanup: () => {
|
||||||
|
if (messageId) {
|
||||||
|
removeAbortController(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import OpenAI from 'openai'
|
|||||||
|
|
||||||
import { CompletionsParams } from '.'
|
import { CompletionsParams } from '.'
|
||||||
import BaseProvider from './BaseProvider'
|
import BaseProvider from './BaseProvider'
|
||||||
|
|
||||||
export default class GeminiProvider extends BaseProvider {
|
export default class GeminiProvider extends BaseProvider {
|
||||||
private sdk: GoogleGenerativeAI
|
private sdk: GoogleGenerativeAI
|
||||||
private requestOptions: RequestOptions
|
private requestOptions: RequestOptions
|
||||||
@ -204,7 +203,11 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessagesStream = await chat.sendMessageStream(messageContents.parts)
|
const lastUserMessage = userMessages.findLast((m) => m.role === 'user')
|
||||||
|
const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id)
|
||||||
|
const { signal } = abortController
|
||||||
|
|
||||||
|
const userMessagesStream = await chat.sendMessageStream(messageContents.parts, { signal }).finally(cleanup)
|
||||||
let time_first_token_millsec = 0
|
let time_first_token_millsec = 0
|
||||||
|
|
||||||
for await (const chunk of userMessagesStream.stream) {
|
for await (const chunk of userMessagesStream.stream) {
|
||||||
|
|||||||
@ -213,21 +213,31 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
let time_first_token_millsec = 0
|
let time_first_token_millsec = 0
|
||||||
let time_first_content_millsec = 0
|
let time_first_content_millsec = 0
|
||||||
const start_time_millsec = new Date().getTime()
|
const start_time_millsec = new Date().getTime()
|
||||||
|
const lastUserMessage = _messages.findLast((m) => m.role === 'user')
|
||||||
|
const { abortController, cleanup } = this.createAbortController(lastUserMessage?.id)
|
||||||
|
const { signal } = abortController
|
||||||
|
|
||||||
// @ts-ignore key is not typed
|
const stream = await this.sdk.chat.completions
|
||||||
const stream = await this.sdk.chat.completions.create({
|
// @ts-ignore key is not typed
|
||||||
model: model.id,
|
.create(
|
||||||
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
{
|
||||||
temperature: this.getTemperature(assistant, model),
|
model: model.id,
|
||||||
top_p: this.getTopP(assistant, model),
|
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
|
||||||
max_tokens: maxTokens,
|
temperature: this.getTemperature(assistant, model),
|
||||||
keep_alive: this.keepAliveTime,
|
top_p: this.getTopP(assistant, model),
|
||||||
stream: isSupportStreamOutput(),
|
max_tokens: maxTokens,
|
||||||
...this.getReasoningEffort(assistant, model),
|
keep_alive: this.keepAliveTime,
|
||||||
...getOpenAIWebSearchParams(assistant, model),
|
stream: isSupportStreamOutput(),
|
||||||
...this.getProviderSpecificParameters(assistant, model),
|
...this.getReasoningEffort(assistant, model),
|
||||||
...this.getCustomParameters(assistant)
|
...getOpenAIWebSearchParams(assistant, model),
|
||||||
})
|
...this.getProviderSpecificParameters(assistant, model),
|
||||||
|
...this.getCustomParameters(assistant)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.finally(cleanup)
|
||||||
|
|
||||||
if (!isSupportStreamOutput()) {
|
if (!isSupportStreamOutput()) {
|
||||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import i18n from '@renderer/i18n'
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
|
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||||
import { formatErrorMessage, formatMessageError } from '@renderer/utils/error'
|
import { addAbortController } from '@renderer/utils/abortController'
|
||||||
|
import { formatMessageError } from '@renderer/utils/error'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
import AiProvider from '../providers/AiProvider'
|
import AiProvider from '../providers/AiProvider'
|
||||||
@ -16,7 +17,6 @@ import {
|
|||||||
import { EVENT_NAMES, EventEmitter } from './EventService'
|
import { EVENT_NAMES, EventEmitter } from './EventService'
|
||||||
import { filterMessages, filterUsefulMessages } from './MessagesService'
|
import { filterMessages, filterUsefulMessages } from './MessagesService'
|
||||||
import { estimateMessagesUsage } from './TokenService'
|
import { estimateMessagesUsage } from './TokenService'
|
||||||
|
|
||||||
export async function fetchChatCompletion({
|
export async function fetchChatCompletion({
|
||||||
message,
|
message,
|
||||||
messages,
|
messages,
|
||||||
@ -37,18 +37,14 @@ export async function fetchChatCompletion({
|
|||||||
|
|
||||||
onResponse({ ...message })
|
onResponse({ ...message })
|
||||||
|
|
||||||
// Handle paused state
|
const pauseFn = (message: Message) => {
|
||||||
let paused = false
|
message.status = 'paused'
|
||||||
const timer = setInterval(() => {
|
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
|
||||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
store.dispatch(setGenerating(false))
|
||||||
paused = true
|
onResponse({ ...message, status: 'paused' })
|
||||||
message.status = 'paused'
|
}
|
||||||
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
|
|
||||||
store.dispatch(setGenerating(false))
|
addAbortController(message.askId ?? message.id, pauseFn.bind(null, message))
|
||||||
onResponse({ ...message, status: 'paused' })
|
|
||||||
clearInterval(timer)
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let _messages: Message[] = []
|
let _messages: Message[] = []
|
||||||
@ -94,16 +90,9 @@ export async function fetchChatCompletion({
|
|||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.status = 'error'
|
message.status = 'error'
|
||||||
message.content = formatErrorMessage(error)
|
|
||||||
message.error = formatMessageError(error)
|
message.error = formatMessageError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer && clearInterval(timer)
|
|
||||||
|
|
||||||
if (paused) {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update message status
|
// Update message status
|
||||||
message.status = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED) ? 'paused' : message.status
|
message.status = window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED) ? 'paused' : message.status
|
||||||
|
|
||||||
|
|||||||
@ -30,8 +30,8 @@ export function getDefaultTranslateAssistant(targetLanguage: string, text: strin
|
|||||||
|
|
||||||
assistant.prompt = store
|
assistant.prompt = store
|
||||||
.getState()
|
.getState()
|
||||||
.settings.translateModelPrompt.replace('{{target_language}}', targetLanguage)
|
.settings.translateModelPrompt.replaceAll('{{target_language}}', targetLanguage)
|
||||||
.replace('{{text}}', text)
|
.replaceAll('{{text}}', text)
|
||||||
return assistant
|
return assistant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT, DEFAULT_KNOWLEDGE_THRESHOLD } from '@
|
|||||||
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||||
import AiProvider from '@renderer/providers/AiProvider'
|
import AiProvider from '@renderer/providers/AiProvider'
|
||||||
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
|
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
|
||||||
import { t } from 'i18next'
|
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
|
|
||||||
import { getProviderByModel } from './AssistantService'
|
import { getProviderByModel } from './AssistantService'
|
||||||
@ -91,14 +90,6 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
|
|||||||
return item.score >= threshold
|
return item.score >= threshold
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
if (searchResults.length === 0) {
|
|
||||||
window.message.info({
|
|
||||||
content: t('knowledge.no_match'),
|
|
||||||
duration: 4,
|
|
||||||
key: 'knowledge-base-no-match-info'
|
|
||||||
})
|
|
||||||
return { referencesContent: '', referencesCount: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
const _searchResults = await Promise.all(
|
const _searchResults = await Promise.all(
|
||||||
searchResults.map(async (item) => {
|
searchResults.map(async (item) => {
|
||||||
@ -121,7 +112,5 @@ export const getKnowledgeReferences = async (base: KnowledgeBase, message: Messa
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const referencesContent = `\`\`\`json\n${JSON.stringify(references, null, 2)}\n\`\`\``
|
return references
|
||||||
|
|
||||||
return { referencesContent, referencesCount: references.length }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
|
import { t } from 'i18next'
|
||||||
import { pick } from 'lodash'
|
import { pick } from 'lodash'
|
||||||
|
|
||||||
export const getModelUniqId = (m?: Model) => {
|
export const getModelUniqId = (m?: Model) => {
|
||||||
@ -17,5 +18,13 @@ export const hasModel = (m?: Model) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getModelName(model?: Model) {
|
export function getModelName(model?: Model) {
|
||||||
return model?.name || model?.id || ''
|
const provider = store.getState().llm.providers.find((p) => p.id === model?.provider)
|
||||||
|
const modelName = model?.name || model?.id || ''
|
||||||
|
|
||||||
|
if (provider) {
|
||||||
|
const providerName = provider?.isSystem ? t(`provider.${provider.id}`) : provider?.name
|
||||||
|
return `${modelName} | ${providerName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelName
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 69,
|
version: 70,
|
||||||
blacklist: ['runtime'],
|
blacklist: ['runtime'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -152,7 +152,7 @@ const initialState: LlmState = {
|
|||||||
name: 'DMXAPI',
|
name: 'DMXAPI',
|
||||||
type: 'openai',
|
type: 'openai',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiHost: 'https://api.dmxapi.com',
|
apiHost: 'https://www.dmxapi.cn',
|
||||||
models: SYSTEM_MODELS.dmxapi,
|
models: SYSTEM_MODELS.dmxapi,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@ -1071,6 +1071,32 @@ const migrateConfig = {
|
|||||||
state.minapps.enabled.push(coze)
|
state.minapps.enabled.push(coze)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.settings.gridColumns = 2
|
||||||
|
state.settings.gridPopoverTrigger = 'hover'
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
'70': (state: RootState) => {
|
||||||
|
state.llm.providers.forEach((provider) => {
|
||||||
|
if (provider.id === 'dmxapi') {
|
||||||
|
provider.apiHost = 'https://www.dmxapi.cn'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
'71': (state: RootState) => {
|
||||||
|
const appIds = ['dify', 'wpslingxi', 'lechat', 'abacus', 'lambdachat']
|
||||||
|
|
||||||
|
if (state.minapps) {
|
||||||
|
appIds.forEach((id) => {
|
||||||
|
const app = DEFAULT_MIN_APPS.find((app) => app.id === id)
|
||||||
|
if (app) {
|
||||||
|
state.minapps.enabled.push(app)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state.settings.thoughtAutoCollapse = true
|
||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,8 @@ export interface SettingsState {
|
|||||||
mathEngine: 'MathJax' | 'KaTeX'
|
mathEngine: 'MathJax' | 'KaTeX'
|
||||||
messageStyle: 'plain' | 'bubble'
|
messageStyle: 'plain' | 'bubble'
|
||||||
codeStyle: CodeStyleVarious
|
codeStyle: CodeStyleVarious
|
||||||
|
gridColumns: number
|
||||||
|
gridPopoverTrigger: 'hover' | 'click'
|
||||||
// webdav 配置 host, user, pass, path
|
// webdav 配置 host, user, pass, path
|
||||||
webdavHost: string
|
webdavHost: string
|
||||||
webdavUser: string
|
webdavUser: string
|
||||||
@ -67,9 +69,11 @@ export interface SettingsState {
|
|||||||
multiModelMessageStyle: MultiModelMessageStyle
|
multiModelMessageStyle: MultiModelMessageStyle
|
||||||
notionDatabaseID: string | null
|
notionDatabaseID: string | null
|
||||||
notionApiKey: string | null
|
notionApiKey: string | null
|
||||||
|
notionPageNameKey: string | null
|
||||||
|
thoughtAutoCollapse: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold'
|
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
showAssistants: true,
|
showAssistants: true,
|
||||||
@ -99,6 +103,8 @@ const initialState: SettingsState = {
|
|||||||
mathEngine: 'KaTeX',
|
mathEngine: 'KaTeX',
|
||||||
messageStyle: 'plain',
|
messageStyle: 'plain',
|
||||||
codeStyle: 'auto',
|
codeStyle: 'auto',
|
||||||
|
gridColumns: 2,
|
||||||
|
gridPopoverTrigger: 'hover',
|
||||||
webdavHost: '',
|
webdavHost: '',
|
||||||
webdavUser: '',
|
webdavUser: '',
|
||||||
webdavPass: '',
|
webdavPass: '',
|
||||||
@ -119,7 +125,9 @@ const initialState: SettingsState = {
|
|||||||
clickTrayToShowQuickAssistant: false,
|
clickTrayToShowQuickAssistant: false,
|
||||||
multiModelMessageStyle: 'fold',
|
multiModelMessageStyle: 'fold',
|
||||||
notionDatabaseID: '',
|
notionDatabaseID: '',
|
||||||
notionApiKey: ''
|
notionApiKey: '',
|
||||||
|
notionPageNameKey: 'Name',
|
||||||
|
thoughtAutoCollapse: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -224,6 +232,12 @@ const settingsSlice = createSlice({
|
|||||||
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
||||||
state.mathEngine = action.payload
|
state.mathEngine = action.payload
|
||||||
},
|
},
|
||||||
|
setGridColumns: (state, action: PayloadAction<number>) => {
|
||||||
|
state.gridColumns = action.payload
|
||||||
|
},
|
||||||
|
setGridPopoverTrigger: (state, action: PayloadAction<'hover' | 'click'>) => {
|
||||||
|
state.gridPopoverTrigger = action.payload
|
||||||
|
},
|
||||||
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
|
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
|
||||||
state.messageStyle = action.payload
|
state.messageStyle = action.payload
|
||||||
},
|
},
|
||||||
@ -265,7 +279,7 @@ const settingsSlice = createSlice({
|
|||||||
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
|
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
|
||||||
state.enableQuickAssistant = action.payload
|
state.enableQuickAssistant = action.payload
|
||||||
},
|
},
|
||||||
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => {
|
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold' | 'grid'>) => {
|
||||||
state.multiModelMessageStyle = action.payload
|
state.multiModelMessageStyle = action.payload
|
||||||
},
|
},
|
||||||
setNotionDatabaseID: (state, action: PayloadAction<string>) => {
|
setNotionDatabaseID: (state, action: PayloadAction<string>) => {
|
||||||
@ -273,6 +287,12 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setNotionApiKey: (state, action: PayloadAction<string>) => {
|
setNotionApiKey: (state, action: PayloadAction<string>) => {
|
||||||
state.notionApiKey = action.payload
|
state.notionApiKey = action.payload
|
||||||
|
},
|
||||||
|
setNotionPageNameKey: (state, action: PayloadAction<string>) => {
|
||||||
|
state.notionPageNameKey = action.payload
|
||||||
|
},
|
||||||
|
setThoughtAutoCollapse: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.thoughtAutoCollapse = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -310,6 +330,8 @@ export const {
|
|||||||
setCodeShowLineNumbers,
|
setCodeShowLineNumbers,
|
||||||
setCodeCollapsible,
|
setCodeCollapsible,
|
||||||
setMathEngine,
|
setMathEngine,
|
||||||
|
setGridColumns,
|
||||||
|
setGridPopoverTrigger,
|
||||||
setMessageStyle,
|
setMessageStyle,
|
||||||
setCodeStyle,
|
setCodeStyle,
|
||||||
setTranslateModelPrompt,
|
setTranslateModelPrompt,
|
||||||
@ -324,7 +346,9 @@ export const {
|
|||||||
setEnableQuickAssistant,
|
setEnableQuickAssistant,
|
||||||
setMultiModelMessageStyle,
|
setMultiModelMessageStyle,
|
||||||
setNotionDatabaseID,
|
setNotionDatabaseID,
|
||||||
setNotionApiKey
|
setNotionApiKey,
|
||||||
|
setNotionPageNameKey,
|
||||||
|
setThoughtAutoCollapse
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -10,6 +10,13 @@ export interface ShortcutsState {
|
|||||||
const initialState: ShortcutsState = {
|
const initialState: ShortcutsState = {
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
...ZOOM_SHORTCUTS,
|
...ZOOM_SHORTCUTS,
|
||||||
|
{
|
||||||
|
key: 'show_settings',
|
||||||
|
shortcut: [isMac ? 'Command' : 'Ctrl', ','],
|
||||||
|
editable: false,
|
||||||
|
enabled: true,
|
||||||
|
system: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'show_app',
|
key: 'show_app',
|
||||||
shortcut: [],
|
shortcut: [],
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export type Assistant = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
prompt: string
|
prompt: string
|
||||||
knowledge_base?: KnowledgeBase
|
knowledge_bases?: KnowledgeBase[]
|
||||||
topics: Topic[]
|
topics: Topic[]
|
||||||
type: string
|
type: string
|
||||||
emoji?: string
|
emoji?: string
|
||||||
@ -271,4 +271,13 @@ export type GenerateImageParams = {
|
|||||||
promptEnhancement?: boolean
|
promptEnhancement?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TranslateHistory {
|
||||||
|
id: string
|
||||||
|
sourceText: string
|
||||||
|
targetText: string
|
||||||
|
sourceLanguage: string
|
||||||
|
targetLanguage: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
||||||
|
|||||||
25
src/renderer/src/utils/abortController.ts
Normal file
25
src/renderer/src/utils/abortController.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export const abortMap = new Map<string, () => void>()
|
||||||
|
|
||||||
|
export const addAbortController = (id: string, abortFn: () => void) => {
|
||||||
|
let callback = abortFn
|
||||||
|
const existingCallback = abortMap.get(id)
|
||||||
|
if (existingCallback) {
|
||||||
|
callback = () => {
|
||||||
|
existingCallback?.()
|
||||||
|
abortFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
abortMap.set(id, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeAbortController = (id: string) => {
|
||||||
|
abortMap.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const abortCompletion = (id: string) => {
|
||||||
|
const abortFn = abortMap.get(id)
|
||||||
|
if (abortFn) {
|
||||||
|
abortFn()
|
||||||
|
removeAbortController(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,7 +67,7 @@ export const exportTopicToNotion = async (topic: Topic) => {
|
|||||||
const response = await notion.pages.create({
|
const response = await notion.pages.create({
|
||||||
parent: { database_id: notionDatabaseID },
|
parent: { database_id: notionDatabaseID },
|
||||||
properties: {
|
properties: {
|
||||||
Name: {
|
[store.getState().settings.notionPageNameKey || 'Name']: {
|
||||||
title: [{ text: { content: topic.name } }]
|
title: [{ text: { content: topic.name } }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const Inputbar: FC = () => {
|
|||||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase>()
|
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>()
|
||||||
|
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
@ -274,8 +274,8 @@ const Inputbar: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
const handleKnowledgeBaseSelect = (bases: KnowledgeBase[]) => {
|
||||||
setSelectedKnowledgeBase(base)
|
setSelectedKnowledgeBase(bases?.[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -317,7 +317,7 @@ const Inputbar: FC = () => {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<KnowledgeBaseButton
|
<KnowledgeBaseButton
|
||||||
selectedBase={selectedKnowledgeBase}
|
selectedBases={selectedKnowledgeBase ? [selectedKnowledgeBase] : []}
|
||||||
onSelect={handleKnowledgeBaseSelect}
|
onSelect={handleKnowledgeBaseSelect}
|
||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
disabled={files.length > 0}
|
disabled={files.length > 0}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user