mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-01 20:56:59 +08:00
Compare commits
223 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
624e4dbaaf | ||
|
9bbb4f396b | ||
|
b2c1644d69 | ||
|
5629f842da | ||
|
f900283b09 | ||
|
b667eff6bd | ||
|
54fdf40f5a | ||
|
690542145d | ||
|
94c4cf0624 | ||
|
3da717d9fc | ||
|
0902efc719 | ||
|
d7e2ee63d8 | ||
|
7deb36ee1f | ||
|
bfe4e88246 | ||
|
9ab45c3969 | ||
|
fec80c6c51 | ||
|
a6b7432358 | ||
|
3486954e07 | ||
|
150fc84b9b | ||
|
b023a00445 | ||
|
d0e296adf8 | ||
|
aa40015e9b | ||
|
141ce2c99a | ||
|
4a95dcb6e9 | ||
|
1610675c8f | ||
|
724c814bfe | ||
|
764c0cb865 | ||
|
8a4b8a84d6 | ||
|
8ec6acc55a | ||
|
b6a022b0ef | ||
|
716899c030 | ||
|
d9e407fd2b | ||
|
deb140de73 | ||
|
3c1e5e7978 | ||
|
4a8e85c28a | ||
|
8498cadae8 | ||
|
8c83fe23a1 | ||
|
a8c65e3d27 | ||
|
324d30bef9 | ||
|
46cb48023e | ||
|
1c24ca58c7 | ||
|
9193a9a0e0 | ||
|
957244ba2e | ||
|
ac599aa47c | ||
|
67a90ffb76 | ||
|
feaa6f9bf0 | ||
|
753bf3b924 | ||
|
b3219f57c8 | ||
|
a17df037af | ||
|
dfc36e5210 | ||
|
c359b92ddc | ||
|
e1d6131f13 | ||
|
6a0bda00f5 | ||
|
f85ec95877 | ||
|
a024980c03 | ||
|
fd9e94e078 | ||
|
f6a6c51d15 | ||
|
966db1e4be | ||
|
b8bbc37b8e | ||
|
40cbabc330 | ||
|
04a4e1b39a | ||
|
99f3160aa2 | ||
|
8cb72d8452 | ||
|
c9eb9f3eda | ||
|
64c3dcd732 | ||
|
d49ececcc5 | ||
|
90e1fadb1e | ||
|
071391ddff | ||
|
d70d46b4d5 | ||
|
3ef596b215 | ||
|
35c5518668 | ||
|
8b513537b7 | ||
|
b27f394995 | ||
|
3f9f556e1c | ||
|
1772d5c4b6 | ||
|
715d1dc02f | ||
|
6737f016f5 | ||
|
f2d2622172 | ||
|
72d6f97024 | ||
|
a0f0b4ff9e | ||
|
c27ef6ffbf | ||
|
f5499ff699 | ||
|
c4334d4e5f | ||
|
51e8f0440d | ||
|
5ec0311f84 | ||
|
556d563ba0 | ||
|
6a083b24c4 | ||
|
825929fdc8 | ||
|
941a03ed6c | ||
|
cf63619182 | ||
|
5c04d3c5ea | ||
|
46a47db2d8 | ||
|
21ef9a4567 | ||
|
6f0846b2af | ||
|
ecd78b3bdd | ||
|
d8afd1af88 | ||
|
7c1bc1f1a1 | ||
|
763fc89b29 | ||
|
47b33f2b17 | ||
|
9f0e16b045 | ||
|
2efedb1736 | ||
|
044116c14c | ||
|
b4bf11d648 | ||
|
6cc0a5a1a4 | ||
|
8f14de5108 | ||
|
8f6e5d73a2 | ||
|
ab9f5382b2 | ||
|
fd441d9303 | ||
|
e31bec3aff | ||
|
2a1c05a028 | ||
|
421bf33c0e | ||
|
3935c725c9 | ||
|
908ee0060f | ||
|
82e6fd7bb5 | ||
|
6b98b14179 | ||
|
1ecefd88f7 | ||
|
2e9e20ce7c | ||
|
fb60fbb217 | ||
|
4199e17da0 | ||
|
dfd089132d | ||
|
3a10f58b28 | ||
|
9d55adbaf2 | ||
|
00be2be24f | ||
|
5b126c7e52 | ||
|
1943f3b53f | ||
|
4a0bef9afb | ||
|
dfd2a53129 | ||
|
aa4e855012 | ||
|
d6089e6309 | ||
|
038e6df8f0 | ||
|
2fd68bcac3 | ||
|
e468fecf12 | ||
|
fc31d8e5d1 | ||
|
115f357a07 | ||
|
ac04a1cac8 | ||
|
87a286ef07 | ||
|
622d8a4edb | ||
|
b44086f0dc | ||
|
0236e13187 | ||
|
a3d4a7253f | ||
|
e079f1b31a | ||
|
9a78a72eb3 | ||
|
862c2e8810 | ||
|
12cad4c418 | ||
|
89b9d3a7f7 | ||
|
57831d4880 | ||
|
052004d70e | ||
|
a765237441 | ||
|
ac470a6d07 | ||
|
7237d33be3 | ||
|
1610b480af | ||
|
287fa0a39c | ||
|
afa1a4303b | ||
|
28cedb1493 | ||
|
a280e25ee7 | ||
|
8464ca8931 | ||
|
44340f277d | ||
|
74bf99f9ea | ||
|
9caf820758 | ||
|
26c2598f56 | ||
|
6d9abf261c | ||
|
b16d0185dd | ||
|
ca51c2e93d | ||
|
da254975cd | ||
|
86bae6be3a | ||
|
fc8c7ef18d | ||
|
f654629c6a | ||
|
d4a87c561a | ||
|
d8872d48b3 | ||
|
ed16c2c18d | ||
|
0478a6ce3b | ||
|
68b60e82ba | ||
|
8edc0989e2 | ||
|
fbf3551bbe | ||
|
238d3122c4 | ||
|
ee22fba448 | ||
|
f8a2a28bff | ||
|
b3cfaf1420 | ||
|
2e9f701bb7 | ||
|
9aabc4ad6a | ||
|
5dc731bc77 | ||
|
32d05c9855 | ||
|
5a0d0c0b75 | ||
|
17d4a8fb26 | ||
|
348c1a7d5f | ||
|
49151dabf5 | ||
|
eb7c7cdcb6 | ||
|
ec95292209 | ||
|
4b84fb328c | ||
|
47d27c1f41 | ||
|
5267ad46da | ||
|
94bc880b7f | ||
|
bab3e0bc9b | ||
|
b3a324b6f5 | ||
|
a1117cd4ee | ||
|
6ece818d69 | ||
|
5df09d5e2a | ||
|
33450ce429 | ||
|
e2f0206d88 | ||
|
3767b2c7f9 | ||
|
1779f1f3da | ||
|
a0290b0c1b | ||
|
dd1030139b | ||
|
30ca2117bb | ||
|
89024a8dc8 | ||
|
d61cb98ac7 | ||
|
a7ceb61e27 | ||
|
74b915a790 | ||
|
01ea690421 | ||
|
17cc9284a0 | ||
|
498d0f0b8b | ||
|
2b0153807c | ||
|
d726c71141 | ||
|
a16725ac17 | ||
|
54401162bd | ||
|
7fde9327a2 | ||
|
bbbf59c74a | ||
|
34034be0e3 | ||
|
d21481173e | ||
|
fa6ebadc7b | ||
|
a51fb24f36 | ||
|
74986803db | ||
|
24bf7950d8 |
@@ -1,21 +1,20 @@
|
|||||||
|
|
||||||
# Your openai api key. (required)
|
# Your openai api key. (required)
|
||||||
OPENAI_API_KEY=sk-xxxx
|
OPENAI_API_KEY=sk-xxxx
|
||||||
|
|
||||||
# Access password, separated by comma. (optional)
|
# Access password, separated by comma. (optional)
|
||||||
CODE=your-password
|
CODE=your-password
|
||||||
|
|
||||||
# You can start service behind a proxy
|
# You can start service behind a proxy. (optional)
|
||||||
PROXY_URL=http://localhost:7890
|
PROXY_URL=http://localhost:7890
|
||||||
|
|
||||||
# (optional)
|
# (optional)
|
||||||
# Default: Empty
|
# Default: Empty
|
||||||
# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
||||||
GOOGLE_API_KEY=
|
GOOGLE_API_KEY=
|
||||||
|
|
||||||
# (optional)
|
# (optional)
|
||||||
# Default: https://generativelanguage.googleapis.com/
|
# Default: https://generativelanguage.googleapis.com/
|
||||||
# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
|
# Google Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
|
||||||
GOOGLE_URL=
|
GOOGLE_URL=
|
||||||
|
|
||||||
# Override openai api request base url. (optional)
|
# Override openai api request base url. (optional)
|
||||||
@@ -47,6 +46,15 @@ ENABLE_BALANCE_QUERY=
|
|||||||
# If you want to disable parse settings from url, set this value to 1.
|
# If you want to disable parse settings from url, set this value to 1.
|
||||||
DISABLE_FAST_LINK=
|
DISABLE_FAST_LINK=
|
||||||
|
|
||||||
|
# (optional)
|
||||||
|
# Default: Empty
|
||||||
|
# To control custom models, use + to add a custom model, use - to hide a model, use name=displayName to customize model name, separated by comma.
|
||||||
|
CUSTOM_MODELS=
|
||||||
|
|
||||||
|
# (optional)
|
||||||
|
# Default: Empty
|
||||||
|
# Change default model
|
||||||
|
DEFAULT_MODEL=
|
||||||
|
|
||||||
# anthropic claude Api Key.(optional)
|
# anthropic claude Api Key.(optional)
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
@@ -54,8 +62,6 @@ ANTHROPIC_API_KEY=
|
|||||||
### anthropic claude Api version. (optional)
|
### anthropic claude Api version. (optional)
|
||||||
ANTHROPIC_API_VERSION=
|
ANTHROPIC_API_VERSION=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### anthropic claude Api url (optional)
|
### anthropic claude Api url (optional)
|
||||||
ANTHROPIC_URL=
|
ANTHROPIC_URL=
|
||||||
|
|
||||||
|
80
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
80
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: '🐛 Bug Report'
|
||||||
|
description: 'Report an bug'
|
||||||
|
title: '[Bug] '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: '📦 Deployment Method'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 'Official installation package'
|
||||||
|
- 'Vercel'
|
||||||
|
- 'Zeabur'
|
||||||
|
- 'Sealos'
|
||||||
|
- 'Netlify'
|
||||||
|
- 'Docker'
|
||||||
|
- 'Other'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: '📌 Version'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: '💻 Operating System'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 'Windows'
|
||||||
|
- 'macOS'
|
||||||
|
- 'Ubuntu'
|
||||||
|
- 'Other Linux'
|
||||||
|
- 'iOS'
|
||||||
|
- 'iPad OS'
|
||||||
|
- 'Android'
|
||||||
|
- 'Other'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: '📌 System Version'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: '🌐 Browser'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 'Chrome'
|
||||||
|
- 'Edge'
|
||||||
|
- 'Safari'
|
||||||
|
- 'Firefox'
|
||||||
|
- 'Other'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: '📌 Browser Version'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🐛 Bug Description'
|
||||||
|
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '📷 Recurrence Steps'
|
||||||
|
description: A clear and concise description of how to recurrence.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🚦 Expected Behavior'
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '📝 Additional Information'
|
||||||
|
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.
|
80
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal file
80
.github/ISSUE_TEMPLATE/1_bug_report_cn.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: '🐛 反馈缺陷'
|
||||||
|
description: '反馈一个问题/缺陷'
|
||||||
|
title: '[Bug] '
|
||||||
|
labels: ['bug']
|
||||||
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: '📦 部署方式'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- '官方安装包'
|
||||||
|
- 'Vercel'
|
||||||
|
- 'Zeabur'
|
||||||
|
- 'Sealos'
|
||||||
|
- 'Netlify'
|
||||||
|
- 'Docker'
|
||||||
|
- 'Other'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: '📌 软件版本'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: '💻 系统环境'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 'Windows'
|
||||||
|
- 'macOS'
|
||||||
|
- 'Ubuntu'
|
||||||
|
- 'Other Linux'
|
||||||
|
- 'iOS'
|
||||||
|
- 'iPad OS'
|
||||||
|
- 'Android'
|
||||||
|
- 'Other'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: '📌 系统版本'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: '🌐 浏览器'
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- 'Chrome'
|
||||||
|
- 'Edge'
|
||||||
|
- 'Safari'
|
||||||
|
- 'Firefox'
|
||||||
|
- 'Other'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: '📌 浏览器版本'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🐛 问题描述'
|
||||||
|
description: 请提供一个清晰且简洁的问题描述,若上述选项为`Other`,也请详细说明。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '📷 复现步骤'
|
||||||
|
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🚦 期望结果'
|
||||||
|
description: 请提供一个清晰且简洁的描述,说明您期望发生什么。
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '📝 补充信息'
|
||||||
|
description: 如果您的问题需要进一步说明,或者您遇到的问题无法在一个简单的示例中复现,请在这里添加更多信息。
|
21
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/2_feature_request.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: '🌠 Feature Request'
|
||||||
|
description: 'Suggest an idea'
|
||||||
|
title: '[Feature Request] '
|
||||||
|
labels: ['enhancement']
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🥰 Feature Description'
|
||||||
|
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🧐 Proposed Solution'
|
||||||
|
description: Describe the solution you'd like in a clear and concise manner.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '📝 Additional Information'
|
||||||
|
description: Add any other context about the problem here.
|
21
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/2_feature_request_cn.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: '🌠 功能需求'
|
||||||
|
description: '提出需求或建议'
|
||||||
|
title: '[Feature Request] '
|
||||||
|
labels: ['enhancement']
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🥰 需求描述'
|
||||||
|
description: 请添加一个清晰且简洁的问题描述,阐述您希望通过这个功能需求解决的问题。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '🧐 解决方案'
|
||||||
|
description: 请清晰且简洁地描述您想要的解决方案。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: '📝 补充信息'
|
||||||
|
description: 在这里添加关于问题的任何其他背景信息。
|
146
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
146
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,146 +0,0 @@
|
|||||||
name: Bug report
|
|
||||||
description: Create a report to help us improve
|
|
||||||
title: "[Bug] "
|
|
||||||
labels: ["bug"]
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Describe the bug"
|
|
||||||
- type: textarea
|
|
||||||
id: bug-description
|
|
||||||
attributes:
|
|
||||||
label: "Bug Description"
|
|
||||||
description: "A clear and concise description of what the bug is."
|
|
||||||
placeholder: "Explain the bug..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## To Reproduce"
|
|
||||||
- type: textarea
|
|
||||||
id: steps-to-reproduce
|
|
||||||
attributes:
|
|
||||||
label: "Steps to Reproduce"
|
|
||||||
description: "Steps to reproduce the behavior:"
|
|
||||||
placeholder: |
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Expected behavior"
|
|
||||||
- type: textarea
|
|
||||||
id: expected-behavior
|
|
||||||
attributes:
|
|
||||||
label: "Expected Behavior"
|
|
||||||
description: "A clear and concise description of what you expected to happen."
|
|
||||||
placeholder: "Describe what you expected to happen..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Screenshots"
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: "Screenshots"
|
|
||||||
description: "If applicable, add screenshots to help explain your problem."
|
|
||||||
placeholder: "Paste your screenshots here or write 'N/A' if not applicable..."
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Deployment"
|
|
||||||
- type: checkboxes
|
|
||||||
id: deployment
|
|
||||||
attributes:
|
|
||||||
label: "Deployment Method"
|
|
||||||
description: "Please select the deployment method you are using."
|
|
||||||
options:
|
|
||||||
- label: "Docker"
|
|
||||||
- label: "Vercel"
|
|
||||||
- label: "Server"
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Desktop (please complete the following information):"
|
|
||||||
- type: input
|
|
||||||
id: desktop-os
|
|
||||||
attributes:
|
|
||||||
label: "Desktop OS"
|
|
||||||
description: "Your desktop operating system."
|
|
||||||
placeholder: "e.g., Windows 10"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: desktop-browser
|
|
||||||
attributes:
|
|
||||||
label: "Desktop Browser"
|
|
||||||
description: "Your desktop browser."
|
|
||||||
placeholder: "e.g., Chrome, Safari"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: desktop-version
|
|
||||||
attributes:
|
|
||||||
label: "Desktop Browser Version"
|
|
||||||
description: "Version of your desktop browser."
|
|
||||||
placeholder: "e.g., 89.0"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Smartphone (please complete the following information):"
|
|
||||||
- type: input
|
|
||||||
id: smartphone-device
|
|
||||||
attributes:
|
|
||||||
label: "Smartphone Device"
|
|
||||||
description: "Your smartphone device."
|
|
||||||
placeholder: "e.g., iPhone X"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: smartphone-os
|
|
||||||
attributes:
|
|
||||||
label: "Smartphone OS"
|
|
||||||
description: "Your smartphone operating system."
|
|
||||||
placeholder: "e.g., iOS 14.4"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: smartphone-browser
|
|
||||||
attributes:
|
|
||||||
label: "Smartphone Browser"
|
|
||||||
description: "Your smartphone browser."
|
|
||||||
placeholder: "e.g., Safari"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: input
|
|
||||||
id: smartphone-version
|
|
||||||
attributes:
|
|
||||||
label: "Smartphone Browser Version"
|
|
||||||
description: "Version of your smartphone browser."
|
|
||||||
placeholder: "e.g., 14"
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Additional Logs"
|
|
||||||
- type: textarea
|
|
||||||
id: additional-logs
|
|
||||||
attributes:
|
|
||||||
label: "Additional Logs"
|
|
||||||
description: "Add any logs about the problem here."
|
|
||||||
placeholder: "Paste any relevant logs here..."
|
|
||||||
validations:
|
|
||||||
required: false
|
|
53
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
53
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Suggest an idea for this project
|
|
||||||
title: "[Feature Request]: "
|
|
||||||
labels: ["enhancement"]
|
|
||||||
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Is your feature request related to a problem? Please describe."
|
|
||||||
- type: textarea
|
|
||||||
id: problem-description
|
|
||||||
attributes:
|
|
||||||
label: Problem Description
|
|
||||||
description: "A clear and concise description of what the problem is. Example: I'm always frustrated when [...]"
|
|
||||||
placeholder: "Explain the problem you are facing..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Describe the solution you'd like"
|
|
||||||
- type: textarea
|
|
||||||
id: desired-solution
|
|
||||||
attributes:
|
|
||||||
label: Solution Description
|
|
||||||
description: A clear and concise description of what you want to happen.
|
|
||||||
placeholder: "Describe the solution you'd like..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Describe alternatives you've considered"
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives-considered
|
|
||||||
attributes:
|
|
||||||
label: Alternatives Considered
|
|
||||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
placeholder: "Describe any alternative solutions or features you've considered..."
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: "## Additional context"
|
|
||||||
- type: textarea
|
|
||||||
id: additional-context
|
|
||||||
attributes:
|
|
||||||
label: Additional Context
|
|
||||||
description: Add any other context or screenshots about the feature request here.
|
|
||||||
placeholder: "Add any other context or screenshots about the feature request here..."
|
|
||||||
validations:
|
|
||||||
required: false
|
|
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
28
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#### 💻 变更类型 | Change Type
|
||||||
|
|
||||||
|
<!-- For change type, change [ ] to [x]. -->
|
||||||
|
|
||||||
|
- [ ] feat <!-- 引入新功能 | Introduce new features -->
|
||||||
|
- [ ] fix <!-- 修复 Bug | Fix a bug -->
|
||||||
|
- [ ] refactor <!-- 重构代码(既不修复 Bug 也不添加新功能) | Refactor code that neither fixes a bug nor adds a feature -->
|
||||||
|
- [ ] perf <!-- 提升性能的代码变更 | A code change that improves performance -->
|
||||||
|
- [ ] style <!-- 添加或更新不影响代码含义的样式文件 | Add or update style files that do not affect the meaning of the code -->
|
||||||
|
- [ ] test <!-- 添加缺失的测试或纠正现有的测试 | Adding missing tests or correcting existing tests -->
|
||||||
|
- [ ] docs <!-- 仅文档更新 | Documentation only changes -->
|
||||||
|
- [ ] ci <!-- 修改持续集成配置文件和脚本 | Changes to our CI configuration files and scripts -->
|
||||||
|
- [ ] chore <!-- 其他不修改 src 或 test 文件的变更 | Other changes that don’t modify src or test files -->
|
||||||
|
- [ ] build <!-- 进行架构变更 | Make architectural changes -->
|
||||||
|
|
||||||
|
#### 🔀 变更说明 | Description of Change
|
||||||
|
|
||||||
|
<!--
|
||||||
|
感谢您的 Pull Request ,请提供此 Pull Request 的变更说明
|
||||||
|
Thank you for your Pull Request. Please provide a description above.
|
||||||
|
-->
|
||||||
|
|
||||||
|
#### 📝 补充信息 | Additional Information
|
||||||
|
|
||||||
|
<!--
|
||||||
|
请添加与此 Pull Request 相关的补充信息
|
||||||
|
Add any other context about the Pull Request here.
|
||||||
|
-->
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -44,3 +44,5 @@ dev
|
|||||||
|
|
||||||
*.key
|
*.key
|
||||||
*.key.pub
|
*.key.pub
|
||||||
|
|
||||||
|
masks.json
|
||||||
|
94
README.md
94
README.md
@@ -1,5 +1,8 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/images/head-cover.png" alt="icon"/>
|
|
||||||
|
<a href='#企业版'>
|
||||||
|
<img src="./docs/images/ent.svg" alt="icon"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
||||||
|
|
||||||
@@ -14,9 +17,9 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
[![MacOS][MacOS-image]][download-url]
|
[![MacOS][MacOS-image]][download-url]
|
||||||
[![Linux][Linux-image]][download-url]
|
[![Linux][Linux-image]][download-url]
|
||||||
|
|
||||||
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/NextChatDev)
|
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
|
||||||
|
|
||||||
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
||||||
|
|
||||||
[web-url]: https://app.nextchat.dev/
|
[web-url]: https://app.nextchat.dev/
|
||||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
||||||
@@ -25,16 +28,38 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat)
|
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
[](https://zeabur.com/templates/ZBUEFA)
|
|
||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Enterprise Edition
|
||||||
|
|
||||||
|
Meeting Your Company's Privatization and Customization Deployment Requirements:
|
||||||
|
- **Brand Customization**: Tailored VI/UI to seamlessly align with your corporate brand image.
|
||||||
|
- **Resource Integration**: Unified configuration and management of dozens of AI resources by company administrators, ready for use by team members.
|
||||||
|
- **Permission Control**: Clearly defined member permissions, resource permissions, and knowledge base permissions, all controlled via a corporate-grade Admin Panel.
|
||||||
|
- **Knowledge Integration**: Combining your internal knowledge base with AI capabilities, making it more relevant to your company's specific business needs compared to general AI.
|
||||||
|
- **Security Auditing**: Automatically intercept sensitive inquiries and trace all historical conversation records, ensuring AI adherence to corporate information security standards.
|
||||||
|
- **Private Deployment**: Enterprise-level private deployment supporting various mainstream private cloud solutions, ensuring data security and privacy protection.
|
||||||
|
- **Continuous Updates**: Ongoing updates and upgrades in cutting-edge capabilities like multimodal AI, ensuring consistent innovation and advancement.
|
||||||
|
|
||||||
|
For enterprise inquiries, please contact: **business@nextchat.dev**
|
||||||
|
|
||||||
|
## 企业版
|
||||||
|
|
||||||
|
满足企业用户私有化部署和个性化定制需求:
|
||||||
|
- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
|
||||||
|
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
|
||||||
|
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
|
||||||
|
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
|
||||||
|
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
|
||||||
|
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
|
||||||
|
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
|
||||||
|
|
||||||
|
企业版咨询: **business@nextchat.dev**
|
||||||
|
|
||||||
|
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||||
@@ -49,6 +74,12 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
- Automatically compresses chat history to support long conversations while also saving your tokens
|
- Automatically compresses chat history to support long conversations while also saving your tokens
|
||||||
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||||
@@ -57,10 +88,14 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
|
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
|
||||||
- [x] Desktop App with tauri
|
- [x] Desktop App with tauri
|
||||||
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
|
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
|
||||||
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
|
||||||
|
- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
- [x] artifacts
|
||||||
|
- [ ] network search, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
|
- 🚀 v2.14.0 Now supports Artifacts & SD
|
||||||
- 🚀 v2.10.1 support Google Gemini Pro model.
|
- 🚀 v2.10.1 support Google Gemini Pro model.
|
||||||
- 🚀 v2.9.11 you can use azure endpoint now.
|
- 🚀 v2.9.11 you can use azure endpoint now.
|
||||||
- 🚀 v2.8 now we have a client that runs across all platforms!
|
- 🚀 v2.8 now we have a client that runs across all platforms!
|
||||||
@@ -89,15 +124,20 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
|||||||
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
|
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
|
||||||
- [x] 使用 tauri 打包桌面应用
|
- [x] 使用 tauri 打包桌面应用
|
||||||
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
|
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
|
||||||
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
|
||||||
|
- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
- [x] artifacts
|
||||||
|
- [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
|
||||||
## 最新动态
|
## 最新动态
|
||||||
|
|
||||||
|
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
|
||||||
|
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
|
||||||
|
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
|
||||||
|
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
||||||
|
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
||||||
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
|
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
|
||||||
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
|
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
|
||||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
|
||||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
|
||||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
|
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
@@ -180,8 +220,7 @@ Specify OpenAI organization ID.
|
|||||||
|
|
||||||
### `AZURE_URL` (optional)
|
### `AZURE_URL` (optional)
|
||||||
|
|
||||||
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
|
> Example: https://{azure-resource-url}/openai
|
||||||
> if you config deployment name in `CUSTOM_MODELS`, you can remove `{deploy-name}` in `AZURE_URL`
|
|
||||||
|
|
||||||
Azure deploy url.
|
Azure deploy url.
|
||||||
|
|
||||||
@@ -241,6 +280,18 @@ Alibaba Cloud Api Key.
|
|||||||
|
|
||||||
Alibaba Cloud Api Url.
|
Alibaba Cloud Api Url.
|
||||||
|
|
||||||
|
### `IFLYTEK_URL` (Optional)
|
||||||
|
|
||||||
|
iflytek Api Url.
|
||||||
|
|
||||||
|
### `IFLYTEK_API_KEY` (Optional)
|
||||||
|
|
||||||
|
iflytek Api Key.
|
||||||
|
|
||||||
|
### `IFLYTEK_API_SECRET` (Optional)
|
||||||
|
|
||||||
|
iflytek Api Secret.
|
||||||
|
|
||||||
### `HIDE_USER_API_KEY` (optional)
|
### `HIDE_USER_API_KEY` (optional)
|
||||||
|
|
||||||
> Default: Empty
|
> Default: Empty
|
||||||
@@ -276,6 +327,7 @@ User `-all` to disable all default models, `+all` to enable all default models.
|
|||||||
|
|
||||||
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
|
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
|
||||||
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
|
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
|
||||||
|
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
|
||||||
|
|
||||||
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
|
||||||
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
|
||||||
@@ -295,6 +347,14 @@ You can use this option if you want to increase the number of webdav service add
|
|||||||
|
|
||||||
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
|
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
|
||||||
|
|
||||||
|
### `STABILITY_API_KEY` (optional)
|
||||||
|
|
||||||
|
Stability API key.
|
||||||
|
|
||||||
|
### `STABILITY_URL` (optional)
|
||||||
|
|
||||||
|
Customize Stability API url.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
NodeJS >= 18, Docker >= 20
|
NodeJS >= 18, Docker >= 20
|
||||||
|
65
README_CN.md
65
README_CN.md
@@ -1,22 +1,34 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./docs/images/icon.svg" alt="预览"/>
|
|
||||||
|
<a href='#企业版'>
|
||||||
|
<img src="./docs/images/ent.svg" alt="icon"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
<h1 align="center">NextChat</h1>
|
<h1 align="center">NextChat</h1>
|
||||||
|
|
||||||
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||||
|
|
||||||
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
[企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) /[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
[](https://zeabur.com/templates/ZBUEFA)
|
|
||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## 企业版
|
||||||
|
|
||||||
|
满足您公司私有化部署和定制需求
|
||||||
|
- **品牌定制**:企业量身定制 VI/UI,与企业品牌形象无缝契合
|
||||||
|
- **资源集成**:由企业管理人员统一配置和管理数十种 AI 资源,团队成员开箱即用
|
||||||
|
- **权限管理**:成员权限、资源权限、知识库权限层级分明,企业级 Admin Panel 统一控制
|
||||||
|
- **知识接入**:企业内部知识库与 AI 能力相结合,比通用 AI 更贴近企业自身业务需求
|
||||||
|
- **安全审计**:自动拦截敏感提问,支持追溯全部历史对话记录,让 AI 也能遵循企业信息安全规范
|
||||||
|
- **私有部署**:企业级私有部署,支持各类主流私有云部署,确保数据安全和隐私保护
|
||||||
|
- **持续更新**:提供多模态、智能体等前沿能力持续更新升级服务,常用常新、持续先进
|
||||||
|
|
||||||
|
企业版咨询: **business@nextchat.dev**
|
||||||
|
|
||||||
|
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
||||||
|
|
||||||
## 开始使用
|
## 开始使用
|
||||||
|
|
||||||
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
|
||||||
@@ -25,6 +37,12 @@
|
|||||||
3. 部署完毕后,即可开始使用;
|
3. 部署完毕后,即可开始使用;
|
||||||
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
## 保持更新
|
## 保持更新
|
||||||
|
|
||||||
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
|
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
|
||||||
@@ -94,8 +112,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
|||||||
|
|
||||||
### `AZURE_URL` (可选)
|
### `AZURE_URL` (可选)
|
||||||
|
|
||||||
> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
|
> 形如:https://{azure-resource-url}/openai
|
||||||
> 如果你已经在`CUSTOM_MODELS`中参考`displayName`的方式配置了{deploy-name},那么可以从`AZURE_URL`中移除`{deploy-name}`
|
|
||||||
|
|
||||||
Azure 部署地址。
|
Azure 部署地址。
|
||||||
|
|
||||||
@@ -155,6 +172,20 @@ ByteDance Api Url.
|
|||||||
|
|
||||||
阿里云(千问)Api Url.
|
阿里云(千问)Api Url.
|
||||||
|
|
||||||
|
### `IFLYTEK_URL` (可选)
|
||||||
|
|
||||||
|
讯飞星火Api Url.
|
||||||
|
|
||||||
|
### `IFLYTEK_API_KEY` (可选)
|
||||||
|
|
||||||
|
讯飞星火Api Key.
|
||||||
|
|
||||||
|
### `IFLYTEK_API_SECRET` (可选)
|
||||||
|
|
||||||
|
讯飞星火Api Secret.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### `HIDE_USER_API_KEY` (可选)
|
### `HIDE_USER_API_KEY` (可选)
|
||||||
|
|
||||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||||
@@ -186,7 +217,8 @@ ByteDance Api Url.
|
|||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||||
|
|
||||||
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
在Azure的模式下,支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||||
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项
|
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
|
||||||
|
> 如果你只能使用Azure模式,那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
|
||||||
|
|
||||||
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
在ByteDance的模式下,支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
|
||||||
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
|
||||||
@@ -200,6 +232,15 @@ ByteDance Api Url.
|
|||||||
|
|
||||||
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
|
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
|
||||||
|
|
||||||
|
### `STABILITY_API_KEY` (optional)
|
||||||
|
|
||||||
|
Stability API密钥
|
||||||
|
|
||||||
|
### `STABILITY_URL` (optional)
|
||||||
|
|
||||||
|
自定义的Stability API请求地址
|
||||||
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
点击下方按钮,开始二次开发:
|
点击下方按钮,开始二次开发:
|
||||||
|
310
README_JA.md
Normal file
310
README_JA.md
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<div align="center">
|
||||||
|
<img src="./docs/images/ent.svg" alt="プレビュー"/>
|
||||||
|
|
||||||
|
<h1 align="center">NextChat</h1>
|
||||||
|
|
||||||
|
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
|
||||||
|
|
||||||
|
[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
|
||||||
|
|
||||||
|
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 企業版
|
||||||
|
|
||||||
|
あなたの会社のプライベートデプロイとカスタマイズのニーズに応える
|
||||||
|
- **ブランドカスタマイズ**:企業向けに特別に設計された VI/UI、企業ブランドイメージとシームレスにマッチ
|
||||||
|
- **リソース統合**:企業管理者が数十種類のAIリソースを統一管理、チームメンバーはすぐに使用可能
|
||||||
|
- **権限管理**:メンバーの権限、リソースの権限、ナレッジベースの権限を明確にし、企業レベルのAdmin Panelで統一管理
|
||||||
|
- **知識の統合**:企業内部のナレッジベースとAI機能を結びつけ、汎用AIよりも企業自身の業務ニーズに近づける
|
||||||
|
- **セキュリティ監査**:機密質問を自動的にブロックし、すべての履歴対話を追跡可能にし、AIも企業の情報セキュリティ基準に従わせる
|
||||||
|
- **プライベートデプロイ**:企業レベルのプライベートデプロイ、主要なプライベートクラウドデプロイをサポートし、データのセキュリティとプライバシーを保護
|
||||||
|
- **継続的な更新**:マルチモーダル、エージェントなどの最先端機能を継続的に更新し、常に最新であり続ける
|
||||||
|
|
||||||
|
企業版のお問い合わせ: **business@nextchat.dev**
|
||||||
|
|
||||||
|
|
||||||
|
## 始めに
|
||||||
|
|
||||||
|
1. [OpenAI API Key](https://platform.openai.com/account/api-keys)を準備する;
|
||||||
|
2. 右側のボタンをクリックしてデプロイを開始:
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) 、GitHubアカウントで直接ログインし、環境変数ページにAPI Keyと[ページアクセスパスワード](#設定ページアクセスパスワード) CODEを入力してください;
|
||||||
|
3. デプロイが完了したら、すぐに使用を開始できます;
|
||||||
|
4. (オプション)[カスタムドメインをバインド](https://vercel.com/docs/concepts/projects/domains/add-a-domain):Vercelが割り当てたドメインDNSは一部の地域で汚染されているため、カスタムドメインをバインドすると直接接続できます。
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
## 更新を維持する
|
||||||
|
|
||||||
|
もし上記の手順に従ってワンクリックでプロジェクトをデプロイした場合、「更新があります」というメッセージが常に表示されることがあります。これは、Vercel がデフォルトで新しいプロジェクトを作成するためで、本プロジェクトを fork していないことが原因です。そのため、正しく更新を検出できません。
|
||||||
|
|
||||||
|
以下の手順に従って再デプロイすることをお勧めします:
|
||||||
|
|
||||||
|
- 元のリポジトリを削除する
|
||||||
|
- ページ右上の fork ボタンを使って、本プロジェクトを fork する
|
||||||
|
- Vercel で再度選択してデプロイする、[詳細な手順はこちらを参照してください](./docs/vercel-ja.md)。
|
||||||
|
|
||||||
|
|
||||||
|
### 自動更新を開く
|
||||||
|
|
||||||
|
> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
|
||||||
|
|
||||||
|
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### 手動でコードを更新する
|
||||||
|
|
||||||
|
手動で即座に更新したい場合は、[GitHub のドキュメント](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)を参照して、fork したプロジェクトを上流のコードと同期する方法を確認してください。
|
||||||
|
|
||||||
|
このプロジェクトをスターまたはウォッチしたり、作者をフォローすることで、新機能の更新通知をすぐに受け取ることができます。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## ページアクセスパスワードを設定する
|
||||||
|
|
||||||
|
> パスワードを設定すると、ユーザーは設定ページでアクセスコードを手動で入力しない限り、通常のチャットができず、未承認の状態であることを示すメッセージが表示されます。
|
||||||
|
|
||||||
|
> **警告**:パスワードの桁数は十分に長く設定してください。7桁以上が望ましいです。さもないと、[ブルートフォース攻撃を受ける可能性があります](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
|
||||||
|
|
||||||
|
このプロジェクトは限られた権限管理機能を提供しています。Vercel プロジェクトのコントロールパネルで、環境変数ページに `CODE` という名前の環境変数を追加し、値をカンマで区切ったカスタムパスワードに設定してください:
|
||||||
|
|
||||||
|
```
|
||||||
|
code1,code2,code3
|
||||||
|
```
|
||||||
|
|
||||||
|
この環境変数を追加または変更した後、**プロジェクトを再デプロイ**して変更を有効にしてください。
|
||||||
|
|
||||||
|
|
||||||
|
## 環境変数
|
||||||
|
|
||||||
|
> 本プロジェクトのほとんどの設定は環境変数で行います。チュートリアル:[Vercel の環境変数を変更する方法](./docs/vercel-ja.md)。
|
||||||
|
|
||||||
|
### `OPENAI_API_KEY` (必須)
|
||||||
|
|
||||||
|
OpenAI の API キー。OpenAI アカウントページで申請したキーをカンマで区切って複数設定できます。これにより、ランダムにキーが選択されます。
|
||||||
|
|
||||||
|
### `CODE` (オプション)
|
||||||
|
|
||||||
|
アクセスパスワード。カンマで区切って複数設定可能。
|
||||||
|
|
||||||
|
**警告**:この項目を設定しないと、誰でもデプロイしたウェブサイトを利用でき、トークンが急速に消耗する可能性があるため、設定をお勧めします。
|
||||||
|
|
||||||
|
### `BASE_URL` (オプション)
|
||||||
|
|
||||||
|
> デフォルト: `https://api.openai.com`
|
||||||
|
|
||||||
|
> 例: `http://your-openai-proxy.com`
|
||||||
|
|
||||||
|
OpenAI API のプロキシ URL。手動で OpenAI API のプロキシを設定している場合はこのオプションを設定してください。
|
||||||
|
|
||||||
|
> SSL 証明書の問題がある場合は、`BASE_URL` のプロトコルを http に設定してください。
|
||||||
|
|
||||||
|
### `OPENAI_ORG_ID` (オプション)
|
||||||
|
|
||||||
|
OpenAI の組織 ID を指定します。
|
||||||
|
|
||||||
|
### `AZURE_URL` (オプション)
|
||||||
|
|
||||||
|
> 形式: https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||||
|
> `CUSTOM_MODELS` で `displayName` 形式で {deploy-name} を設定した場合、`AZURE_URL` から {deploy-name} を省略できます。
|
||||||
|
|
||||||
|
Azure のデプロイ URL。
|
||||||
|
|
||||||
|
### `AZURE_API_KEY` (オプション)
|
||||||
|
|
||||||
|
Azure の API キー。
|
||||||
|
|
||||||
|
### `AZURE_API_VERSION` (オプション)
|
||||||
|
|
||||||
|
Azure API バージョン。[Azure ドキュメント](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)で確認できます。
|
||||||
|
|
||||||
|
### `GOOGLE_API_KEY` (オプション)
|
||||||
|
|
||||||
|
Google Gemini Pro API キー。
|
||||||
|
|
||||||
|
### `GOOGLE_URL` (オプション)
|
||||||
|
|
||||||
|
Google Gemini Pro API の URL。
|
||||||
|
|
||||||
|
### `ANTHROPIC_API_KEY` (オプション)
|
||||||
|
|
||||||
|
Anthropic Claude API キー。
|
||||||
|
|
||||||
|
### `ANTHROPIC_API_VERSION` (オプション)
|
||||||
|
|
||||||
|
Anthropic Claude API バージョン。
|
||||||
|
|
||||||
|
### `ANTHROPIC_URL` (オプション)
|
||||||
|
|
||||||
|
Anthropic Claude API の URL。
|
||||||
|
|
||||||
|
### `BAIDU_API_KEY` (オプション)
|
||||||
|
|
||||||
|
Baidu API キー。
|
||||||
|
|
||||||
|
### `BAIDU_SECRET_KEY` (オプション)
|
||||||
|
|
||||||
|
Baidu シークレットキー。
|
||||||
|
|
||||||
|
### `BAIDU_URL` (オプション)
|
||||||
|
|
||||||
|
Baidu API の URL。
|
||||||
|
|
||||||
|
### `BYTEDANCE_API_KEY` (オプション)
|
||||||
|
|
||||||
|
ByteDance API キー。
|
||||||
|
|
||||||
|
### `BYTEDANCE_URL` (オプション)
|
||||||
|
|
||||||
|
ByteDance API の URL。
|
||||||
|
|
||||||
|
### `ALIBABA_API_KEY` (オプション)
|
||||||
|
|
||||||
|
アリババ(千问)API キー。
|
||||||
|
|
||||||
|
### `ALIBABA_URL` (オプション)
|
||||||
|
|
||||||
|
アリババ(千问)API の URL。
|
||||||
|
|
||||||
|
### `HIDE_USER_API_KEY` (オプション)
|
||||||
|
|
||||||
|
ユーザーが API キーを入力できないようにしたい場合は、この環境変数を 1 に設定します。
|
||||||
|
|
||||||
|
### `DISABLE_GPT4` (オプション)
|
||||||
|
|
||||||
|
ユーザーが GPT-4 を使用できないようにしたい場合は、この環境変数を 1 に設定します。
|
||||||
|
|
||||||
|
### `ENABLE_BALANCE_QUERY` (オプション)
|
||||||
|
|
||||||
|
バランスクエリ機能を有効にしたい場合は、この環境変数を 1 に設定します。
|
||||||
|
|
||||||
|
### `DISABLE_FAST_LINK` (オプション)
|
||||||
|
|
||||||
|
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
|
||||||
|
|
||||||
|
### `WHITE_WEBDEV_ENDPOINTS` (オプション)
|
||||||
|
|
||||||
|
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
|
||||||
|
- 各アドレスは完全なエンドポイントでなければなりません。
|
||||||
|
> `https://xxxx/xxx`
|
||||||
|
- 複数のアドレスは `,` で接続します。
|
||||||
|
|
||||||
|
### `CUSTOM_MODELS` (オプション)
|
||||||
|
|
||||||
|
> 例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` は `qwen-7b-chat` と `glm-6b` をモデルリストに追加し、`gpt-3.5-turbo` を削除し、`gpt-4-1106-preview` のモデル名を `gpt-4-turbo` として表示します。
|
||||||
|
> すべてのモデルを無効にし、特定のモデルを有効にしたい場合は、`-all,+gpt-3.5-turbo` を使用します。これは `gpt-3.5-turbo` のみを有効にすることを意味します。
|
||||||
|
|
||||||
|
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
|
||||||
|
|
||||||
|
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||||
|
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
|
||||||
|
|
||||||
|
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名(deploy-name)を設定できます。
|
||||||
|
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
|
||||||
|
|
||||||
|
### `DEFAULT_MODEL` (オプション)
|
||||||
|
|
||||||
|
デフォルトのモデルを変更します。
|
||||||
|
|
||||||
|
### `DEFAULT_INPUT_TEMPLATE` (オプション)
|
||||||
|
|
||||||
|
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。
|
||||||
|
|
||||||
|
|
||||||
|
## 開発
|
||||||
|
|
||||||
|
下のボタンをクリックして二次開発を開始してください:
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
|
コードを書く前に、プロジェクトのルートディレクトリに `.env.local` ファイルを新規作成し、環境変数を記入します:
|
||||||
|
|
||||||
|
```
|
||||||
|
OPENAI_API_KEY=<your api key here>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### ローカル開発
|
||||||
|
|
||||||
|
1. Node.js 18 と Yarn をインストールします。具体的な方法は ChatGPT にお尋ねください。
|
||||||
|
2. `yarn install && yarn dev` を実行します。⚠️ 注意:このコマンドはローカル開発用であり、デプロイには使用しないでください。
|
||||||
|
3. ローカルでデプロイしたい場合は、`yarn install && yarn build && yarn start` コマンドを使用してください。プロセスを守るために pm2 を使用することもできます。詳細は ChatGPT にお尋ねください。
|
||||||
|
|
||||||
|
|
||||||
|
## デプロイ
|
||||||
|
|
||||||
|
### コンテナデプロイ(推奨)
|
||||||
|
|
||||||
|
> Docker バージョンは 20 以上が必要です。それ以下だとイメージが見つからないというエラーが出ます。
|
||||||
|
|
||||||
|
> ⚠️ 注意:Docker バージョンは最新バージョンより 1~2 日遅れることが多いため、デプロイ後に「更新があります」の通知が出続けることがありますが、正常です。
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull yidadaa/chatgpt-next-web
|
||||||
|
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e OPENAI_API_KEY=sk-xxxx \
|
||||||
|
-e CODE=ページアクセスパスワード \
|
||||||
|
yidadaa/chatgpt-next-web
|
||||||
|
```
|
||||||
|
|
||||||
|
プロキシを指定することもできます:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e OPENAI_API_KEY=sk-xxxx \
|
||||||
|
-e CODE=ページアクセスパスワード \
|
||||||
|
--net=host \
|
||||||
|
-e PROXY_URL=http://127.0.0.1:7890 \
|
||||||
|
yidadaa/chatgpt-next-web
|
||||||
|
```
|
||||||
|
|
||||||
|
ローカルプロキシがアカウントとパスワードを必要とする場合は、以下を使用できます:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
-e PROXY_URL="http://127.0.0.1:7890 user password"
|
||||||
|
```
|
||||||
|
|
||||||
|
他の環境変数を指定する必要がある場合は、上記のコマンドに `-e 環境変数=環境変数値` を追加して指定してください。
|
||||||
|
|
||||||
|
|
||||||
|
### ローカルデプロイ
|
||||||
|
|
||||||
|
コンソールで以下のコマンドを実行します:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ 注意:インストール中に問題が発生した場合は、Docker を使用してデプロイしてください。
|
||||||
|
|
||||||
|
|
||||||
|
## 謝辞
|
||||||
|
|
||||||
|
### 寄付者
|
||||||
|
|
||||||
|
> 英語版をご覧ください。
|
||||||
|
|
||||||
|
### 貢献者
|
||||||
|
|
||||||
|
[プロジェクトの貢献者リストはこちら](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||||
|
|
||||||
|
### 関連プロジェクト
|
||||||
|
|
||||||
|
- [one-api](https://github.com/songquanpeng/one-api): 一つのプラットフォームで大規模モデルのクォータ管理を提供し、市場に出回っているすべての主要な大規模言語モデルをサポートします。
|
||||||
|
|
||||||
|
|
||||||
|
## オープンソースライセンス
|
||||||
|
|
||||||
|
[MIT](https://opensource.org/license/mit/)
|
66
app/api/[provider]/[...path]/route.ts
Normal file
66
app/api/[provider]/[...path]/route.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { ApiPath } from "@/app/constant";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { handle as openaiHandler } from "../../openai";
|
||||||
|
import { handle as azureHandler } from "../../azure";
|
||||||
|
import { handle as googleHandler } from "../../google";
|
||||||
|
import { handle as anthropicHandler } from "../../anthropic";
|
||||||
|
import { handle as baiduHandler } from "../../baidu";
|
||||||
|
import { handle as bytedanceHandler } from "../../bytedance";
|
||||||
|
import { handle as alibabaHandler } from "../../alibaba";
|
||||||
|
import { handle as moonshotHandler } from "../../moonshot";
|
||||||
|
import { handle as stabilityHandler } from "../../stability";
|
||||||
|
import { handle as iflytekHandler } from "../../iflytek";
|
||||||
|
async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { provider: string; path: string[] } },
|
||||||
|
) {
|
||||||
|
const apiPath = `/api/${params.provider}`;
|
||||||
|
console.log(`[${params.provider} Route] params `, params);
|
||||||
|
switch (apiPath) {
|
||||||
|
case ApiPath.Azure:
|
||||||
|
return azureHandler(req, { params });
|
||||||
|
case ApiPath.Google:
|
||||||
|
return googleHandler(req, { params });
|
||||||
|
case ApiPath.Anthropic:
|
||||||
|
return anthropicHandler(req, { params });
|
||||||
|
case ApiPath.Baidu:
|
||||||
|
return baiduHandler(req, { params });
|
||||||
|
case ApiPath.ByteDance:
|
||||||
|
return bytedanceHandler(req, { params });
|
||||||
|
case ApiPath.Alibaba:
|
||||||
|
return alibabaHandler(req, { params });
|
||||||
|
// case ApiPath.Tencent: using "/api/tencent"
|
||||||
|
case ApiPath.Moonshot:
|
||||||
|
return moonshotHandler(req, { params });
|
||||||
|
case ApiPath.Stability:
|
||||||
|
return stabilityHandler(req, { params });
|
||||||
|
case ApiPath.Iflytek:
|
||||||
|
return iflytekHandler(req, { params });
|
||||||
|
default:
|
||||||
|
return openaiHandler(req, { params });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = handle;
|
||||||
|
export const POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
||||||
|
export const preferredRegion = [
|
||||||
|
"arn1",
|
||||||
|
"bom1",
|
||||||
|
"cdg1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"dub1",
|
||||||
|
"fra1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"lhr1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
@@ -14,7 +14,7 @@ import type { RequestPayload } from "@/app/client/platforms/openai";
|
|||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
async function handle(
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { path: string[] } },
|
||||||
) {
|
) {
|
||||||
@@ -40,30 +40,6 @@ async function handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"arn1",
|
|
||||||
"bom1",
|
|
||||||
"cdg1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"dub1",
|
|
||||||
"fra1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"lhr1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
||||||
|
|
||||||
async function request(req: NextRequest) {
|
async function request(req: NextRequest) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@@ -9,13 +9,13 @@ import {
|
|||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "../../auth";
|
import { auth } from "./auth";
|
||||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
|
|
||||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
|
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
|
||||||
|
|
||||||
async function handle(
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { path: string[] } },
|
||||||
) {
|
) {
|
||||||
@@ -56,30 +56,6 @@ async function handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"arn1",
|
|
||||||
"bom1",
|
|
||||||
"cdg1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"dub1",
|
|
||||||
"fra1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"lhr1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
async function request(req: NextRequest) {
|
async function request(req: NextRequest) {
|
73
app/api/artifacts/route.ts
Normal file
73
app/api/artifacts/route.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import md5 from "spark-md5";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
|
||||||
|
async function handle(req: NextRequest, res: NextResponse) {
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
const storeUrl = () =>
|
||||||
|
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
|
||||||
|
const storeHeaders = () => ({
|
||||||
|
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
|
||||||
|
});
|
||||||
|
if (req.method === "POST") {
|
||||||
|
const clonedBody = await req.text();
|
||||||
|
const hashedCode = md5.hash(clonedBody).trim();
|
||||||
|
const body: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
expiration_ttl?: number;
|
||||||
|
} = {
|
||||||
|
key: hashedCode,
|
||||||
|
value: clonedBody,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
|
||||||
|
if (ttl > 60) {
|
||||||
|
body["expiration_ttl"] = ttl;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
const res = await fetch(`${storeUrl()}/bulk`, {
|
||||||
|
headers: {
|
||||||
|
...storeHeaders(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify([body]),
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
console.log("save data", result);
|
||||||
|
if (result?.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ code: 0, id: hashedCode, result },
|
||||||
|
{ status: res.status },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: true, msg: "Save data error" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (req.method === "GET") {
|
||||||
|
const id = req?.nextUrl?.searchParams?.get("id");
|
||||||
|
const res = await fetch(`${storeUrl()}/values/${id}`, {
|
||||||
|
headers: storeHeaders(),
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: res.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: true, msg: "Invalid request" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = handle;
|
||||||
|
export const GET = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
@@ -67,6 +67,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
let systemApiKey: string | undefined;
|
let systemApiKey: string | undefined;
|
||||||
|
|
||||||
switch (modelProvider) {
|
switch (modelProvider) {
|
||||||
|
case ModelProvider.Stability:
|
||||||
|
systemApiKey = serverConfig.stabilityApiKey;
|
||||||
|
break;
|
||||||
case ModelProvider.GeminiPro:
|
case ModelProvider.GeminiPro:
|
||||||
systemApiKey = serverConfig.googleApiKey;
|
systemApiKey = serverConfig.googleApiKey;
|
||||||
break;
|
break;
|
||||||
@@ -82,6 +85,13 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
|||||||
case ModelProvider.Qwen:
|
case ModelProvider.Qwen:
|
||||||
systemApiKey = serverConfig.alibabaApiKey;
|
systemApiKey = serverConfig.alibabaApiKey;
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.Moonshot:
|
||||||
|
systemApiKey = serverConfig.moonshotApiKey;
|
||||||
|
break;
|
||||||
|
case ModelProvider.Iflytek:
|
||||||
|
systemApiKey =
|
||||||
|
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||||
|
break;
|
||||||
case ModelProvider.GPT:
|
case ModelProvider.GPT:
|
||||||
default:
|
default:
|
||||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||||
|
@@ -2,10 +2,10 @@ import { getServerSideConfig } from "@/app/config/server";
|
|||||||
import { ModelProvider } from "@/app/constant";
|
import { ModelProvider } from "@/app/constant";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "../../auth";
|
import { auth } from "./auth";
|
||||||
import { requestOpenai } from "../../common";
|
import { requestOpenai } from "./common";
|
||||||
|
|
||||||
async function handle(
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { path: string[] } },
|
||||||
) {
|
) {
|
||||||
@@ -31,27 +31,3 @@ async function handle(
|
|||||||
return NextResponse.json(prettyObject(e));
|
return NextResponse.json(prettyObject(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"arn1",
|
|
||||||
"bom1",
|
|
||||||
"cdg1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"dub1",
|
|
||||||
"fra1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"lhr1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
@@ -14,7 +14,7 @@ import { getAccessToken } from "@/app/utils/baidu";
|
|||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
async function handle(
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { path: string[] } },
|
||||||
) {
|
) {
|
||||||
@@ -52,30 +52,6 @@ async function handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"arn1",
|
|
||||||
"bom1",
|
|
||||||
"cdg1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"dub1",
|
|
||||||
"fra1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"lhr1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
||||||
|
|
||||||
async function request(req: NextRequest) {
|
async function request(req: NextRequest) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@@ -12,7 +12,7 @@ import { isModelAvailableInServer } from "@/app/utils/model";
|
|||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
async function handle(
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { path: string[] } },
|
||||||
) {
|
) {
|
||||||
@@ -38,30 +38,6 @@ async function handle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"arn1",
|
|
||||||
"bom1",
|
|
||||||
"cdg1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"dub1",
|
|
||||||
"fra1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"lhr1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
||||||
|
|
||||||
async function request(req: NextRequest) {
|
async function request(req: NextRequest) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
@@ -1,11 +1,19 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "../../auth";
|
import { auth } from "./auth";
|
||||||
import { getServerSideConfig } from "@/app/config/server";
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
|
import {
|
||||||
|
ApiPath,
|
||||||
|
GEMINI_BASE_URL,
|
||||||
|
Google,
|
||||||
|
ModelProvider,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
|
||||||
async function handle(
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { provider: string; path: string[] } },
|
||||||
) {
|
) {
|
||||||
console.log("[Google Route] params ", params);
|
console.log("[Google Route] params ", params);
|
||||||
|
|
||||||
@@ -13,32 +21,6 @@ async function handle(
|
|||||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
|
||||||
|
|
||||||
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
|
|
||||||
|
|
||||||
if (!baseUrl.startsWith("http")) {
|
|
||||||
baseUrl = `https://${baseUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseUrl.endsWith("/")) {
|
|
||||||
baseUrl = baseUrl.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
|
|
||||||
|
|
||||||
console.log("[Proxy] ", path);
|
|
||||||
console.log("[Base Url]", baseUrl);
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(
|
|
||||||
() => {
|
|
||||||
controller.abort();
|
|
||||||
},
|
|
||||||
10 * 60 * 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
const authResult = auth(req, ModelProvider.GeminiPro);
|
const authResult = auth(req, ModelProvider.GeminiPro);
|
||||||
if (authResult.error) {
|
if (authResult.error) {
|
||||||
return NextResponse.json(authResult, {
|
return NextResponse.json(authResult, {
|
||||||
@@ -49,9 +31,9 @@ async function handle(
|
|||||||
const bearToken = req.headers.get("Authorization") ?? "";
|
const bearToken = req.headers.get("Authorization") ?? "";
|
||||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||||
|
|
||||||
const key = token ? token : serverConfig.googleApiKey;
|
const apiKey = token ? token : serverConfig.googleApiKey;
|
||||||
|
|
||||||
if (!key) {
|
if (!apiKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
@@ -62,10 +44,63 @@ async function handle(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const response = await request(req, apiKey);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Google] ", e);
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchUrl = `${baseUrl}/${path}?key=${key}${
|
export const GET = handle;
|
||||||
req?.nextUrl?.searchParams?.get("alt") == "sse" ? "&alt=sse" : ""
|
export const POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
||||||
|
export const preferredRegion = [
|
||||||
|
"bom1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function request(req: NextRequest, apiKey: string) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
|
||||||
|
|
||||||
|
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Google, "");
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
|
||||||
|
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
console.log("[Fetch Url] ", fetchUrl);
|
||||||
const fetchOptions: RequestInit = {
|
const fetchOptions: RequestInit = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -97,22 +132,3 @@ async function handle(
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"bom1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
131
app/api/iflytek.ts
Normal file
131
app/api/iflytek.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
Iflytek,
|
||||||
|
IFLYTEK_BASE_URL,
|
||||||
|
ApiPath,
|
||||||
|
ModelProvider,
|
||||||
|
ServiceProvider,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/api/auth";
|
||||||
|
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||||
|
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||||
|
// iflytek
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
export async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[Iflytek Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.Iflytek);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(req);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Iflytek] ", e);
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(req: NextRequest) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// iflytek use base url or just remove the path
|
||||||
|
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}${path}`;
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: req.headers.get("Authorization") ?? "",
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
// try to refuse some request to some models
|
||||||
|
if (serverConfig.customModels && req.body) {
|
||||||
|
try {
|
||||||
|
const clonedBody = await req.text();
|
||||||
|
fetchOptions.body = clonedBody;
|
||||||
|
|
||||||
|
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||||
|
|
||||||
|
// not undefined and is false
|
||||||
|
if (
|
||||||
|
isModelAvailableInServer(
|
||||||
|
serverConfig.customModels,
|
||||||
|
jsonBody?.model as string,
|
||||||
|
ServiceProvider.Iflytek as string,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Iflytek] filter`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
130
app/api/moonshot.ts
Normal file
130
app/api/moonshot.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
Moonshot,
|
||||||
|
MOONSHOT_BASE_URL,
|
||||||
|
ApiPath,
|
||||||
|
ModelProvider,
|
||||||
|
ServiceProvider,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/api/auth";
|
||||||
|
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||||
|
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
export async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[Moonshot Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.Moonshot);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(req);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Moonshot] ", e);
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(req: NextRequest) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// alibaba use base url or just remove the path
|
||||||
|
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Moonshot, "");
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.moonshotUrl || MOONSHOT_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy] ", path);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}${path}`;
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: req.headers.get("Authorization") ?? "",
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
// #1815 try to refuse some request to some models
|
||||||
|
if (serverConfig.customModels && req.body) {
|
||||||
|
try {
|
||||||
|
const clonedBody = await req.text();
|
||||||
|
fetchOptions.body = clonedBody;
|
||||||
|
|
||||||
|
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||||
|
|
||||||
|
// not undefined and is false
|
||||||
|
if (
|
||||||
|
isModelAvailableInServer(
|
||||||
|
serverConfig.customModels,
|
||||||
|
jsonBody?.model as string,
|
||||||
|
ServiceProvider.Moonshot as string,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Moonshot] filter`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
@@ -3,8 +3,8 @@ import { getServerSideConfig } from "@/app/config/server";
|
|||||||
import { ModelProvider, OpenaiPath } from "@/app/constant";
|
import { ModelProvider, OpenaiPath } from "@/app/constant";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "../../auth";
|
import { auth } from "./auth";
|
||||||
import { requestOpenai } from "../../common";
|
import { requestOpenai } from "./common";
|
||||||
|
|
||||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
|
|||||||
return remoteModelRes;
|
return remoteModelRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handle(
|
export async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { path: string[] } },
|
{ params }: { params: { path: string[] } },
|
||||||
) {
|
) {
|
||||||
@@ -70,27 +70,3 @@ async function handle(
|
|||||||
return NextResponse.json(prettyObject(e));
|
return NextResponse.json(prettyObject(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = handle;
|
|
||||||
export const POST = handle;
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = [
|
|
||||||
"arn1",
|
|
||||||
"bom1",
|
|
||||||
"cdg1",
|
|
||||||
"cle1",
|
|
||||||
"cpt1",
|
|
||||||
"dub1",
|
|
||||||
"fra1",
|
|
||||||
"gru1",
|
|
||||||
"hnd1",
|
|
||||||
"iad1",
|
|
||||||
"icn1",
|
|
||||||
"kix1",
|
|
||||||
"lhr1",
|
|
||||||
"pdx1",
|
|
||||||
"sfo1",
|
|
||||||
"sin1",
|
|
||||||
"syd1",
|
|
||||||
];
|
|
99
app/api/stability.ts
Normal file
99
app/api/stability.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
|
||||||
|
import { auth } from "@/app/api/auth";
|
||||||
|
|
||||||
|
export async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[Stability] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
|
||||||
|
|
||||||
|
console.log("[Stability Proxy] ", path);
|
||||||
|
console.log("[Stability Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.Stability);
|
||||||
|
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bearToken = req.headers.get("Authorization") ?? "";
|
||||||
|
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||||
|
|
||||||
|
const key = token ? token : serverConfig.stabilityApiKey;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `missing STABILITY_API_KEY in server env vars`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}/${path}`;
|
||||||
|
console.log("[Stability Url] ", fetchUrl);
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
|
||||||
|
Accept: req.headers.get("Accept") || "application/json",
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
124
app/api/tencent/route.ts
Normal file
124
app/api/tencent/route.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import {
|
||||||
|
TENCENT_BASE_URL,
|
||||||
|
ApiPath,
|
||||||
|
ModelProvider,
|
||||||
|
ServiceProvider,
|
||||||
|
Tencent,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/api/auth";
|
||||||
|
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||||
|
import { getHeader } from "@/app/utils/tencent";
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[Tencent Route] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.Hunyuan);
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request(req);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Tencent] ", e);
|
||||||
|
return NextResponse.json(prettyObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = handle;
|
||||||
|
export const POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
||||||
|
export const preferredRegion = [
|
||||||
|
"arn1",
|
||||||
|
"bom1",
|
||||||
|
"cdg1",
|
||||||
|
"cle1",
|
||||||
|
"cpt1",
|
||||||
|
"dub1",
|
||||||
|
"fra1",
|
||||||
|
"gru1",
|
||||||
|
"hnd1",
|
||||||
|
"iad1",
|
||||||
|
"icn1",
|
||||||
|
"kix1",
|
||||||
|
"lhr1",
|
||||||
|
"pdx1",
|
||||||
|
"sfo1",
|
||||||
|
"sin1",
|
||||||
|
"syd1",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function request(req: NextRequest) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchUrl = baseUrl;
|
||||||
|
|
||||||
|
const body = await req.text();
|
||||||
|
const headers = await getHeader(
|
||||||
|
body,
|
||||||
|
serverConfig.tencentSecretId as string,
|
||||||
|
serverConfig.tencentSecretKey as string,
|
||||||
|
);
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers,
|
||||||
|
method: req.method,
|
||||||
|
body,
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
@@ -29,6 +29,7 @@ async function handle(
|
|||||||
|
|
||||||
const requestUrl = new URL(req.url);
|
const requestUrl = new URL(req.url);
|
||||||
let endpoint = requestUrl.searchParams.get("endpoint");
|
let endpoint = requestUrl.searchParams.get("endpoint");
|
||||||
|
let proxy_method = requestUrl.searchParams.get("proxy_method") || req.method;
|
||||||
|
|
||||||
// Validate the endpoint to prevent potential SSRF attacks
|
// Validate the endpoint to prevent potential SSRF attacks
|
||||||
if (
|
if (
|
||||||
@@ -37,9 +38,13 @@ async function handle(
|
|||||||
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
|
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
|
||||||
const normalizedEndpoint = normalizeUrl(endpoint as string);
|
const normalizedEndpoint = normalizeUrl(endpoint as string);
|
||||||
|
|
||||||
return normalizedEndpoint &&
|
return (
|
||||||
|
normalizedEndpoint &&
|
||||||
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
|
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
|
||||||
normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname);
|
normalizedEndpoint.pathname.startsWith(
|
||||||
|
normalizedAllowedEndpoint.pathname,
|
||||||
|
)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -61,7 +66,11 @@ async function handle(
|
|||||||
const targetPath = `${endpoint}${endpointPath}`;
|
const targetPath = `${endpoint}${endpointPath}`;
|
||||||
|
|
||||||
// only allow MKCOL, GET, PUT
|
// only allow MKCOL, GET, PUT
|
||||||
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
|
if (
|
||||||
|
proxy_method !== "MKCOL" &&
|
||||||
|
proxy_method !== "GET" &&
|
||||||
|
proxy_method !== "PUT"
|
||||||
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
@@ -74,7 +83,7 @@ async function handle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for MKCOL request, only allow request ${folder}
|
// for MKCOL request, only allow request ${folder}
|
||||||
if (req.method === "MKCOL" && !targetPath.endsWith(folder)) {
|
if (proxy_method === "MKCOL" && !targetPath.endsWith(folder)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
@@ -87,7 +96,7 @@ async function handle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for GET request, only allow request ending with fileName
|
// for GET request, only allow request ending with fileName
|
||||||
if (req.method === "GET" && !targetPath.endsWith(fileName)) {
|
if (proxy_method === "GET" && !targetPath.endsWith(fileName)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
@@ -100,7 +109,7 @@ async function handle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// for PUT request, only allow request ending with fileName
|
// for PUT request, only allow request ending with fileName
|
||||||
if (req.method === "PUT" && !targetPath.endsWith(fileName)) {
|
if (proxy_method === "PUT" && !targetPath.endsWith(fileName)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
@@ -114,7 +123,7 @@ async function handle(
|
|||||||
|
|
||||||
const targetUrl = targetPath;
|
const targetUrl = targetPath;
|
||||||
|
|
||||||
const method = req.method;
|
const method = proxy_method || req.method;
|
||||||
const shouldNotHaveBody = ["get", "head"].includes(
|
const shouldNotHaveBody = ["get", "head"].includes(
|
||||||
method?.toLowerCase() ?? "",
|
method?.toLowerCase() ?? "",
|
||||||
);
|
);
|
||||||
@@ -139,7 +148,7 @@ async function handle(
|
|||||||
"[Any Proxy]",
|
"[Any Proxy]",
|
||||||
targetUrl,
|
targetUrl,
|
||||||
{
|
{
|
||||||
method: req.method,
|
method: method,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: fetchResult?.status,
|
status: fetchResult?.status,
|
||||||
|
@@ -6,12 +6,15 @@ import {
|
|||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||||
import { ChatGPTApi } from "./platforms/openai";
|
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
|
||||||
import { GeminiProApi } from "./platforms/google";
|
import { GeminiProApi } from "./platforms/google";
|
||||||
import { ClaudeApi } from "./platforms/anthropic";
|
import { ClaudeApi } from "./platforms/anthropic";
|
||||||
import { ErnieApi } from "./platforms/baidu";
|
import { ErnieApi } from "./platforms/baidu";
|
||||||
import { DoubaoApi } from "./platforms/bytedance";
|
import { DoubaoApi } from "./platforms/bytedance";
|
||||||
import { QwenApi } from "./platforms/alibaba";
|
import { QwenApi } from "./platforms/alibaba";
|
||||||
|
import { HunyuanApi } from "./platforms/tencent";
|
||||||
|
import { MoonshotApi } from "./platforms/moonshot";
|
||||||
|
import { SparkApi } from "./platforms/iflytek";
|
||||||
|
|
||||||
export const ROLES = ["system", "user", "assistant"] as const;
|
export const ROLES = ["system", "user", "assistant"] as const;
|
||||||
export type MessageRole = (typeof ROLES)[number];
|
export type MessageRole = (typeof ROLES)[number];
|
||||||
@@ -40,6 +43,7 @@ export interface LLMConfig {
|
|||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
presence_penalty?: number;
|
presence_penalty?: number;
|
||||||
frequency_penalty?: number;
|
frequency_penalty?: number;
|
||||||
|
size?: DalleRequestPayload["size"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatOptions {
|
export interface ChatOptions {
|
||||||
@@ -62,12 +66,14 @@ export interface LLMModel {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
available: boolean;
|
available: boolean;
|
||||||
provider: LLMModelProvider;
|
provider: LLMModelProvider;
|
||||||
|
sorted: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LLMModelProvider {
|
export interface LLMModelProvider {
|
||||||
id: string;
|
id: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
providerType: string;
|
providerType: string;
|
||||||
|
sorted: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class LLMApi {
|
export abstract class LLMApi {
|
||||||
@@ -117,6 +123,15 @@ export class ClientApi {
|
|||||||
case ModelProvider.Qwen:
|
case ModelProvider.Qwen:
|
||||||
this.llm = new QwenApi();
|
this.llm = new QwenApi();
|
||||||
break;
|
break;
|
||||||
|
case ModelProvider.Hunyuan:
|
||||||
|
this.llm = new HunyuanApi();
|
||||||
|
break;
|
||||||
|
case ModelProvider.Moonshot:
|
||||||
|
this.llm = new MoonshotApi();
|
||||||
|
break;
|
||||||
|
case ModelProvider.Iflytek:
|
||||||
|
this.llm = new SparkApi();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.llm = new ChatGPTApi();
|
this.llm = new ChatGPTApi();
|
||||||
}
|
}
|
||||||
@@ -168,6 +183,19 @@ export class ClientApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBearerToken(
|
||||||
|
apiKey: string,
|
||||||
|
noBearer: boolean = false,
|
||||||
|
): string {
|
||||||
|
return validString(apiKey)
|
||||||
|
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validString(x: string): boolean {
|
||||||
|
return x?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export function getHeaders() {
|
export function getHeaders() {
|
||||||
const accessStore = useAccessStore.getState();
|
const accessStore = useAccessStore.getState();
|
||||||
const chatStore = useChatStore.getState();
|
const chatStore = useChatStore.getState();
|
||||||
@@ -186,6 +214,8 @@ export function getHeaders() {
|
|||||||
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
||||||
const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
|
const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
|
||||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||||
|
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||||
|
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||||
const apiKey = isGoogle
|
const apiKey = isGoogle
|
||||||
? accessStore.googleApiKey
|
? accessStore.googleApiKey
|
||||||
@@ -197,6 +227,12 @@ export function getHeaders() {
|
|||||||
? accessStore.bytedanceApiKey
|
? accessStore.bytedanceApiKey
|
||||||
: isAlibaba
|
: isAlibaba
|
||||||
? accessStore.alibabaApiKey
|
? accessStore.alibabaApiKey
|
||||||
|
: isMoonshot
|
||||||
|
? accessStore.moonshotApiKey
|
||||||
|
: isIflytek
|
||||||
|
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||||
|
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||||
|
: ""
|
||||||
: accessStore.openaiApiKey;
|
: accessStore.openaiApiKey;
|
||||||
return {
|
return {
|
||||||
isGoogle,
|
isGoogle,
|
||||||
@@ -205,6 +241,8 @@ export function getHeaders() {
|
|||||||
isBaidu,
|
isBaidu,
|
||||||
isByteDance,
|
isByteDance,
|
||||||
isAlibaba,
|
isAlibaba,
|
||||||
|
isMoonshot,
|
||||||
|
isIflytek,
|
||||||
apiKey,
|
apiKey,
|
||||||
isEnabledAccessControl,
|
isEnabledAccessControl,
|
||||||
};
|
};
|
||||||
@@ -214,15 +252,6 @@ export function getHeaders() {
|
|||||||
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
|
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBearerToken(apiKey: string, noBearer: boolean = false): string {
|
|
||||||
return validString(apiKey)
|
|
||||||
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function validString(x: string): boolean {
|
|
||||||
return x?.length > 0;
|
|
||||||
}
|
|
||||||
const {
|
const {
|
||||||
isGoogle,
|
isGoogle,
|
||||||
isAzure,
|
isAzure,
|
||||||
@@ -263,6 +292,12 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
|||||||
return new ClientApi(ModelProvider.Doubao);
|
return new ClientApi(ModelProvider.Doubao);
|
||||||
case ServiceProvider.Alibaba:
|
case ServiceProvider.Alibaba:
|
||||||
return new ClientApi(ModelProvider.Qwen);
|
return new ClientApi(ModelProvider.Qwen);
|
||||||
|
case ServiceProvider.Tencent:
|
||||||
|
return new ClientApi(ModelProvider.Hunyuan);
|
||||||
|
case ServiceProvider.Moonshot:
|
||||||
|
return new ClientApi(ModelProvider.Moonshot);
|
||||||
|
case ServiceProvider.Iflytek:
|
||||||
|
return new ClientApi(ModelProvider.Iflytek);
|
||||||
default:
|
default:
|
||||||
return new ClientApi(ModelProvider.GPT);
|
return new ClientApi(ModelProvider.GPT);
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@ import {
|
|||||||
} from "@fortaine/fetch-event-source";
|
} from "@fortaine/fetch-event-source";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
object: string;
|
object: string;
|
||||||
|
@@ -3,7 +3,6 @@ import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
|
|||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||||
import { RequestMessage } from "@/app/typing";
|
|
||||||
import {
|
import {
|
||||||
EventStreamContentType,
|
EventStreamContentType,
|
||||||
fetchEventSource,
|
fetchEventSource,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
import Locale from "../../locales";
|
import Locale from "../../locales";
|
||||||
import { prettyObject } from "@/app/utils/format";
|
import { prettyObject } from "@/app/utils/format";
|
||||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||||
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
|
|
||||||
export type MultiBlockContent = {
|
export type MultiBlockContent = {
|
||||||
@@ -93,7 +93,12 @@ export class ClaudeApi implements LLMApi {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const messages = [...options.messages];
|
// try get base64image from local cache image_url
|
||||||
|
const messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = await preProcessImageContent(v.content);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
const keys = ["system", "user"];
|
const keys = ["system", "user"];
|
||||||
|
|
||||||
|
@@ -77,16 +77,24 @@ export class ErnieApi implements LLMApi {
|
|||||||
|
|
||||||
async chat(options: ChatOptions) {
|
async chat(options: ChatOptions) {
|
||||||
const messages = options.messages.map((v) => ({
|
const messages = options.messages.map((v) => ({
|
||||||
role: v.role,
|
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
|
||||||
|
role: v.role === "system" ? "user" : v.role,
|
||||||
content: getMessageTextContent(v),
|
content: getMessageTextContent(v),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// "error_code": 336006, "error_msg": "the length of messages must be an odd number",
|
// "error_code": 336006, "error_msg": "the length of messages must be an odd number",
|
||||||
if (messages.length % 2 === 0) {
|
if (messages.length % 2 === 0) {
|
||||||
messages.unshift({
|
if (messages.at(0)?.role === "user") {
|
||||||
role: "user",
|
messages.splice(1, 0, {
|
||||||
content: " ",
|
role: "assistant",
|
||||||
});
|
content: " ",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages.unshift({
|
||||||
|
role: "user",
|
||||||
|
content: " ",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelConfig = {
|
const modelConfig = {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||||
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
import { getClientConfig } from "@/app/config/client";
|
import { getClientConfig } from "@/app/config/client";
|
||||||
@@ -14,8 +14,39 @@ import {
|
|||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
|
|
||||||
export class GeminiProApi implements LLMApi {
|
export class GeminiProApi implements LLMApi {
|
||||||
|
path(path: string): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.googleUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
|
||||||
|
}
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Google)) {
|
||||||
|
baseUrl = "https://" + baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||||
|
|
||||||
|
let chatPath = [baseUrl, path].join("/");
|
||||||
|
|
||||||
|
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
|
||||||
|
// if chatPath.startsWith('http') then add key in query string
|
||||||
|
if (chatPath.startsWith("http") && accessStore.googleApiKey) {
|
||||||
|
chatPath += `&key=${accessStore.googleApiKey}`;
|
||||||
|
}
|
||||||
|
return chatPath;
|
||||||
|
}
|
||||||
extractMessage(res: any) {
|
extractMessage(res: any) {
|
||||||
console.log("[Response] gemini-pro response: ", res);
|
console.log("[Response] gemini-pro response: ", res);
|
||||||
|
|
||||||
@@ -28,7 +59,14 @@ export class GeminiProApi implements LLMApi {
|
|||||||
async chat(options: ChatOptions): Promise<void> {
|
async chat(options: ChatOptions): Promise<void> {
|
||||||
const apiClient = this;
|
const apiClient = this;
|
||||||
let multimodal = false;
|
let multimodal = false;
|
||||||
const messages = options.messages.map((v) => {
|
|
||||||
|
// try get base64image from local cache image_url
|
||||||
|
const _messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = await preProcessImageContent(v.content);
|
||||||
|
_messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
const messages = _messages.map((v) => {
|
||||||
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
let parts: any[] = [{ text: getMessageTextContent(v) }];
|
||||||
if (isVisionModel(options.config.model)) {
|
if (isVisionModel(options.config.model)) {
|
||||||
const images = getMessageImages(v);
|
const images = getMessageImages(v);
|
||||||
@@ -70,6 +108,9 @@ export class GeminiProApi implements LLMApi {
|
|||||||
// if (visionModel && messages.length > 1) {
|
// if (visionModel && messages.length > 1) {
|
||||||
// options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
|
// options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
const modelConfig = {
|
const modelConfig = {
|
||||||
...useAppConfig.getState().modelConfig,
|
...useAppConfig.getState().modelConfig,
|
||||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
@@ -91,47 +132,30 @@ export class GeminiProApi implements LLMApi {
|
|||||||
safetySettings: [
|
safetySettings: [
|
||||||
{
|
{
|
||||||
category: "HARM_CATEGORY_HARASSMENT",
|
category: "HARM_CATEGORY_HARASSMENT",
|
||||||
threshold: "BLOCK_ONLY_HIGH",
|
threshold: accessStore.googleSafetySettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||||
threshold: "BLOCK_ONLY_HIGH",
|
threshold: accessStore.googleSafetySettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||||
threshold: "BLOCK_ONLY_HIGH",
|
threshold: accessStore.googleSafetySettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||||
threshold: "BLOCK_ONLY_HIGH",
|
threshold: accessStore.googleSafetySettings,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const accessStore = useAccessStore.getState();
|
|
||||||
|
|
||||||
let baseUrl = "";
|
|
||||||
|
|
||||||
if (accessStore.useCustomConfig) {
|
|
||||||
baseUrl = accessStore.googleUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isApp = !!getClientConfig()?.isApp;
|
|
||||||
|
|
||||||
let shouldStream = !!options.config.stream;
|
let shouldStream = !!options.config.stream;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
options.onController?.(controller);
|
options.onController?.(controller);
|
||||||
try {
|
try {
|
||||||
if (!baseUrl && isApp) {
|
// https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
|
||||||
baseUrl = DEFAULT_API_HOST + "/api/proxy/google/";
|
const chatPath = this.path(Google.ChatPath(modelConfig.model));
|
||||||
}
|
|
||||||
baseUrl = `${baseUrl}/${Google.ChatPath(modelConfig.model)}`.replaceAll(
|
|
||||||
"//",
|
|
||||||
"/",
|
|
||||||
);
|
|
||||||
if (isApp) {
|
|
||||||
baseUrl += `?key=${accessStore.googleApiKey}`;
|
|
||||||
}
|
|
||||||
const chatPayload = {
|
const chatPayload = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(requestPayload),
|
body: JSON.stringify(requestPayload),
|
||||||
@@ -181,10 +205,6 @@ export class GeminiProApi implements LLMApi {
|
|||||||
|
|
||||||
controller.signal.onabort = finish;
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
// https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
|
|
||||||
const chatPath =
|
|
||||||
baseUrl.replace("generateContent", "streamGenerateContent") +
|
|
||||||
(baseUrl.indexOf("?") > -1 ? "&alt=sse" : "?alt=sse");
|
|
||||||
fetchEventSource(chatPath, {
|
fetchEventSource(chatPath, {
|
||||||
...chatPayload,
|
...chatPayload,
|
||||||
async onopen(res) {
|
async onopen(res) {
|
||||||
@@ -259,7 +279,7 @@ export class GeminiProApi implements LLMApi {
|
|||||||
openWhenHidden: true,
|
openWhenHidden: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(baseUrl, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
const resJson = await res.json();
|
const resJson = await res.json();
|
||||||
if (resJson?.promptFeedback?.blockReason) {
|
if (resJson?.promptFeedback?.blockReason) {
|
||||||
@@ -285,14 +305,4 @@ export class GeminiProApi implements LLMApi {
|
|||||||
async models(): Promise<LLMModel[]> {
|
async models(): Promise<LLMModel[]> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
path(path: string): string {
|
|
||||||
return "/api/google/" + path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureProperEnding(str: string) {
|
|
||||||
if (str.startsWith("[") && !str.endsWith("]")) {
|
|
||||||
return str + "]";
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
240
app/client/platforms/iflytek.ts
Normal file
240
app/client/platforms/iflytek.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
ApiPath,
|
||||||
|
DEFAULT_API_HOST,
|
||||||
|
Iflytek,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
|
|
||||||
|
import { ChatOptions, getHeaders, LLMApi, LLMModel } from "../api";
|
||||||
|
import Locale from "../../locales";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
|
||||||
|
import { OpenAIListModelResponse, RequestPayload } from "./openai";
|
||||||
|
|
||||||
|
export class SparkApi implements LLMApi {
|
||||||
|
private disableListModels = true;
|
||||||
|
|
||||||
|
path(path: string): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.iflytekUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
const apiPath = ApiPath.Iflytek;
|
||||||
|
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
|
||||||
|
baseUrl = "https://" + baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||||
|
|
||||||
|
return [baseUrl, path].join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
extractMessage(res: any) {
|
||||||
|
return res.choices?.at(0)?.message?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(options: ChatOptions) {
|
||||||
|
const messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = getMessageTextContent(v);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
providerName: options.config.providerName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = {
|
||||||
|
messages,
|
||||||
|
stream: options.config.stream,
|
||||||
|
model: modelConfig.model,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
presence_penalty: modelConfig.presence_penalty,
|
||||||
|
frequency_penalty: modelConfig.frequency_penalty,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||||
|
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Request] Spark payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path(Iflytek.ChatPath);
|
||||||
|
const chatPayload = {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
let responseText = "";
|
||||||
|
let remainText = "";
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
// Animate response text to make it look smooth
|
||||||
|
function animateResponseText() {
|
||||||
|
if (finished || controller.signal.aborted) {
|
||||||
|
responseText += remainText;
|
||||||
|
console.log("[Response Animation] finished");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||||
|
const fetchText = remainText.slice(0, fetchCount);
|
||||||
|
responseText += fetchText;
|
||||||
|
remainText = remainText.slice(fetchCount);
|
||||||
|
options.onUpdate?.(responseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateResponseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animation
|
||||||
|
animateResponseText();
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
options.onFinish(responseText + remainText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
|
fetchEventSource(chatPath, {
|
||||||
|
...chatPayload,
|
||||||
|
async onopen(res) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log("[Spark] request response content type: ", contentType);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
responseText = await res.clone().text();
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different error scenarios
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
extraInfo = Locale.Error.Unauthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.onError?.(
|
||||||
|
new Error(
|
||||||
|
`Request failed with status ${res.status}: ${extraInfo}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]" || finished) {
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: { content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.delta?.content;
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
remainText += delta;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text);
|
||||||
|
options.onError?.(new Error(`Failed to parse response: ${text}`));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
finish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
options.onError?.(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
options.onError?.(
|
||||||
|
new Error(`Request failed with status ${res.status}: ${errorText}`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = this.extractMessage(resJson);
|
||||||
|
options.onFinish(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Request] failed to make a chat request", e);
|
||||||
|
options.onError?.(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async usage() {
|
||||||
|
return {
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async models(): Promise<LLMModel[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
251
app/client/platforms/moonshot.ts
Normal file
251
app/client/platforms/moonshot.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
"use client";
|
||||||
|
// azure and openai, using same models. so using same LLMApi.
|
||||||
|
import {
|
||||||
|
ApiPath,
|
||||||
|
DEFAULT_API_HOST,
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
Moonshot,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
ServiceProvider,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
|
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
||||||
|
import { preProcessImageContent } from "@/app/utils/chat";
|
||||||
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatOptions,
|
||||||
|
getHeaders,
|
||||||
|
LLMApi,
|
||||||
|
LLMModel,
|
||||||
|
LLMUsage,
|
||||||
|
MultimodalContent,
|
||||||
|
} from "../api";
|
||||||
|
import Locale from "../../locales";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { getMessageTextContent } from "@/app/utils";
|
||||||
|
|
||||||
|
import { OpenAIListModelResponse, RequestPayload } from "./openai";
|
||||||
|
|
||||||
|
export class MoonshotApi implements LLMApi {
|
||||||
|
private disableListModels = true;
|
||||||
|
|
||||||
|
path(path: string): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.moonshotUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
const apiPath = ApiPath.Moonshot;
|
||||||
|
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Moonshot)) {
|
||||||
|
baseUrl = "https://" + baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||||
|
|
||||||
|
return [baseUrl, path].join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
extractMessage(res: any) {
|
||||||
|
return res.choices?.at(0)?.message?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(options: ChatOptions) {
|
||||||
|
const messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = getMessageTextContent(v);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
providerName: options.config.providerName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = {
|
||||||
|
messages,
|
||||||
|
stream: options.config.stream,
|
||||||
|
model: modelConfig.model,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
presence_penalty: modelConfig.presence_penalty,
|
||||||
|
frequency_penalty: modelConfig.frequency_penalty,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||||
|
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Request] openai payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path(Moonshot.ChatPath);
|
||||||
|
const chatPayload = {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
let responseText = "";
|
||||||
|
let remainText = "";
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
// animate response to make it looks smooth
|
||||||
|
function animateResponseText() {
|
||||||
|
if (finished || controller.signal.aborted) {
|
||||||
|
responseText += remainText;
|
||||||
|
console.log("[Response Animation] finished");
|
||||||
|
if (responseText?.length === 0) {
|
||||||
|
options.onError?.(new Error("empty response from server"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||||
|
const fetchText = remainText.slice(0, fetchCount);
|
||||||
|
responseText += fetchText;
|
||||||
|
remainText = remainText.slice(fetchCount);
|
||||||
|
options.onUpdate?.(responseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateResponseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// start animaion
|
||||||
|
animateResponseText();
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
options.onFinish(responseText + remainText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
|
fetchEventSource(chatPath, {
|
||||||
|
...chatPayload,
|
||||||
|
async onopen(res) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log(
|
||||||
|
"[OpenAI] request response content type: ",
|
||||||
|
contentType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
responseText = await res.clone().text();
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [responseText];
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]" || finished) {
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: { content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.delta?.content;
|
||||||
|
const textmoderation = json?.prompt_filter_results;
|
||||||
|
|
||||||
|
if (delta) {
|
||||||
|
remainText += delta;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
finish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
options.onError?.(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = this.extractMessage(resJson);
|
||||||
|
options.onFinish(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Request] failed to make a chat request", e);
|
||||||
|
options.onError?.(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async usage() {
|
||||||
|
return {
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async models(): Promise<LLMModel[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@@ -11,7 +11,13 @@ import {
|
|||||||
} from "@/app/constant";
|
} from "@/app/constant";
|
||||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
||||||
|
import {
|
||||||
|
preProcessImageContent,
|
||||||
|
uploadImage,
|
||||||
|
base64Image2Blob,
|
||||||
|
} from "@/app/utils/chat";
|
||||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||||
|
import { DalleSize } from "@/app/typing";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatOptions,
|
ChatOptions,
|
||||||
@@ -32,6 +38,7 @@ import {
|
|||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
|
isDalle3 as _isDalle3,
|
||||||
} from "@/app/utils";
|
} from "@/app/utils";
|
||||||
|
|
||||||
export interface OpenAIListModelResponse {
|
export interface OpenAIListModelResponse {
|
||||||
@@ -57,6 +64,14 @@ export interface RequestPayload {
|
|||||||
max_tokens?: number;
|
max_tokens?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DalleRequestPayload {
|
||||||
|
model: string;
|
||||||
|
prompt: string;
|
||||||
|
response_format: "url" | "b64_json";
|
||||||
|
n: number;
|
||||||
|
size: DalleSize;
|
||||||
|
}
|
||||||
|
|
||||||
export class ChatGPTApi implements LLMApi {
|
export class ChatGPTApi implements LLMApi {
|
||||||
private disableListModels = true;
|
private disableListModels = true;
|
||||||
|
|
||||||
@@ -99,17 +114,31 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
|
return cloudflareAIGatewayUrl([baseUrl, path].join("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
extractMessage(res: any) {
|
async extractMessage(res: any) {
|
||||||
return res.choices?.at(0)?.message?.content ?? "";
|
if (res.error) {
|
||||||
|
return "```\n" + JSON.stringify(res, null, 4) + "\n```";
|
||||||
|
}
|
||||||
|
// dalle3 model return url, using url create image message
|
||||||
|
if (res.data) {
|
||||||
|
let url = res.data?.at(0)?.url ?? "";
|
||||||
|
const b64_json = res.data?.at(0)?.b64_json ?? "";
|
||||||
|
if (!url && b64_json) {
|
||||||
|
// uploadImage
|
||||||
|
url = await uploadImage(base64Image2Blob(b64_json, "image/png"));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return res.choices?.at(0)?.message?.content ?? res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async chat(options: ChatOptions) {
|
async chat(options: ChatOptions) {
|
||||||
const visionModel = isVisionModel(options.config.model);
|
|
||||||
const messages = options.messages.map((v) => ({
|
|
||||||
role: v.role,
|
|
||||||
content: visionModel ? v.content : getMessageTextContent(v),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const modelConfig = {
|
const modelConfig = {
|
||||||
...useAppConfig.getState().modelConfig,
|
...useAppConfig.getState().modelConfig,
|
||||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
@@ -119,26 +148,52 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestPayload: RequestPayload = {
|
let requestPayload: RequestPayload | DalleRequestPayload;
|
||||||
messages,
|
|
||||||
stream: options.config.stream,
|
|
||||||
model: modelConfig.model,
|
|
||||||
temperature: modelConfig.temperature,
|
|
||||||
presence_penalty: modelConfig.presence_penalty,
|
|
||||||
frequency_penalty: modelConfig.frequency_penalty,
|
|
||||||
top_p: modelConfig.top_p,
|
|
||||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
|
||||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
|
||||||
};
|
|
||||||
|
|
||||||
// add max_tokens to vision model
|
const isDalle3 = _isDalle3(options.config.model);
|
||||||
if (visionModel && modelConfig.model.includes("preview")) {
|
if (isDalle3) {
|
||||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
const prompt = getMessageTextContent(
|
||||||
|
options.messages.slice(-1)?.pop() as any,
|
||||||
|
);
|
||||||
|
requestPayload = {
|
||||||
|
model: options.config.model,
|
||||||
|
prompt,
|
||||||
|
// URLs are only valid for 60 minutes after the image has been generated.
|
||||||
|
response_format: "b64_json", // using b64_json, and save image in CacheStorage
|
||||||
|
n: 1,
|
||||||
|
size: options.config?.size ?? "1024x1024",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const visionModel = isVisionModel(options.config.model);
|
||||||
|
const messages: ChatOptions["messages"] = [];
|
||||||
|
for (const v of options.messages) {
|
||||||
|
const content = visionModel
|
||||||
|
? await preProcessImageContent(v.content)
|
||||||
|
: getMessageTextContent(v);
|
||||||
|
messages.push({ role: v.role, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPayload = {
|
||||||
|
messages,
|
||||||
|
stream: options.config.stream,
|
||||||
|
model: modelConfig.model,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
presence_penalty: modelConfig.presence_penalty,
|
||||||
|
frequency_penalty: modelConfig.frequency_penalty,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||||
|
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||||
|
};
|
||||||
|
|
||||||
|
// add max_tokens to vision model
|
||||||
|
if (visionModel && modelConfig.model.includes("preview")) {
|
||||||
|
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Request] openai payload: ", requestPayload);
|
console.log("[Request] openai payload: ", requestPayload);
|
||||||
|
|
||||||
const shouldStream = !!options.config.stream;
|
const shouldStream = !isDalle3 && !!options.config.stream;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
options.onController?.(controller);
|
options.onController?.(controller);
|
||||||
|
|
||||||
@@ -164,13 +219,15 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
model?.provider?.providerName === ServiceProvider.Azure,
|
model?.provider?.providerName === ServiceProvider.Azure,
|
||||||
);
|
);
|
||||||
chatPath = this.path(
|
chatPath = this.path(
|
||||||
Azure.ChatPath(
|
(isDalle3 ? Azure.ImagePath : Azure.ChatPath)(
|
||||||
(model?.displayName ?? model?.name) as string,
|
(model?.displayName ?? model?.name) as string,
|
||||||
useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
|
useCustomConfig ? useAccessStore.getState().azureApiVersion : "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
chatPath = this.path(OpenaiPath.ChatPath);
|
chatPath = this.path(
|
||||||
|
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const chatPayload = {
|
const chatPayload = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -182,7 +239,7 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
// make a fetch request
|
// make a fetch request
|
||||||
const requestTimeoutId = setTimeout(
|
const requestTimeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
REQUEST_TIMEOUT_MS,
|
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
@@ -313,7 +370,7 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
const resJson = await res.json();
|
const resJson = await res.json();
|
||||||
const message = this.extractMessage(resJson);
|
const message = await this.extractMessage(resJson);
|
||||||
options.onFinish(message);
|
options.onFinish(message);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -407,13 +464,17 @@ export class ChatGPTApi implements LLMApi {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//由于目前 OpenAI 的 disableListModels 默认为 true,所以当前实际不会运行到这场
|
||||||
|
let seq = 1000; //同 Constant.ts 中的排序保持一致
|
||||||
return chatModels.map((m) => ({
|
return chatModels.map((m) => ({
|
||||||
name: m.id,
|
name: m.id,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "openai",
|
id: "openai",
|
||||||
providerName: "OpenAI",
|
providerName: "OpenAI",
|
||||||
providerType: "openai",
|
providerType: "openai",
|
||||||
|
sorted: 1,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
268
app/client/platforms/tencent.ts
Normal file
268
app/client/platforms/tencent.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"use client";
|
||||||
|
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||||
|
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatOptions,
|
||||||
|
getHeaders,
|
||||||
|
LLMApi,
|
||||||
|
LLMModel,
|
||||||
|
MultimodalContent,
|
||||||
|
} from "../api";
|
||||||
|
import Locale from "../../locales";
|
||||||
|
import {
|
||||||
|
EventStreamContentType,
|
||||||
|
fetchEventSource,
|
||||||
|
} from "@fortaine/fetch-event-source";
|
||||||
|
import { prettyObject } from "@/app/utils/format";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||||
|
import mapKeys from "lodash-es/mapKeys";
|
||||||
|
import mapValues from "lodash-es/mapValues";
|
||||||
|
import isArray from "lodash-es/isArray";
|
||||||
|
import isObject from "lodash-es/isObject";
|
||||||
|
|
||||||
|
export interface OpenAIListModelResponse {
|
||||||
|
object: string;
|
||||||
|
data: Array<{
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
root: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestPayload {
|
||||||
|
Messages: {
|
||||||
|
Role: "system" | "user" | "assistant";
|
||||||
|
Content: string | MultimodalContent[];
|
||||||
|
}[];
|
||||||
|
Stream?: boolean;
|
||||||
|
Model: string;
|
||||||
|
Temperature: number;
|
||||||
|
TopP: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function capitalizeKeys(obj: any): any {
|
||||||
|
if (isArray(obj)) {
|
||||||
|
return obj.map(capitalizeKeys);
|
||||||
|
} else if (isObject(obj)) {
|
||||||
|
return mapValues(
|
||||||
|
mapKeys(obj, (value: any, key: string) =>
|
||||||
|
key.replace(/(^|_)(\w)/g, (m, $1, $2) => $2.toUpperCase()),
|
||||||
|
),
|
||||||
|
capitalizeKeys,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HunyuanApi implements LLMApi {
|
||||||
|
path(): string {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
|
||||||
|
let baseUrl = "";
|
||||||
|
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
baseUrl = accessStore.tencentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.length === 0) {
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
baseUrl = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||||
|
: ApiPath.Tencent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
|
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
|
||||||
|
baseUrl = "https://" + baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Proxy Endpoint] ", baseUrl);
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractMessage(res: any) {
|
||||||
|
return res.Choices?.at(0)?.Message?.Content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async chat(options: ChatOptions) {
|
||||||
|
const visionModel = isVisionModel(options.config.model);
|
||||||
|
const messages = options.messages.map((v, index) => ({
|
||||||
|
// "Messages 中 system 角色必须位于列表的最开始"
|
||||||
|
role: index !== 0 && v.role === "system" ? "user" : v.role,
|
||||||
|
content: visionModel ? v.content : getMessageTextContent(v),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modelConfig = {
|
||||||
|
...useAppConfig.getState().modelConfig,
|
||||||
|
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||||
|
...{
|
||||||
|
model: options.config.model,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPayload: RequestPayload = capitalizeKeys({
|
||||||
|
model: modelConfig.model,
|
||||||
|
messages,
|
||||||
|
temperature: modelConfig.temperature,
|
||||||
|
top_p: modelConfig.top_p,
|
||||||
|
stream: options.config.stream,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Request] Tencent payload: ", requestPayload);
|
||||||
|
|
||||||
|
const shouldStream = !!options.config.stream;
|
||||||
|
const controller = new AbortController();
|
||||||
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatPath = this.path();
|
||||||
|
const chatPayload = {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
let responseText = "";
|
||||||
|
let remainText = "";
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
// animate response to make it looks smooth
|
||||||
|
function animateResponseText() {
|
||||||
|
if (finished || controller.signal.aborted) {
|
||||||
|
responseText += remainText;
|
||||||
|
console.log("[Response Animation] finished");
|
||||||
|
if (responseText?.length === 0) {
|
||||||
|
options.onError?.(new Error("empty response from server"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainText.length > 0) {
|
||||||
|
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||||
|
const fetchText = remainText.slice(0, fetchCount);
|
||||||
|
responseText += fetchText;
|
||||||
|
remainText = remainText.slice(fetchCount);
|
||||||
|
options.onUpdate?.(responseText, fetchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(animateResponseText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// start animaion
|
||||||
|
animateResponseText();
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
options.onFinish(responseText + remainText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.signal.onabort = finish;
|
||||||
|
|
||||||
|
fetchEventSource(chatPath, {
|
||||||
|
...chatPayload,
|
||||||
|
async onopen(res) {
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
console.log(
|
||||||
|
"[Tencent] request response content type: ",
|
||||||
|
contentType,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (contentType?.startsWith("text/plain")) {
|
||||||
|
responseText = await res.clone().text();
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!res.ok ||
|
||||||
|
!res.headers
|
||||||
|
.get("content-type")
|
||||||
|
?.startsWith(EventStreamContentType) ||
|
||||||
|
res.status !== 200
|
||||||
|
) {
|
||||||
|
const responseTexts = [responseText];
|
||||||
|
let extraInfo = await res.clone().text();
|
||||||
|
try {
|
||||||
|
const resJson = await res.clone().json();
|
||||||
|
extraInfo = prettyObject(resJson);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
responseTexts.push(Locale.Error.Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraInfo) {
|
||||||
|
responseTexts.push(extraInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
responseText = responseTexts.join("\n\n");
|
||||||
|
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onmessage(msg) {
|
||||||
|
if (msg.data === "[DONE]" || finished) {
|
||||||
|
return finish();
|
||||||
|
}
|
||||||
|
const text = msg.data;
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.Choices as Array<{
|
||||||
|
Delta: { Content: string };
|
||||||
|
}>;
|
||||||
|
const delta = choices[0]?.Delta?.Content;
|
||||||
|
if (delta) {
|
||||||
|
remainText += delta;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Request] parse error", text, msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
finish();
|
||||||
|
},
|
||||||
|
onerror(e) {
|
||||||
|
options.onError?.(e);
|
||||||
|
throw e;
|
||||||
|
},
|
||||||
|
openWhenHidden: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const res = await fetch(chatPath, chatPayload);
|
||||||
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
|
const resJson = await res.json();
|
||||||
|
const message = this.extractMessage(resJson);
|
||||||
|
options.onFinish(message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Request] failed to make a chat request", e);
|
||||||
|
options.onError?.(e as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async usage() {
|
||||||
|
return {
|
||||||
|
used: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async models(): Promise<LLMModel[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@@ -41,13 +41,16 @@ interface ChatCommands {
|
|||||||
del?: Command;
|
del?: Command;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatCommandPrefix = ":";
|
// Compatible with Chinese colon character ":"
|
||||||
|
export const ChatCommandPrefix = /^[::]/;
|
||||||
|
|
||||||
export function useChatCommand(commands: ChatCommands = {}) {
|
export function useChatCommand(commands: ChatCommands = {}) {
|
||||||
function extract(userInput: string) {
|
function extract(userInput: string) {
|
||||||
return (
|
const match = userInput.match(ChatCommandPrefix);
|
||||||
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
|
if (match) {
|
||||||
) as keyof ChatCommands;
|
return userInput.slice(1) as keyof ChatCommands;
|
||||||
|
}
|
||||||
|
return userInput as keyof ChatCommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
function search(userInput: string) {
|
function search(userInput: string) {
|
||||||
@@ -57,7 +60,7 @@ export function useChatCommand(commands: ChatCommands = {}) {
|
|||||||
.filter((c) => c.startsWith(input))
|
.filter((c) => c.startsWith(input))
|
||||||
.map((c) => ({
|
.map((c) => ({
|
||||||
title: desc[c as keyof ChatCommands],
|
title: desc[c as keyof ChatCommands],
|
||||||
content: ChatCommandPrefix + c,
|
content: ":" + c,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
app/components/artifacts.module.scss
Normal file
31
app/components/artifacts.module.scss
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.artifacts {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
&-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--second);
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
&-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0 20px 20px 20px;
|
||||||
|
background-color: var(--second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifacts-iframe {
|
||||||
|
width: 100%;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--gray);
|
||||||
|
}
|
234
app/components/artifacts.tsx
Normal file
234
app/components/artifacts.tsx
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { useEffect, useState, useRef, useMemo } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import { useWindowSize } from "@/app/utils";
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import ExportIcon from "../icons/share.svg";
|
||||||
|
import CopyIcon from "../icons/copy.svg";
|
||||||
|
import DownloadIcon from "../icons/download.svg";
|
||||||
|
import GithubIcon from "../icons/github.svg";
|
||||||
|
import LoadingButtonIcon from "../icons/loading.svg";
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { Modal, showToast } from "./ui-lib";
|
||||||
|
import { copyToClipboard, downloadAs } from "../utils";
|
||||||
|
import { Path, ApiPath, REPO_URL } from "@/app/constant";
|
||||||
|
import { Loading } from "./home";
|
||||||
|
import styles from "./artifacts.module.scss";
|
||||||
|
|
||||||
|
export function HTMLPreview(props: {
|
||||||
|
code: string;
|
||||||
|
autoHeight?: boolean;
|
||||||
|
height?: number | string;
|
||||||
|
onLoad?: (title?: string) => void;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLIFrameElement>(null);
|
||||||
|
const frameId = useRef<string>(nanoid());
|
||||||
|
const [iframeHeight, setIframeHeight] = useState(600);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
/*
|
||||||
|
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
|
||||||
|
* 1. using srcdoc
|
||||||
|
* 2. using src with dataurl:
|
||||||
|
* easy to share
|
||||||
|
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = (e: any) => {
|
||||||
|
const { id, height, title } = e.data;
|
||||||
|
setTitle(title);
|
||||||
|
if (id == frameId.current) {
|
||||||
|
setIframeHeight(height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("message", handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const height = useMemo(() => {
|
||||||
|
if (!props.autoHeight) return props.height || 600;
|
||||||
|
if (typeof props.height === "string") {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
const parentHeight = props.height || 600;
|
||||||
|
return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40;
|
||||||
|
}, [props.autoHeight, props.height, iframeHeight]);
|
||||||
|
|
||||||
|
const srcDoc = useMemo(() => {
|
||||||
|
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
||||||
|
if (props.code.includes("</head>")) {
|
||||||
|
props.code.replace("</head>", "</head>" + script);
|
||||||
|
}
|
||||||
|
return props.code + script;
|
||||||
|
}, [props.code]);
|
||||||
|
|
||||||
|
const handleOnLoad = () => {
|
||||||
|
if (props?.onLoad) {
|
||||||
|
props.onLoad(title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
className={styles["artifacts-iframe"]}
|
||||||
|
id={frameId.current}
|
||||||
|
ref={ref}
|
||||||
|
sandbox="allow-forms allow-modals allow-scripts"
|
||||||
|
style={{ height }}
|
||||||
|
srcDoc={srcDoc}
|
||||||
|
onLoad={handleOnLoad}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArtifactsShareButton({
|
||||||
|
getCode,
|
||||||
|
id,
|
||||||
|
style,
|
||||||
|
fileName,
|
||||||
|
}: {
|
||||||
|
getCode: () => string;
|
||||||
|
id?: string;
|
||||||
|
style?: any;
|
||||||
|
fileName?: string;
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [name, setName] = useState(id);
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const shareUrl = useMemo(
|
||||||
|
() => [location.origin, "#", Path.Artifacts, "/", name].join(""),
|
||||||
|
[name],
|
||||||
|
);
|
||||||
|
const upload = (code: string) =>
|
||||||
|
id
|
||||||
|
? Promise.resolve({ id })
|
||||||
|
: fetch(ApiPath.Artifacts, {
|
||||||
|
method: "POST",
|
||||||
|
body: code,
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(({ id }) => {
|
||||||
|
if (id) {
|
||||||
|
return { id };
|
||||||
|
}
|
||||||
|
throw Error();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
showToast(Locale.Export.Artifacts.Error);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="window-action-button" style={style}>
|
||||||
|
<IconButton
|
||||||
|
icon={loading ? <LoadingButtonIcon /> : <ExportIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Export.Artifacts.Title}
|
||||||
|
onClick={() => {
|
||||||
|
if (loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
upload(getCode())
|
||||||
|
.then((res) => {
|
||||||
|
if (res?.id) {
|
||||||
|
setShow(true);
|
||||||
|
setName(res?.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{show && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Export.Artifacts.Title}
|
||||||
|
onClose={() => setShow(false)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="download"
|
||||||
|
icon={<DownloadIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Export.Download}
|
||||||
|
onClick={() => {
|
||||||
|
downloadAs(getCode(), `${fileName || name}.html`).then(() =>
|
||||||
|
setShow(false),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Chat.Actions.Copy}
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(shareUrl).then(() => setShow(false));
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a target="_blank" href={shareUrl}>
|
||||||
|
{shareUrl}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Artifacts() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [fileName, setFileName] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetch(`${ApiPath.Artifacts}?id=${id}`)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status > 300) {
|
||||||
|
throw Error("can not get content");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then(setCode)
|
||||||
|
.catch((e) => {
|
||||||
|
showToast(Locale.Export.Artifacts.Error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["artifacts"]}>
|
||||||
|
<div className={styles["artifacts-header"]}>
|
||||||
|
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||||
|
<IconButton bordered icon={<GithubIcon />} shadow />
|
||||||
|
</a>
|
||||||
|
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
|
||||||
|
<ArtifactsShareButton
|
||||||
|
id={id}
|
||||||
|
getCode={() => code}
|
||||||
|
fileName={fileName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles["artifacts-content"]}>
|
||||||
|
{loading && <Loading />}
|
||||||
|
{code && (
|
||||||
|
<HTMLPreview
|
||||||
|
code={code}
|
||||||
|
autoHeight={false}
|
||||||
|
height={"100%"}
|
||||||
|
onLoad={(title) => {
|
||||||
|
setFileName(title as string);
|
||||||
|
setLoading(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import styles from "./button.module.scss";
|
import styles from "./button.module.scss";
|
||||||
|
import { CSSProperties } from "react";
|
||||||
|
|
||||||
export type ButtonType = "primary" | "danger" | null;
|
export type ButtonType = "primary" | "danger" | null;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export function IconButton(props: {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -31,6 +33,7 @@ export function IconButton(props: {
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={props.tabIndex}
|
tabIndex={props.tabIndex}
|
||||||
autoFocus={props.autoFocus}
|
autoFocus={props.autoFocus}
|
||||||
|
style={props.style}
|
||||||
>
|
>
|
||||||
{props.icon && (
|
{props.icon && (
|
||||||
<div
|
<div
|
||||||
|
@@ -37,6 +37,8 @@ import AutoIcon from "../icons/auto.svg";
|
|||||||
import BottomIcon from "../icons/bottom.svg";
|
import BottomIcon from "../icons/bottom.svg";
|
||||||
import StopIcon from "../icons/pause.svg";
|
import StopIcon from "../icons/pause.svg";
|
||||||
import RobotIcon from "../icons/robot.svg";
|
import RobotIcon from "../icons/robot.svg";
|
||||||
|
import SizeIcon from "../icons/size.svg";
|
||||||
|
import PluginIcon from "../icons/plugin.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
@@ -59,13 +61,15 @@ import {
|
|||||||
getMessageTextContent,
|
getMessageTextContent,
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
|
isDalle3,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { compressImage } from "@/app/utils/chat";
|
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import { ChatControllerPool } from "../client/controller";
|
import { ChatControllerPool } from "../client/controller";
|
||||||
|
import { DalleSize } from "../typing";
|
||||||
import { Prompt, usePromptStore } from "../store/prompt";
|
import { Prompt, usePromptStore } from "../store/prompt";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
@@ -89,6 +93,7 @@ import {
|
|||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
UNFINISHED_INPUT,
|
UNFINISHED_INPUT,
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
|
Plugin,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { Avatar } from "./emoji";
|
import { Avatar } from "./emoji";
|
||||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||||
@@ -245,11 +250,11 @@ function useSubmitHandler() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RenderPompt = Pick<Prompt, "title" | "content">;
|
export type RenderPrompt = Pick<Prompt, "title" | "content">;
|
||||||
|
|
||||||
export function PromptHints(props: {
|
export function PromptHints(props: {
|
||||||
prompts: RenderPompt[];
|
prompts: RenderPrompt[];
|
||||||
onPromptSelect: (prompt: RenderPompt) => void;
|
onPromptSelect: (prompt: RenderPrompt) => void;
|
||||||
}) {
|
}) {
|
||||||
const noPrompts = props.prompts.length === 0;
|
const noPrompts = props.prompts.length === 0;
|
||||||
const [selectIndex, setSelectIndex] = useState(0);
|
const [selectIndex, setSelectIndex] = useState(0);
|
||||||
@@ -338,7 +343,7 @@ function ClearContextDivider() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatAction(props: {
|
export function ChatAction(props: {
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@@ -476,8 +481,14 @@ export function ChatActions(props: {
|
|||||||
return model?.displayName ?? "";
|
return model?.displayName ?? "";
|
||||||
}, [models, currentModel, currentProviderName]);
|
}, [models, currentModel, currentProviderName]);
|
||||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||||
|
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
||||||
const [showUploadImage, setShowUploadImage] = useState(false);
|
const [showUploadImage, setShowUploadImage] = useState(false);
|
||||||
|
|
||||||
|
const [showSizeSelector, setShowSizeSelector] = useState(false);
|
||||||
|
const dalle3Sizes: DalleSize[] = ["1024x1024", "1792x1024", "1024x1792"];
|
||||||
|
const currentSize =
|
||||||
|
chatStore.currentSession().mask.modelConfig?.size ?? "1024x1024";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const show = isVisionModel(currentModel);
|
const show = isVisionModel(currentModel);
|
||||||
setShowUploadImage(show);
|
setShowUploadImage(show);
|
||||||
@@ -620,6 +631,61 @@ export function ChatActions(props: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isDalle3(currentModel) && (
|
||||||
|
<ChatAction
|
||||||
|
onClick={() => setShowSizeSelector(true)}
|
||||||
|
text={currentSize}
|
||||||
|
icon={<SizeIcon />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSizeSelector && (
|
||||||
|
<Selector
|
||||||
|
defaultSelectedValue={currentSize}
|
||||||
|
items={dalle3Sizes.map((m) => ({
|
||||||
|
title: m,
|
||||||
|
value: m,
|
||||||
|
}))}
|
||||||
|
onClose={() => setShowSizeSelector(false)}
|
||||||
|
onSelection={(s) => {
|
||||||
|
if (s.length === 0) return;
|
||||||
|
const size = s[0];
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.mask.modelConfig.size = size;
|
||||||
|
});
|
||||||
|
showToast(size);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ChatAction
|
||||||
|
onClick={() => setShowPluginSelector(true)}
|
||||||
|
text={Locale.Plugin.Name}
|
||||||
|
icon={<PluginIcon />}
|
||||||
|
/>
|
||||||
|
{showPluginSelector && (
|
||||||
|
<Selector
|
||||||
|
multiple
|
||||||
|
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: Locale.Plugin.Artifacts,
|
||||||
|
value: Plugin.Artifacts,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onClose={() => setShowPluginSelector(false)}
|
||||||
|
onSelection={(s) => {
|
||||||
|
const plugin = s[0];
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.mask.plugin = s;
|
||||||
|
});
|
||||||
|
if (plugin) {
|
||||||
|
showToast(plugin);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -701,6 +767,7 @@ function _Chat() {
|
|||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const fontSize = config.fontSize;
|
const fontSize = config.fontSize;
|
||||||
|
const fontFamily = config.fontFamily;
|
||||||
|
|
||||||
const [showExport, setShowExport] = useState(false);
|
const [showExport, setShowExport] = useState(false);
|
||||||
|
|
||||||
@@ -727,7 +794,7 @@ function _Chat() {
|
|||||||
|
|
||||||
// prompt hints
|
// prompt hints
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
|
const [promptHints, setPromptHints] = useState<RenderPrompt[]>([]);
|
||||||
const onSearch = useDebouncedCallback(
|
const onSearch = useDebouncedCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
const matchedPrompts = promptStore.search(text);
|
const matchedPrompts = promptStore.search(text);
|
||||||
@@ -780,7 +847,7 @@ function _Chat() {
|
|||||||
// clear search results
|
// clear search results
|
||||||
if (n === 0) {
|
if (n === 0) {
|
||||||
setPromptHints([]);
|
setPromptHints([]);
|
||||||
} else if (text.startsWith(ChatCommandPrefix)) {
|
} else if (text.match(ChatCommandPrefix)) {
|
||||||
setPromptHints(chatCommands.search(text));
|
setPromptHints(chatCommands.search(text));
|
||||||
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||||
// check if need to trigger auto completion
|
// check if need to trigger auto completion
|
||||||
@@ -812,7 +879,7 @@ function _Chat() {
|
|||||||
setAutoScroll(true);
|
setAutoScroll(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPromptSelect = (prompt: RenderPompt) => {
|
const onPromptSelect = (prompt: RenderPrompt) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPromptHints([]);
|
setPromptHints([]);
|
||||||
|
|
||||||
@@ -1167,7 +1234,7 @@ function _Chat() {
|
|||||||
...(await new Promise<string[]>((res, rej) => {
|
...(await new Promise<string[]>((res, rej) => {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
const imagesData: string[] = [];
|
const imagesData: string[] = [];
|
||||||
compressImage(file, 256 * 1024)
|
uploadImageRemote(file)
|
||||||
.then((dataUrl) => {
|
.then((dataUrl) => {
|
||||||
imagesData.push(dataUrl);
|
imagesData.push(dataUrl);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
@@ -1209,7 +1276,7 @@ function _Chat() {
|
|||||||
const imagesData: string[] = [];
|
const imagesData: string[] = [];
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = event.target.files[i];
|
const file = event.target.files[i];
|
||||||
compressImage(file, 256 * 1024)
|
uploadImageRemote(file)
|
||||||
.then((dataUrl) => {
|
.then((dataUrl) => {
|
||||||
imagesData.push(dataUrl);
|
imagesData.push(dataUrl);
|
||||||
if (
|
if (
|
||||||
@@ -1439,6 +1506,7 @@ function _Chat() {
|
|||||||
)}
|
)}
|
||||||
<div className={styles["chat-message-item"]}>
|
<div className={styles["chat-message-item"]}>
|
||||||
<Markdown
|
<Markdown
|
||||||
|
key={message.streaming ? "loading" : "done"}
|
||||||
content={getMessageTextContent(message)}
|
content={getMessageTextContent(message)}
|
||||||
loading={
|
loading={
|
||||||
(message.preview || message.streaming) &&
|
(message.preview || message.streaming) &&
|
||||||
@@ -1451,6 +1519,7 @@ function _Chat() {
|
|||||||
setUserInput(getMessageTextContent(message));
|
setUserInput(getMessageTextContent(message));
|
||||||
}}
|
}}
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
|
fontFamily={fontFamily}
|
||||||
parentRef={scrollRef}
|
parentRef={scrollRef}
|
||||||
defaultShow={i >= messages.length - 6}
|
defaultShow={i >= messages.length - 6}
|
||||||
/>
|
/>
|
||||||
@@ -1545,6 +1614,7 @@ function _Chat() {
|
|||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
style={{
|
style={{
|
||||||
fontSize: config.fontSize,
|
fontSize: config.fontSize,
|
||||||
|
fontFamily: config.fontFamily,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{attachImages.length != 0 && (
|
{attachImages.length != 0 && (
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import GithubIcon from "../icons/github.svg";
|
import GithubIcon from "../icons/github.svg";
|
||||||
|
@@ -541,7 +541,7 @@ export function ImagePreviewer(props: {
|
|||||||
<div>
|
<div>
|
||||||
<div className={styles["main-title"]}>NextChat</div>
|
<div className={styles["main-title"]}>NextChat</div>
|
||||||
<div className={styles["sub-title"]}>
|
<div className={styles["sub-title"]}>
|
||||||
github.com/Yidadaa/ChatGPT-Next-Web
|
github.com/ChatGPTNextWeb/ChatGPT-Next-Web
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["icons"]}>
|
<div className={styles["icons"]}>
|
||||||
<ExportAvatar avatar={config.avatar} />
|
<ExportAvatar avatar={config.avatar} />
|
||||||
@@ -583,6 +583,7 @@ export function ImagePreviewer(props: {
|
|||||||
<Markdown
|
<Markdown
|
||||||
content={getMessageTextContent(m)}
|
content={getMessageTextContent(m)}
|
||||||
fontSize={config.fontSize}
|
fontSize={config.fontSize}
|
||||||
|
fontFamily={config.fontFamily}
|
||||||
defaultShow
|
defaultShow
|
||||||
/>
|
/>
|
||||||
{getMessageImages(m).length == 1 && (
|
{getMessageImages(m).length == 1 && (
|
||||||
|
@@ -137,12 +137,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
position: absolute;
|
display: inline-flex;
|
||||||
right: 0;
|
}
|
||||||
bottom: 18px;
|
|
||||||
|
.sidebar-title-container {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
|
@@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Artifacts = dynamic(async () => (await import("./artifacts")).Artifacts, {
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
});
|
||||||
|
|
||||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
@@ -55,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
|||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
});
|
||||||
|
|
||||||
export function useSwitchTheme() {
|
export function useSwitchTheme() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
@@ -122,11 +130,23 @@ const loadAsyncGoogleFont = () => {
|
|||||||
document.head.appendChild(linkEl);
|
document.head.appendChild(linkEl);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function WindowContent(props: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
||||||
|
{props?.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const isArtifact = location.pathname.includes(Path.Artifacts);
|
||||||
const isHome = location.pathname === Path.Home;
|
const isHome = location.pathname === Path.Home;
|
||||||
const isAuth = location.pathname === Path.Auth;
|
const isAuth = location.pathname === Path.Auth;
|
||||||
|
const isSd = location.pathname === Path.Sd;
|
||||||
|
const isSdNew = location.pathname === Path.SdNew;
|
||||||
|
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const shouldTightBorder =
|
const shouldTightBorder =
|
||||||
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||||
@@ -135,34 +155,40 @@ function Screen() {
|
|||||||
loadAsyncGoogleFont();
|
loadAsyncGoogleFont();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (isArtifact) {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/artifacts/:id" element={<Artifacts />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isAuth) return <AuthPage />;
|
||||||
|
if (isSd) return <Sd />;
|
||||||
|
if (isSdNew) return <Sd />;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||||
|
<WindowContent>
|
||||||
|
<Routes>
|
||||||
|
<Route path={Path.Home} element={<Chat />} />
|
||||||
|
<Route path={Path.NewChat} element={<NewChat />} />
|
||||||
|
<Route path={Path.Masks} element={<MaskPage />} />
|
||||||
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
|
</Routes>
|
||||||
|
</WindowContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={`${styles.container} ${
|
||||||
styles.container +
|
shouldTightBorder ? styles["tight-container"] : styles.container
|
||||||
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
|
||||||
getLang() === "ar" ? styles["rtl-screen"] : ""
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{isAuth ? (
|
{renderContent()}
|
||||||
<>
|
|
||||||
<AuthPage />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
|
||||||
|
|
||||||
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
|
||||||
<Routes>
|
|
||||||
<Route path={Path.Home} element={<Chat />} />
|
|
||||||
<Route path={Path.NewChat} element={<NewChat />} />
|
|
||||||
<Route path={Path.Masks} element={<MaskPage />} />
|
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -6,14 +6,16 @@ import RehypeKatex from "rehype-katex";
|
|||||||
import RemarkGfm from "remark-gfm";
|
import RemarkGfm from "remark-gfm";
|
||||||
import RehypeHighlight from "rehype-highlight";
|
import RehypeHighlight from "rehype-highlight";
|
||||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard, useWindowSize } from "../utils";
|
||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
|
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { showImageModal } from "./ui-lib";
|
import { showImageModal, FullScreen } from "./ui-lib";
|
||||||
|
import { ArtifactsShareButton, HTMLPreview } from "./artifacts";
|
||||||
|
import { Plugin } from "../constant";
|
||||||
|
import { useChatStore } from "../store";
|
||||||
export function Mermaid(props: { code: string }) {
|
export function Mermaid(props: { code: string }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
@@ -64,25 +66,64 @@ export function PreCode(props: { children: any }) {
|
|||||||
const ref = useRef<HTMLPreElement>(null);
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
const refText = ref.current?.innerText;
|
const refText = ref.current?.innerText;
|
||||||
const [mermaidCode, setMermaidCode] = useState("");
|
const [mermaidCode, setMermaidCode] = useState("");
|
||||||
|
const [htmlCode, setHtmlCode] = useState("");
|
||||||
|
const { height } = useWindowSize();
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const plugins = session.mask?.plugin;
|
||||||
|
|
||||||
const renderMermaid = useDebouncedCallback(() => {
|
const renderArtifacts = useDebouncedCallback(() => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
||||||
if (mermaidDom) {
|
if (mermaidDom) {
|
||||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||||
}
|
}
|
||||||
|
const htmlDom = ref.current.querySelector("code.language-html");
|
||||||
|
if (htmlDom) {
|
||||||
|
setHtmlCode((htmlDom as HTMLElement).innerText);
|
||||||
|
} else if (refText?.startsWith("<!DOCTYPE")) {
|
||||||
|
setHtmlCode(refText);
|
||||||
|
}
|
||||||
}, 600);
|
}, 600);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(renderMermaid, 1);
|
setTimeout(renderArtifacts, 1);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [refText]);
|
}, [refText]);
|
||||||
|
|
||||||
|
const enableArtifacts = useMemo(
|
||||||
|
() => plugins?.includes(Plugin.Artifacts),
|
||||||
|
[plugins],
|
||||||
|
);
|
||||||
|
|
||||||
|
//Wrap the paragraph for plain-text
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
const codeElements = ref.current.querySelectorAll(
|
||||||
|
"code",
|
||||||
|
) as NodeListOf<HTMLElement>;
|
||||||
|
const wrapLanguages = [
|
||||||
|
"",
|
||||||
|
"md",
|
||||||
|
"markdown",
|
||||||
|
"text",
|
||||||
|
"txt",
|
||||||
|
"plaintext",
|
||||||
|
"tex",
|
||||||
|
"latex",
|
||||||
|
];
|
||||||
|
codeElements.forEach((codeElement) => {
|
||||||
|
let languageClass = codeElement.className.match(/language-(\w+)/);
|
||||||
|
let name = languageClass ? languageClass[1] : "";
|
||||||
|
if (wrapLanguages.includes(name)) {
|
||||||
|
codeElement.style.whiteSpace = "pre-wrap";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{mermaidCode.length > 0 && (
|
|
||||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
|
||||||
)}
|
|
||||||
<pre ref={ref}>
|
<pre ref={ref}>
|
||||||
<span
|
<span
|
||||||
className="copy-code-button"
|
className="copy-code-button"
|
||||||
@@ -95,6 +136,22 @@ export function PreCode(props: { children: any }) {
|
|||||||
></span>
|
></span>
|
||||||
{props.children}
|
{props.children}
|
||||||
</pre>
|
</pre>
|
||||||
|
{mermaidCode.length > 0 && (
|
||||||
|
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||||
|
)}
|
||||||
|
{htmlCode.length > 0 && enableArtifacts && (
|
||||||
|
<FullScreen className="no-dark html" right={70}>
|
||||||
|
<ArtifactsShareButton
|
||||||
|
style={{ position: "absolute", right: 20, top: 10 }}
|
||||||
|
getCode={() => htmlCode}
|
||||||
|
/>
|
||||||
|
<HTMLPreview
|
||||||
|
code={htmlCode}
|
||||||
|
autoHeight={!document.fullscreenElement}
|
||||||
|
height={!document.fullscreenElement ? 600 : height}
|
||||||
|
/>
|
||||||
|
</FullScreen>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -175,6 +232,7 @@ export function Markdown(
|
|||||||
content: string;
|
content: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
fontFamily?: string;
|
||||||
parentRef?: RefObject<HTMLDivElement>;
|
parentRef?: RefObject<HTMLDivElement>;
|
||||||
defaultShow?: boolean;
|
defaultShow?: boolean;
|
||||||
} & React.DOMAttributes<HTMLDivElement>,
|
} & React.DOMAttributes<HTMLDivElement>,
|
||||||
@@ -186,6 +244,7 @@ export function Markdown(
|
|||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
style={{
|
style={{
|
||||||
fontSize: `${props.fontSize ?? 14}px`,
|
fontSize: `${props.fontSize ?? 14}px`,
|
||||||
|
fontFamily: props.fontFamily || "inherit",
|
||||||
}}
|
}}
|
||||||
ref={mdRef}
|
ref={mdRef}
|
||||||
onContextMenu={props.onContextMenu}
|
onContextMenu={props.onContextMenu}
|
||||||
|
2
app/components/sd/index.tsx
Normal file
2
app/components/sd/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./sd";
|
||||||
|
export * from "./sd-panel";
|
45
app/components/sd/sd-panel.module.scss
Normal file
45
app/components/sd/sd-panel.module.scss
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
.ctrl-param-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 10px 0;
|
||||||
|
animation: slide-in ease 0.6s;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ctrl-param-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ctrl-param-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bolder;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl-param-item-sub-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
min-height: 36px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
padding: 0 10px;
|
||||||
|
max-width: 50%;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-models {
|
||||||
|
button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
317
app/components/sd/sd-panel.tsx
Normal file
317
app/components/sd/sd-panel.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import styles from "./sd-panel.module.scss";
|
||||||
|
import React from "react";
|
||||||
|
import { Select } from "@/app/components/ui-lib";
|
||||||
|
import { IconButton } from "@/app/components/button";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import { useSdStore } from "@/app/store/sd";
|
||||||
|
|
||||||
|
export const params = [
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.Prompt,
|
||||||
|
value: "prompt",
|
||||||
|
type: "textarea",
|
||||||
|
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.ModelVersion,
|
||||||
|
value: "model",
|
||||||
|
type: "select",
|
||||||
|
default: "sd3-medium",
|
||||||
|
support: ["sd3"],
|
||||||
|
options: [
|
||||||
|
{ name: "SD3 Medium", value: "sd3-medium" },
|
||||||
|
{ name: "SD3 Large", value: "sd3-large" },
|
||||||
|
{ name: "SD3 Large Turbo", value: "sd3-large-turbo" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.NegativePrompt,
|
||||||
|
value: "negative_prompt",
|
||||||
|
type: "textarea",
|
||||||
|
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.AspectRatio,
|
||||||
|
value: "aspect_ratio",
|
||||||
|
type: "select",
|
||||||
|
default: "1:1",
|
||||||
|
options: [
|
||||||
|
{ name: "1:1", value: "1:1" },
|
||||||
|
{ name: "16:9", value: "16:9" },
|
||||||
|
{ name: "21:9", value: "21:9" },
|
||||||
|
{ name: "2:3", value: "2:3" },
|
||||||
|
{ name: "3:2", value: "3:2" },
|
||||||
|
{ name: "4:5", value: "4:5" },
|
||||||
|
{ name: "5:4", value: "5:4" },
|
||||||
|
{ name: "9:16", value: "9:16" },
|
||||||
|
{ name: "9:21", value: "9:21" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.ImageStyle,
|
||||||
|
value: "style",
|
||||||
|
type: "select",
|
||||||
|
default: "3d-model",
|
||||||
|
support: ["core"],
|
||||||
|
options: [
|
||||||
|
{ name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
|
||||||
|
{ name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
|
||||||
|
{ name: Locale.SdPanel.Styles.Anime, value: "anime" },
|
||||||
|
{ name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
|
||||||
|
{ name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
|
||||||
|
{ name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
|
||||||
|
{ name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
|
||||||
|
{ name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
|
||||||
|
{ name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
|
||||||
|
{ name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
|
||||||
|
{ name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.Styles.ModelingCompound,
|
||||||
|
value: "modeling-compound",
|
||||||
|
},
|
||||||
|
{ name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
|
||||||
|
{ name: Locale.SdPanel.Styles.Origami, value: "origami" },
|
||||||
|
{ name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
|
||||||
|
{ name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
|
||||||
|
{ name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Seed",
|
||||||
|
value: "seed",
|
||||||
|
type: "number",
|
||||||
|
default: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 4294967294,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: Locale.SdPanel.OutFormat,
|
||||||
|
value: "output_format",
|
||||||
|
type: "select",
|
||||||
|
default: "png",
|
||||||
|
options: [
|
||||||
|
{ name: "PNG", value: "png" },
|
||||||
|
{ name: "JPEG", value: "jpeg" },
|
||||||
|
{ name: "WebP", value: "webp" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sdCommonParams = (model: string, data: any) => {
|
||||||
|
return params.filter((item) => {
|
||||||
|
return !(item.support && !item.support.includes(model));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const models = [
|
||||||
|
{
|
||||||
|
name: "Stable Image Ultra",
|
||||||
|
value: "ultra",
|
||||||
|
params: (data: any) => sdCommonParams("ultra", data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Stable Image Core",
|
||||||
|
value: "core",
|
||||||
|
params: (data: any) => sdCommonParams("core", data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Stable Diffusion 3",
|
||||||
|
value: "sd3",
|
||||||
|
params: (data: any) => {
|
||||||
|
return sdCommonParams("sd3", data).filter((item) => {
|
||||||
|
return !(
|
||||||
|
data.model === "sd3-large-turbo" && item.value == "negative_prompt"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ControlParamItem(props: {
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
required?: boolean;
|
||||||
|
children?: JSX.Element | JSX.Element[];
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
|
||||||
|
<div className={styles["ctrl-param-item-header"]}>
|
||||||
|
<div className={styles["ctrl-param-item-title"]}>
|
||||||
|
<div>
|
||||||
|
{props.title}
|
||||||
|
{props.required && <span style={{ color: "red" }}>*</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{props.children}
|
||||||
|
{props.subTitle && (
|
||||||
|
<div className={styles["ctrl-param-item-sub-title"]}>
|
||||||
|
{props.subTitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ControlParam(props: {
|
||||||
|
columns: any[];
|
||||||
|
data: any;
|
||||||
|
onChange: (field: string, val: any) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.columns?.map((item) => {
|
||||||
|
let element: null | JSX.Element;
|
||||||
|
switch (item.type) {
|
||||||
|
case "textarea":
|
||||||
|
element = (
|
||||||
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
rows={item.rows || 3}
|
||||||
|
style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
|
||||||
|
placeholder={item.placeholder}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.onChange(item.value, e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
value={props.data[item.value]}
|
||||||
|
></textarea>
|
||||||
|
</ControlParamItem>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "select":
|
||||||
|
element = (
|
||||||
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={props.data[item.value]}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.onChange(item.value, e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.options.map((opt: any) => {
|
||||||
|
return (
|
||||||
|
<option value={opt.value} key={opt.value}>
|
||||||
|
{opt.name}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</ControlParamItem>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
element = (
|
||||||
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={item.min}
|
||||||
|
max={item.max}
|
||||||
|
value={props.data[item.value] || 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.onChange(item.value, parseInt(e.currentTarget.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ControlParamItem>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
element = (
|
||||||
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={props.data[item.value]}
|
||||||
|
style={{ maxWidth: "100%", width: "100%" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.onChange(item.value, e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ControlParamItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div key={item.value}>{element}</div>;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getModelParamBasicData = (
|
||||||
|
columns: any[],
|
||||||
|
data: any,
|
||||||
|
clearText?: boolean,
|
||||||
|
) => {
|
||||||
|
const newParams: any = {};
|
||||||
|
columns.forEach((item: any) => {
|
||||||
|
if (clearText && ["text", "textarea", "number"].includes(item.type)) {
|
||||||
|
newParams[item.value] = item.default || "";
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
newParams[item.value] = data[item.value] || item.default || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getParams = (model: any, params: any) => {
|
||||||
|
return models.find((m) => m.value === model.value)?.params(params) || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SdPanel() {
|
||||||
|
const sdStore = useSdStore();
|
||||||
|
const currentModel = sdStore.currentModel;
|
||||||
|
const setCurrentModel = sdStore.setCurrentModel;
|
||||||
|
const params = sdStore.currentParams;
|
||||||
|
const setParams = sdStore.setCurrentParams;
|
||||||
|
|
||||||
|
const handleValueChange = (field: string, val: any) => {
|
||||||
|
setParams({
|
||||||
|
...params,
|
||||||
|
[field]: val,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleModelChange = (model: any) => {
|
||||||
|
setCurrentModel(model);
|
||||||
|
setParams(getModelParamBasicData(model.params({}), params));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ControlParamItem title={Locale.SdPanel.AIModel}>
|
||||||
|
<div className={styles["ai-models"]}>
|
||||||
|
{models.map((item) => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
text={item.name}
|
||||||
|
key={item.value}
|
||||||
|
type={currentModel.value == item.value ? "primary" : null}
|
||||||
|
shadow
|
||||||
|
onClick={() => handleModelChange(item)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ControlParamItem>
|
||||||
|
<ControlParam
|
||||||
|
columns={getParams?.(currentModel, params) as any[]}
|
||||||
|
data={params}
|
||||||
|
onChange={handleValueChange}
|
||||||
|
></ControlParam>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
140
app/components/sd/sd-sidebar.tsx
Normal file
140
app/components/sd/sd-sidebar.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { IconButton } from "@/app/components/button";
|
||||||
|
import GithubIcon from "@/app/icons/github.svg";
|
||||||
|
import SDIcon from "@/app/icons/sd.svg";
|
||||||
|
import ReturnIcon from "@/app/icons/return.svg";
|
||||||
|
import HistoryIcon from "@/app/icons/history.svg";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
|
||||||
|
import { Path, REPO_URL } from "@/app/constant";
|
||||||
|
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import {
|
||||||
|
SideBarContainer,
|
||||||
|
SideBarBody,
|
||||||
|
SideBarHeader,
|
||||||
|
SideBarTail,
|
||||||
|
useDragSideBar,
|
||||||
|
useHotKey,
|
||||||
|
} from "@/app/components/sidebar";
|
||||||
|
|
||||||
|
import { getParams, getModelParamBasicData } from "./sd-panel";
|
||||||
|
import { useSdStore } from "@/app/store/sd";
|
||||||
|
import { showToast } from "@/app/components/ui-lib";
|
||||||
|
import { useMobileScreen } from "@/app/utils";
|
||||||
|
|
||||||
|
const SdPanel = dynamic(
|
||||||
|
async () => (await import("@/app/components/sd")).SdPanel,
|
||||||
|
{
|
||||||
|
loading: () => null,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export function SideBar(props: { className?: string }) {
|
||||||
|
useHotKey();
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const sdStore = useSdStore();
|
||||||
|
const currentModel = sdStore.currentModel;
|
||||||
|
const params = sdStore.currentParams;
|
||||||
|
const setParams = sdStore.setCurrentParams;
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const columns = getParams?.(currentModel, params);
|
||||||
|
const reqParams: any = {};
|
||||||
|
for (let i = 0; i < columns.length; i++) {
|
||||||
|
const item = columns[i];
|
||||||
|
reqParams[item.value] = params[item.value] ?? null;
|
||||||
|
if (item.required) {
|
||||||
|
if (!reqParams[item.value]) {
|
||||||
|
showToast(Locale.SdPanel.ParamIsRequired(item.name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let data: any = {
|
||||||
|
model: currentModel.value,
|
||||||
|
model_name: currentModel.name,
|
||||||
|
status: "wait",
|
||||||
|
params: reqParams,
|
||||||
|
created_at: new Date().toLocaleString(),
|
||||||
|
img_data: "",
|
||||||
|
};
|
||||||
|
sdStore.sendTask(data, () => {
|
||||||
|
setParams(getModelParamBasicData(columns, params, true));
|
||||||
|
navigate(Path.SdNew);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SideBarContainer
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
shouldNarrow={shouldNarrow}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{isMobileScreen ? (
|
||||||
|
<div
|
||||||
|
className="window-header"
|
||||||
|
data-tauri-drag-region
|
||||||
|
style={{
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<ReturnIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Sd.Actions.ReturnHome}
|
||||||
|
onClick={() => navigate(Path.Home)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SDIcon width={50} height={50} />
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<HistoryIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Sd.Actions.History}
|
||||||
|
onClick={() => navigate(Path.SdNew)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SideBarHeader
|
||||||
|
title={
|
||||||
|
<IconButton
|
||||||
|
icon={<ReturnIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Sd.Actions.ReturnHome}
|
||||||
|
onClick={() => navigate(Path.Home)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
logo={<SDIcon width={38} height={"100%"} />}
|
||||||
|
></SideBarHeader>
|
||||||
|
)}
|
||||||
|
<SideBarBody>
|
||||||
|
<SdPanel />
|
||||||
|
</SideBarBody>
|
||||||
|
<SideBarTail
|
||||||
|
primaryAction={
|
||||||
|
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||||
|
<IconButton icon={<GithubIcon />} shadow />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
secondaryAction={
|
||||||
|
<IconButton
|
||||||
|
text={Locale.SdPanel.Submit}
|
||||||
|
type="primary"
|
||||||
|
shadow
|
||||||
|
onClick={handleSubmit}
|
||||||
|
></IconButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SideBarContainer>
|
||||||
|
);
|
||||||
|
}
|
53
app/components/sd/sd.module.scss
Normal file
53
app/components/sd/sd.module.scss
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
.sd-img-list{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
.sd-img-item{
|
||||||
|
width: 48%;
|
||||||
|
.sd-img-item-info{
|
||||||
|
flex:1;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
user-select: text;
|
||||||
|
p{
|
||||||
|
margin: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.line-1{
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pre-img{
|
||||||
|
display: flex;
|
||||||
|
width: 130px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--second);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.img{
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .3s;
|
||||||
|
&:hover{
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:not(:last-child){
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.sd-img-list{
|
||||||
|
.sd-img-item{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
335
app/components/sd/sd.tsx
Normal file
335
app/components/sd/sd.tsx
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
import chatStyles from "@/app/components/chat.module.scss";
|
||||||
|
import styles from "@/app/components/sd/sd.module.scss";
|
||||||
|
import homeStyles from "@/app/components/home.module.scss";
|
||||||
|
|
||||||
|
import { IconButton } from "@/app/components/button";
|
||||||
|
import ReturnIcon from "@/app/icons/return.svg";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import { Path } from "@/app/constant";
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
getMessageTextContent,
|
||||||
|
useMobileScreen,
|
||||||
|
} from "@/app/utils";
|
||||||
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { useAppConfig } from "@/app/store";
|
||||||
|
import MinIcon from "@/app/icons/min.svg";
|
||||||
|
import MaxIcon from "@/app/icons/max.svg";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { ChatAction } from "@/app/components/chat";
|
||||||
|
import DeleteIcon from "@/app/icons/clear.svg";
|
||||||
|
import CopyIcon from "@/app/icons/copy.svg";
|
||||||
|
import PromptIcon from "@/app/icons/prompt.svg";
|
||||||
|
import ResetIcon from "@/app/icons/reload.svg";
|
||||||
|
import { useSdStore } from "@/app/store/sd";
|
||||||
|
import LoadingIcon from "@/app/icons/three-dots.svg";
|
||||||
|
import ErrorIcon from "@/app/icons/delete.svg";
|
||||||
|
import SDIcon from "@/app/icons/sd.svg";
|
||||||
|
import { Property } from "csstype";
|
||||||
|
import {
|
||||||
|
showConfirm,
|
||||||
|
showImageModal,
|
||||||
|
showModal,
|
||||||
|
} from "@/app/components/ui-lib";
|
||||||
|
import { removeImage } from "@/app/utils/chat";
|
||||||
|
import { SideBar } from "./sd-sidebar";
|
||||||
|
import { WindowContent } from "@/app/components/home";
|
||||||
|
import { params } from "./sd-panel";
|
||||||
|
|
||||||
|
function getSdTaskStatus(item: any) {
|
||||||
|
let s: string;
|
||||||
|
let color: Property.Color | undefined = undefined;
|
||||||
|
switch (item.status) {
|
||||||
|
case "success":
|
||||||
|
s = Locale.Sd.Status.Success;
|
||||||
|
color = "green";
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
s = Locale.Sd.Status.Error;
|
||||||
|
color = "red";
|
||||||
|
break;
|
||||||
|
case "wait":
|
||||||
|
s = Locale.Sd.Status.Wait;
|
||||||
|
color = "yellow";
|
||||||
|
break;
|
||||||
|
case "running":
|
||||||
|
s = Locale.Sd.Status.Running;
|
||||||
|
color = "blue";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
s = item.status.toUpperCase();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<p className={styles["line-1"]} title={item.error} style={{ color: color }}>
|
||||||
|
<span>
|
||||||
|
{Locale.Sd.Status.Name}: {s}
|
||||||
|
</span>
|
||||||
|
{item.status === "error" && (
|
||||||
|
<span
|
||||||
|
className="clickable"
|
||||||
|
onClick={() => {
|
||||||
|
showModal({
|
||||||
|
title: Locale.Sd.Detail,
|
||||||
|
children: (
|
||||||
|
<div style={{ color: color, userSelect: "text" }}>
|
||||||
|
{item.error}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
- {item.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sd() {
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const clientConfig = useMemo(() => getClientConfig(), []);
|
||||||
|
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
|
||||||
|
const config = useAppConfig();
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sdStore = useSdStore();
|
||||||
|
const [sdImages, setSdImages] = useState(sdStore.draw);
|
||||||
|
const isSd = location.pathname === Path.Sd;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSdImages(sdStore.draw);
|
||||||
|
}, [sdStore.currentId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SideBar className={isSd ? homeStyles["sidebar-show"] : ""} />
|
||||||
|
<WindowContent>
|
||||||
|
<div className={chatStyles.chat} key={"1"}>
|
||||||
|
<div className="window-header" data-tauri-drag-region>
|
||||||
|
{isMobileScreen && (
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className={"window-action-button"}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ReturnIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.ChatList}
|
||||||
|
onClick={() => navigate(Path.Sd)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`window-header-title ${chatStyles["chat-body-title"]}`}
|
||||||
|
>
|
||||||
|
<div className={`window-header-main-title`}>Stability AI</div>
|
||||||
|
<div className="window-header-sub-title">
|
||||||
|
{Locale.Sd.SubTitle(sdImages.length || 0)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window-actions">
|
||||||
|
{showMaxIcon && (
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={() => {
|
||||||
|
config.update(
|
||||||
|
(config) => (config.tightBorder = !config.tightBorder),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isMobileScreen && <SDIcon width={50} height={50} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={chatStyles["chat-body"]} ref={scrollRef}>
|
||||||
|
<div className={styles["sd-img-list"]}>
|
||||||
|
{sdImages.length > 0 ? (
|
||||||
|
sdImages.map((item: any) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
style={{ display: "flex" }}
|
||||||
|
className={styles["sd-img-item"]}
|
||||||
|
>
|
||||||
|
{item.status === "success" ? (
|
||||||
|
<img
|
||||||
|
className={styles["img"]}
|
||||||
|
src={item.img_data}
|
||||||
|
alt={item.id}
|
||||||
|
onClick={(e) =>
|
||||||
|
showImageModal(
|
||||||
|
item.img_data,
|
||||||
|
true,
|
||||||
|
isMobileScreen
|
||||||
|
? { width: "100%", height: "fit-content" }
|
||||||
|
: { maxWidth: "100%", maxHeight: "100%" },
|
||||||
|
isMobileScreen
|
||||||
|
? { width: "100%", height: "fit-content" }
|
||||||
|
: { width: "100%", height: "100%" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : item.status === "error" ? (
|
||||||
|
<div className={styles["pre-img"]}>
|
||||||
|
<ErrorIcon />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles["pre-img"]}>
|
||||||
|
<LoadingIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{ marginLeft: "10px" }}
|
||||||
|
className={styles["sd-img-item-info"]}
|
||||||
|
>
|
||||||
|
<p className={styles["line-1"]}>
|
||||||
|
{Locale.SdPanel.Prompt}:{" "}
|
||||||
|
<span
|
||||||
|
className="clickable"
|
||||||
|
title={item.params.prompt}
|
||||||
|
onClick={() => {
|
||||||
|
showModal({
|
||||||
|
title: Locale.Sd.Detail,
|
||||||
|
children: (
|
||||||
|
<div style={{ userSelect: "text" }}>
|
||||||
|
{item.params.prompt}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.params.prompt}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{Locale.SdPanel.AIModel}: {item.model_name}
|
||||||
|
</p>
|
||||||
|
{getSdTaskStatus(item)}
|
||||||
|
<p>{item.created_at}</p>
|
||||||
|
<div className={chatStyles["chat-message-actions"]}>
|
||||||
|
<div className={chatStyles["chat-input-actions"]}>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Sd.Actions.Params}
|
||||||
|
icon={<PromptIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
showModal({
|
||||||
|
title: Locale.Sd.GenerateParams,
|
||||||
|
children: (
|
||||||
|
<div style={{ userSelect: "text" }}>
|
||||||
|
{Object.keys(item.params).map((key) => {
|
||||||
|
let label = key;
|
||||||
|
let value = item.params[key];
|
||||||
|
switch (label) {
|
||||||
|
case "prompt":
|
||||||
|
label = Locale.SdPanel.Prompt;
|
||||||
|
break;
|
||||||
|
case "negative_prompt":
|
||||||
|
label =
|
||||||
|
Locale.SdPanel.NegativePrompt;
|
||||||
|
break;
|
||||||
|
case "aspect_ratio":
|
||||||
|
label = Locale.SdPanel.AspectRatio;
|
||||||
|
break;
|
||||||
|
case "seed":
|
||||||
|
label = "Seed";
|
||||||
|
value = value || 0;
|
||||||
|
break;
|
||||||
|
case "output_format":
|
||||||
|
label = Locale.SdPanel.OutFormat;
|
||||||
|
value = value?.toUpperCase();
|
||||||
|
break;
|
||||||
|
case "style":
|
||||||
|
label = Locale.SdPanel.ImageStyle;
|
||||||
|
value = params
|
||||||
|
.find(
|
||||||
|
(item) =>
|
||||||
|
item.value === "style",
|
||||||
|
)
|
||||||
|
?.options?.find(
|
||||||
|
(item) => item.value === value,
|
||||||
|
)?.name;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
style={{ margin: "10px" }}
|
||||||
|
>
|
||||||
|
<strong>{label}: </strong>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Sd.Actions.Copy}
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
copyToClipboard(
|
||||||
|
getMessageTextContent({
|
||||||
|
role: "user",
|
||||||
|
content: item.params.prompt,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Sd.Actions.Retry}
|
||||||
|
icon={<ResetIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
const reqData = {
|
||||||
|
model: item.model,
|
||||||
|
model_name: item.model_name,
|
||||||
|
status: "wait",
|
||||||
|
params: { ...item.params },
|
||||||
|
created_at: new Date().toLocaleString(),
|
||||||
|
img_data: "",
|
||||||
|
};
|
||||||
|
sdStore.sendTask(reqData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Sd.Actions.Delete}
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
onClick={async () => {
|
||||||
|
if (
|
||||||
|
await showConfirm(Locale.Sd.Danger.Delete)
|
||||||
|
) {
|
||||||
|
// remove img_data + remove item in list
|
||||||
|
removeImage(item.img_data).finally(() => {
|
||||||
|
sdStore.draw = sdImages.filter(
|
||||||
|
(i: any) => i.id !== item.id,
|
||||||
|
);
|
||||||
|
sdStore.getNextId();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div>{Locale.Sd.EmptyRecord}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WindowContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useMemo } from "react";
|
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
|
||||||
|
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ import AddIcon from "../icons/add.svg";
|
|||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import MaskIcon from "../icons/mask.svg";
|
import MaskIcon from "../icons/mask.svg";
|
||||||
import PluginIcon from "../icons/plugin.svg";
|
|
||||||
import DragIcon from "../icons/drag.svg";
|
import DragIcon from "../icons/drag.svg";
|
||||||
|
import DiscoveryIcon from "../icons/discovery.svg";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
@@ -23,19 +23,20 @@ import {
|
|||||||
MIN_SIDEBAR_WIDTH,
|
MIN_SIDEBAR_WIDTH,
|
||||||
NARROW_SIDEBAR_WIDTH,
|
NARROW_SIDEBAR_WIDTH,
|
||||||
Path,
|
Path,
|
||||||
|
PLUGINS,
|
||||||
REPO_URL,
|
REPO_URL,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { isIOS, useMobileScreen } from "../utils";
|
import { isIOS, useMobileScreen } from "../utils";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { showConfirm, showToast } from "./ui-lib";
|
import { showConfirm, Selector } from "./ui-lib";
|
||||||
|
|
||||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||||
loading: () => null,
|
loading: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useHotKey() {
|
export function useHotKey() {
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -54,7 +55,7 @@ function useHotKey() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDragSideBar() {
|
export function useDragSideBar() {
|
||||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
||||||
|
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
@@ -127,25 +128,21 @@ function useDragSideBar() {
|
|||||||
shouldNarrow,
|
shouldNarrow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function SideBarContainer(props: {
|
||||||
export function SideBar(props: { className?: string }) {
|
children: React.ReactNode;
|
||||||
const chatStore = useChatStore();
|
onDragStart: (e: MouseEvent) => void;
|
||||||
|
shouldNarrow: boolean;
|
||||||
// drag side bar
|
className?: string;
|
||||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
}) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const config = useAppConfig();
|
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const isIOSMobile = useMemo(
|
const isIOSMobile = useMemo(
|
||||||
() => isIOS() && isMobileScreen,
|
() => isIOS() && isMobileScreen,
|
||||||
[isMobileScreen],
|
[isMobileScreen],
|
||||||
);
|
);
|
||||||
|
const { children, className, onDragStart, shouldNarrow } = props;
|
||||||
useHotKey();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.sidebar} ${props.className} ${
|
className={`${styles.sidebar} ${className} ${
|
||||||
shouldNarrow && styles["narrow-sidebar"]
|
shouldNarrow && styles["narrow-sidebar"]
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
@@ -153,43 +150,130 @@ export function SideBar(props: { className?: string }) {
|
|||||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
{children}
|
||||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
|
||||||
NextChat
|
|
||||||
</div>
|
|
||||||
<div className={styles["sidebar-sub-title"]}>
|
|
||||||
Build your own AI assistant.
|
|
||||||
</div>
|
|
||||||
<div className={styles["sidebar-logo"] + " no-dark"}>
|
|
||||||
<ChatGptIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles["sidebar-header-bar"]}>
|
|
||||||
<IconButton
|
|
||||||
icon={<MaskIcon />}
|
|
||||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
|
||||||
className={styles["sidebar-bar-button"]}
|
|
||||||
onClick={() => {
|
|
||||||
if (config.dontShowMaskSplashScreen !== true) {
|
|
||||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
|
||||||
} else {
|
|
||||||
navigate(Path.Masks, { state: { fromHome: true } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
shadow
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={<PluginIcon />}
|
|
||||||
text={shouldNarrow ? undefined : Locale.Plugin.Name}
|
|
||||||
className={styles["sidebar-bar-button"]}
|
|
||||||
onClick={() => showToast(Locale.WIP)}
|
|
||||||
shadow
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles["sidebar-body"]}
|
className={styles["sidebar-drag"]}
|
||||||
|
onPointerDown={(e) => onDragStart(e as any)}
|
||||||
|
>
|
||||||
|
<DragIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SideBarHeader(props: {
|
||||||
|
title?: string | React.ReactNode;
|
||||||
|
subTitle?: string | React.ReactNode;
|
||||||
|
logo?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { title, subTitle, logo, children } = props;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||||
|
<div className={styles["sidebar-title-container"]}>
|
||||||
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SideBarBody(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
|
}) {
|
||||||
|
const { onClick, children } = props;
|
||||||
|
return (
|
||||||
|
<div className={styles["sidebar-body"]} onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SideBarTail(props: {
|
||||||
|
primaryAction?: React.ReactNode;
|
||||||
|
secondaryAction?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const { primaryAction, secondaryAction } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["sidebar-tail"]}>
|
||||||
|
<div className={styles["sidebar-actions"]}>{primaryAction}</div>
|
||||||
|
<div className={styles["sidebar-actions"]}>{secondaryAction}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SideBar(props: { className?: string }) {
|
||||||
|
useHotKey();
|
||||||
|
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||||
|
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const config = useAppConfig();
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SideBarContainer
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
shouldNarrow={shouldNarrow}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SideBarHeader
|
||||||
|
title="NextChat"
|
||||||
|
subTitle="Build your own AI assistant."
|
||||||
|
logo={<ChatGptIcon />}
|
||||||
|
>
|
||||||
|
<div className={styles["sidebar-header-bar"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<MaskIcon />}
|
||||||
|
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||||
|
className={styles["sidebar-bar-button"]}
|
||||||
|
onClick={() => {
|
||||||
|
if (config.dontShowMaskSplashScreen !== true) {
|
||||||
|
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||||
|
} else {
|
||||||
|
navigate(Path.Masks, { state: { fromHome: true } });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
shadow
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DiscoveryIcon />}
|
||||||
|
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
||||||
|
className={styles["sidebar-bar-button"]}
|
||||||
|
onClick={() => setShowPluginSelector(true)}
|
||||||
|
shadow
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showPluginSelector && (
|
||||||
|
<Selector
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: "👇 Please select the plugin you need to use",
|
||||||
|
value: "-",
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
...PLUGINS.map((item) => {
|
||||||
|
return {
|
||||||
|
title: item.name,
|
||||||
|
value: item.path,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
onClose={() => setShowPluginSelector(false)}
|
||||||
|
onSelection={(s) => {
|
||||||
|
navigate(s[0], { state: { fromHome: true } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SideBarHeader>
|
||||||
|
<SideBarBody
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
navigate(Path.Home);
|
navigate(Path.Home);
|
||||||
@@ -197,32 +281,33 @@ export function SideBar(props: { className?: string }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatList narrow={shouldNarrow} />
|
<ChatList narrow={shouldNarrow} />
|
||||||
</div>
|
</SideBarBody>
|
||||||
|
<SideBarTail
|
||||||
<div className={styles["sidebar-tail"]}>
|
primaryAction={
|
||||||
<div className={styles["sidebar-actions"]}>
|
<>
|
||||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["sidebar-action"]}>
|
<div className={styles["sidebar-action"]}>
|
||||||
<Link to={Path.Settings}>
|
<Link to={Path.Settings}>
|
||||||
<IconButton icon={<SettingsIcon />} shadow />
|
<IconButton icon={<SettingsIcon />} shadow />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["sidebar-action"]}>
|
<div className={styles["sidebar-action"]}>
|
||||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||||
<IconButton icon={<GithubIcon />} shadow />
|
<IconButton icon={<GithubIcon />} shadow />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
<div>
|
}
|
||||||
|
secondaryAction={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
||||||
@@ -236,15 +321,8 @@ export function SideBar(props: { className?: string }) {
|
|||||||
}}
|
}}
|
||||||
shadow
|
shadow
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
</SideBarContainer>
|
||||||
<div
|
|
||||||
className={styles["sidebar-drag"]}
|
|
||||||
onPointerDown={(e) => onDragStart(e as any)}
|
|
||||||
>
|
|
||||||
<DragIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -61,6 +61,19 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.vertical{
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
.list-header{
|
||||||
|
.list-item-title{
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.list-item-sub-title{
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
@@ -291,7 +304,12 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
|
.selector-item-disabled{
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
|
min-width: 300px;
|
||||||
.list {
|
.list {
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
@@ -13,7 +13,15 @@ import MinIcon from "../icons/min.svg";
|
|||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import React, { HTMLProps, useEffect, useState } from "react";
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
HTMLProps,
|
||||||
|
MouseEvent,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
|
|
||||||
export function Popover(props: {
|
export function Popover(props: {
|
||||||
@@ -47,11 +55,16 @@ export function ListItem(props: {
|
|||||||
children?: JSX.Element | JSX.Element[];
|
children?: JSX.Element | JSX.Element[];
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: () => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
vertical?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles["list-item"] + ` ${props.className || ""}`}
|
className={
|
||||||
|
styles["list-item"] +
|
||||||
|
` ${props.vertical ? styles["vertical"] : ""} ` +
|
||||||
|
` ${props.className || ""}`
|
||||||
|
}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<div className={styles["list-header"]}>
|
<div className={styles["list-header"]}>
|
||||||
@@ -420,17 +433,25 @@ export function showPrompt(content: any, value = "", rows = 3) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showImageModal(img: string) {
|
export function showImageModal(
|
||||||
|
img: string,
|
||||||
|
defaultMax?: boolean,
|
||||||
|
style?: CSSProperties,
|
||||||
|
boxStyle?: CSSProperties,
|
||||||
|
) {
|
||||||
showModal({
|
showModal({
|
||||||
title: Locale.Export.Image.Modal,
|
title: Locale.Export.Image.Modal,
|
||||||
|
defaultMax: defaultMax,
|
||||||
children: (
|
children: (
|
||||||
<div>
|
<div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
|
||||||
<img
|
<img
|
||||||
src={img}
|
src={img}
|
||||||
alt="preview"
|
alt="preview"
|
||||||
style={{
|
style={
|
||||||
maxWidth: "100%",
|
style ?? {
|
||||||
}}
|
maxWidth: "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
></img>
|
></img>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -442,27 +463,56 @@ export function Selector<T>(props: {
|
|||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
value: T;
|
value: T;
|
||||||
|
disable?: boolean;
|
||||||
}>;
|
}>;
|
||||||
defaultSelectedValue?: T;
|
defaultSelectedValue?: T[] | T;
|
||||||
onSelection?: (selection: T[]) => void;
|
onSelection?: (selection: T[]) => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const [selectedValues, setSelectedValues] = useState<T[]>(
|
||||||
|
Array.isArray(props.defaultSelectedValue)
|
||||||
|
? props.defaultSelectedValue
|
||||||
|
: props.defaultSelectedValue !== undefined
|
||||||
|
? [props.defaultSelectedValue]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelection = (e: MouseEvent, value: T) => {
|
||||||
|
if (props.multiple) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const newSelectedValues = selectedValues.includes(value)
|
||||||
|
? selectedValues.filter((v) => v !== value)
|
||||||
|
: [...selectedValues, value];
|
||||||
|
setSelectedValues(newSelectedValues);
|
||||||
|
props.onSelection?.(newSelectedValues);
|
||||||
|
} else {
|
||||||
|
setSelectedValues([value]);
|
||||||
|
props.onSelection?.([value]);
|
||||||
|
props.onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
|
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
|
||||||
<div className={styles["selector-content"]}>
|
<div className={styles["selector-content"]}>
|
||||||
<List>
|
<List>
|
||||||
{props.items.map((item, i) => {
|
{props.items.map((item, i) => {
|
||||||
const selected = props.defaultSelectedValue === item.value;
|
const selected = selectedValues.includes(item.value);
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
className={styles["selector-item"]}
|
className={`${styles["selector-item"]} ${
|
||||||
|
item.disable && styles["selector-item-disabled"]
|
||||||
|
}`}
|
||||||
key={i}
|
key={i}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
subTitle={item.subTitle}
|
subTitle={item.subTitle}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
props.onSelection?.([item.value]);
|
if (item.disable) {
|
||||||
props.onClose?.();
|
e.stopPropagation();
|
||||||
|
} else {
|
||||||
|
handleSelection(e, item.value);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
@@ -485,3 +535,38 @@ export function Selector<T>(props: {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export function FullScreen(props: any) {
|
||||||
|
const { children, right = 10, top = 10, ...rest } = props;
|
||||||
|
const ref = useRef<HTMLDivElement>();
|
||||||
|
const [fullScreen, setFullScreen] = useState(false);
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
ref.current?.requestFullscreen();
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScreenChange = (e: any) => {
|
||||||
|
if (e.target === ref.current) {
|
||||||
|
setFullScreen(!!document.fullscreenElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("fullscreenchange", handleScreenChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("fullscreenchange", handleScreenChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div ref={ref} style={{ position: "relative" }} {...rest}>
|
||||||
|
<div style={{ position: "absolute", right, top }}>
|
||||||
|
<IconButton
|
||||||
|
icon={fullScreen ? <MinIcon /> : <MaxIcon />}
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
|
|||||||
export function getClientConfig() {
|
export function getClientConfig() {
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
// client side
|
// client side
|
||||||
return JSON.parse(queryMeta("config")) as BuildConfig;
|
return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof process !== "undefined") {
|
if (typeof process !== "undefined") {
|
||||||
|
@@ -21,7 +21,11 @@ declare global {
|
|||||||
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
|
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
|
||||||
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
|
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
|
||||||
CUSTOM_MODELS?: string; // to control custom models
|
CUSTOM_MODELS?: string; // to control custom models
|
||||||
DEFAULT_MODEL?: string; // to cnntrol default model in every new chat window
|
DEFAULT_MODEL?: string; // to control default model in every new chat window
|
||||||
|
|
||||||
|
// stability only
|
||||||
|
STABILITY_URL?: string;
|
||||||
|
STABILITY_API_KEY?: string;
|
||||||
|
|
||||||
// azure only
|
// azure only
|
||||||
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
||||||
@@ -53,6 +57,20 @@ declare global {
|
|||||||
ALIBABA_URL?: string;
|
ALIBABA_URL?: string;
|
||||||
ALIBABA_API_KEY?: string;
|
ALIBABA_API_KEY?: string;
|
||||||
|
|
||||||
|
// tencent only
|
||||||
|
TENCENT_URL?: string;
|
||||||
|
TENCENT_SECRET_KEY?: string;
|
||||||
|
TENCENT_SECRET_ID?: string;
|
||||||
|
|
||||||
|
// moonshot only
|
||||||
|
MOONSHOT_URL?: string;
|
||||||
|
MOONSHOT_API_KEY?: string;
|
||||||
|
|
||||||
|
// iflytek only
|
||||||
|
IFLYTEK_URL?: string;
|
||||||
|
IFLYTEK_API_KEY?: string;
|
||||||
|
IFLYTEK_API_SECRET?: string;
|
||||||
|
|
||||||
// custom template for preprocessing user input
|
// custom template for preprocessing user input
|
||||||
DEFAULT_INPUT_TEMPLATE?: string;
|
DEFAULT_INPUT_TEMPLATE?: string;
|
||||||
}
|
}
|
||||||
@@ -107,13 +125,18 @@ export const getServerSideConfig = () => {
|
|||||||
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
|
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isStability = !!process.env.STABILITY_API_KEY;
|
||||||
|
|
||||||
const isAzure = !!process.env.AZURE_URL;
|
const isAzure = !!process.env.AZURE_URL;
|
||||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
const isTencent = !!process.env.TENCENT_API_KEY;
|
||||||
|
|
||||||
const isBaidu = !!process.env.BAIDU_API_KEY;
|
const isBaidu = !!process.env.BAIDU_API_KEY;
|
||||||
const isBytedance = !!process.env.BYTEDANCE_API_KEY;
|
const isBytedance = !!process.env.BYTEDANCE_API_KEY;
|
||||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||||
|
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||||
|
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
||||||
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||||
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||||
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||||
@@ -131,6 +154,10 @@ export const getServerSideConfig = () => {
|
|||||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||||
|
|
||||||
|
isStability,
|
||||||
|
stabilityUrl: process.env.STABILITY_URL,
|
||||||
|
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||||
|
|
||||||
isAzure,
|
isAzure,
|
||||||
azureUrl: process.env.AZURE_URL,
|
azureUrl: process.env.AZURE_URL,
|
||||||
azureApiKey: getApiKey(process.env.AZURE_API_KEY),
|
azureApiKey: getApiKey(process.env.AZURE_API_KEY),
|
||||||
@@ -158,6 +185,25 @@ export const getServerSideConfig = () => {
|
|||||||
alibabaUrl: process.env.ALIBABA_URL,
|
alibabaUrl: process.env.ALIBABA_URL,
|
||||||
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
|
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
|
||||||
|
|
||||||
|
isTencent,
|
||||||
|
tencentUrl: process.env.TENCENT_URL,
|
||||||
|
tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
|
||||||
|
tencentSecretId: process.env.TENCENT_SECRET_ID,
|
||||||
|
|
||||||
|
isMoonshot,
|
||||||
|
moonshotUrl: process.env.MOONSHOT_URL,
|
||||||
|
moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
|
||||||
|
|
||||||
|
isIflytek,
|
||||||
|
iflytekUrl: process.env.IFLYTEK_URL,
|
||||||
|
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||||
|
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
||||||
|
|
||||||
|
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
|
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||||
|
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||||
|
cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
|
||||||
|
|
||||||
gtmId: process.env.GTM_ID,
|
gtmId: process.env.GTM_ID,
|
||||||
|
|
||||||
needCode: ACCESS_CODES.size > 0,
|
needCode: ACCESS_CODES.size > 0,
|
||||||
|
154
app/constant.ts
154
app/constant.ts
@@ -1,4 +1,4 @@
|
|||||||
export const OWNER = "Yidadaa";
|
export const OWNER = "ChatGPTNextWeb";
|
||||||
export const REPO = "ChatGPT-Next-Web";
|
export const REPO = "ChatGPT-Next-Web";
|
||||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||||
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
|
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
|
||||||
@@ -8,6 +8,8 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
|
|||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
|
||||||
|
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||||
|
|
||||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||||
@@ -21,6 +23,14 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
|
|||||||
|
|
||||||
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
|
||||||
|
|
||||||
|
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
||||||
|
|
||||||
|
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||||
|
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||||
|
|
||||||
|
export const CACHE_URL_PREFIX = "/api/cache";
|
||||||
|
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||||
|
|
||||||
export enum Path {
|
export enum Path {
|
||||||
Home = "/",
|
Home = "/",
|
||||||
Chat = "/chat",
|
Chat = "/chat",
|
||||||
@@ -28,6 +38,9 @@ export enum Path {
|
|||||||
NewChat = "/new-chat",
|
NewChat = "/new-chat",
|
||||||
Masks = "/masks",
|
Masks = "/masks",
|
||||||
Auth = "/auth",
|
Auth = "/auth",
|
||||||
|
Sd = "/sd",
|
||||||
|
SdNew = "/sd-new",
|
||||||
|
Artifacts = "/artifacts",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
@@ -39,6 +52,11 @@ export enum ApiPath {
|
|||||||
Baidu = "/api/baidu",
|
Baidu = "/api/baidu",
|
||||||
ByteDance = "/api/bytedance",
|
ByteDance = "/api/bytedance",
|
||||||
Alibaba = "/api/alibaba",
|
Alibaba = "/api/alibaba",
|
||||||
|
Tencent = "/api/tencent",
|
||||||
|
Moonshot = "/api/moonshot",
|
||||||
|
Iflytek = "/api/iflytek",
|
||||||
|
Stability = "/api/stability",
|
||||||
|
Artifacts = "/api/artifacts",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
@@ -51,6 +69,10 @@ export enum FileName {
|
|||||||
Prompts = "prompts.json",
|
Prompts = "prompts.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Plugin {
|
||||||
|
Artifacts = "artifacts",
|
||||||
|
}
|
||||||
|
|
||||||
export enum StoreKey {
|
export enum StoreKey {
|
||||||
Chat = "chat-next-web-store",
|
Chat = "chat-next-web-store",
|
||||||
Access = "access-control",
|
Access = "access-control",
|
||||||
@@ -59,6 +81,7 @@ export enum StoreKey {
|
|||||||
Prompt = "prompt-store",
|
Prompt = "prompt-store",
|
||||||
Update = "chat-update",
|
Update = "chat-update",
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
|
SdList = "sd-list",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||||
@@ -85,17 +108,39 @@ export enum ServiceProvider {
|
|||||||
Baidu = "Baidu",
|
Baidu = "Baidu",
|
||||||
ByteDance = "ByteDance",
|
ByteDance = "ByteDance",
|
||||||
Alibaba = "Alibaba",
|
Alibaba = "Alibaba",
|
||||||
|
Tencent = "Tencent",
|
||||||
|
Moonshot = "Moonshot",
|
||||||
|
Stability = "Stability",
|
||||||
|
Iflytek = "Iflytek",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||||
|
// BLOCK_NONE will not block any content, and BLOCK_ONLY_HIGH will block only high-risk content.
|
||||||
|
export enum GoogleSafetySettingsThreshold {
|
||||||
|
BLOCK_NONE = "BLOCK_NONE",
|
||||||
|
BLOCK_ONLY_HIGH = "BLOCK_ONLY_HIGH",
|
||||||
|
BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE",
|
||||||
|
BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
|
Stability = "Stability",
|
||||||
GPT = "GPT",
|
GPT = "GPT",
|
||||||
GeminiPro = "GeminiPro",
|
GeminiPro = "GeminiPro",
|
||||||
Claude = "Claude",
|
Claude = "Claude",
|
||||||
Ernie = "Ernie",
|
Ernie = "Ernie",
|
||||||
Doubao = "Doubao",
|
Doubao = "Doubao",
|
||||||
Qwen = "Qwen",
|
Qwen = "Qwen",
|
||||||
|
Hunyuan = "Hunyuan",
|
||||||
|
Moonshot = "Moonshot",
|
||||||
|
Iflytek = "Iflytek",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Stability = {
|
||||||
|
GeneratePath: "v2beta/stable-image/generate",
|
||||||
|
ExampleEndpoint: "https://api.stability.ai",
|
||||||
|
};
|
||||||
|
|
||||||
export const Anthropic = {
|
export const Anthropic = {
|
||||||
ChatPath: "v1/messages",
|
ChatPath: "v1/messages",
|
||||||
ChatPath1: "v1/complete",
|
ChatPath1: "v1/complete",
|
||||||
@@ -105,6 +150,7 @@ export const Anthropic = {
|
|||||||
|
|
||||||
export const OpenaiPath = {
|
export const OpenaiPath = {
|
||||||
ChatPath: "v1/chat/completions",
|
ChatPath: "v1/chat/completions",
|
||||||
|
ImagePath: "v1/images/generations",
|
||||||
UsagePath: "dashboard/billing/usage",
|
UsagePath: "dashboard/billing/usage",
|
||||||
SubsPath: "dashboard/billing/subscription",
|
SubsPath: "dashboard/billing/subscription",
|
||||||
ListModelPath: "v1/models",
|
ListModelPath: "v1/models",
|
||||||
@@ -113,12 +159,16 @@ export const OpenaiPath = {
|
|||||||
export const Azure = {
|
export const Azure = {
|
||||||
ChatPath: (deployName: string, apiVersion: string) =>
|
ChatPath: (deployName: string, apiVersion: string) =>
|
||||||
`deployments/${deployName}/chat/completions?api-version=${apiVersion}`,
|
`deployments/${deployName}/chat/completions?api-version=${apiVersion}`,
|
||||||
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
|
// https://<your_resource_name>.openai.azure.com/openai/deployments/<your_deployment_name>/images/generations?api-version=<api_version>
|
||||||
|
ImagePath: (deployName: string, apiVersion: string) =>
|
||||||
|
`deployments/${deployName}/images/generations?api-version=${apiVersion}`,
|
||||||
|
ExampleEndpoint: "https://{resource-url}/openai",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Google = {
|
export const Google = {
|
||||||
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
||||||
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
|
ChatPath: (modelName: string) =>
|
||||||
|
`v1beta/models/${modelName}:streamGenerateContent`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Baidu = {
|
export const Baidu = {
|
||||||
@@ -134,6 +184,9 @@ export const Baidu = {
|
|||||||
if (modelName === "ernie-3.5-8k") {
|
if (modelName === "ernie-3.5-8k") {
|
||||||
endpoint = "completions";
|
endpoint = "completions";
|
||||||
}
|
}
|
||||||
|
if (modelName === "ernie-speed-8k") {
|
||||||
|
endpoint = "ernie_speed";
|
||||||
|
}
|
||||||
return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`;
|
return `rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endpoint}`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -148,6 +201,20 @@ export const Alibaba = {
|
|||||||
ChatPath: "v1/services/aigc/text-generation/generation",
|
ChatPath: "v1/services/aigc/text-generation/generation",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Tencent = {
|
||||||
|
ExampleEndpoint: TENCENT_BASE_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Moonshot = {
|
||||||
|
ExampleEndpoint: MOONSHOT_BASE_URL,
|
||||||
|
ChatPath: "v1/chat/completions",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Iflytek = {
|
||||||
|
ExampleEndpoint: IFLYTEK_BASE_URL,
|
||||||
|
ChatPath: "v1/chat/completions",
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||||
@@ -166,7 +233,7 @@ Latex inline: \\(x^2\\)
|
|||||||
Latex block: $$e=mc^2$$
|
Latex block: $$e=mc^2$$
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
export const SUMMARIZE_MODEL = "gpt-4o-mini";
|
||||||
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
|
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
|
||||||
|
|
||||||
export const KnowledgeCutOffDate: Record<string, string> = {
|
export const KnowledgeCutOffDate: Record<string, string> = {
|
||||||
@@ -176,6 +243,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
|||||||
"gpt-4-turbo-preview": "2023-12",
|
"gpt-4-turbo-preview": "2023-12",
|
||||||
"gpt-4o": "2023-10",
|
"gpt-4o": "2023-10",
|
||||||
"gpt-4o-2024-05-13": "2023-10",
|
"gpt-4o-2024-05-13": "2023-10",
|
||||||
|
"gpt-4o-mini": "2023-10",
|
||||||
|
"gpt-4o-mini-2024-07-18": "2023-10",
|
||||||
"gpt-4-vision-preview": "2023-04",
|
"gpt-4-vision-preview": "2023-04",
|
||||||
// After improvements,
|
// After improvements,
|
||||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||||
@@ -195,9 +264,12 @@ const openaiModels = [
|
|||||||
"gpt-4-turbo-preview",
|
"gpt-4-turbo-preview",
|
||||||
"gpt-4o",
|
"gpt-4o",
|
||||||
"gpt-4o-2024-05-13",
|
"gpt-4o-2024-05-13",
|
||||||
|
"gpt-4o-mini",
|
||||||
|
"gpt-4o-mini-2024-07-18",
|
||||||
"gpt-4-vision-preview",
|
"gpt-4-vision-preview",
|
||||||
"gpt-4-turbo-2024-04-09",
|
"gpt-4-turbo-2024-04-09",
|
||||||
"gpt-4-1106-preview",
|
"gpt-4-1106-preview",
|
||||||
|
"dall-e-3",
|
||||||
];
|
];
|
||||||
|
|
||||||
const googleModels = [
|
const googleModels = [
|
||||||
@@ -225,6 +297,10 @@ const baiduModels = [
|
|||||||
"ernie-4.0-8k-latest",
|
"ernie-4.0-8k-latest",
|
||||||
"ernie-3.5-8k",
|
"ernie-3.5-8k",
|
||||||
"ernie-3.5-8k-0205",
|
"ernie-3.5-8k-0205",
|
||||||
|
"ernie-speed-128k",
|
||||||
|
"ernie-speed-8k",
|
||||||
|
"ernie-lite-8k",
|
||||||
|
"ernie-tiny-8k",
|
||||||
];
|
];
|
||||||
|
|
||||||
const bytedanceModels = [
|
const bytedanceModels = [
|
||||||
@@ -246,68 +322,136 @@ const alibabaModes = [
|
|||||||
"qwen-max-longcontext",
|
"qwen-max-longcontext",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const tencentModels = [
|
||||||
|
"hunyuan-pro",
|
||||||
|
"hunyuan-standard",
|
||||||
|
"hunyuan-lite",
|
||||||
|
"hunyuan-role",
|
||||||
|
"hunyuan-functioncall",
|
||||||
|
"hunyuan-code",
|
||||||
|
"hunyuan-vision",
|
||||||
|
];
|
||||||
|
|
||||||
|
const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
|
||||||
|
|
||||||
|
const iflytekModels = [
|
||||||
|
"general",
|
||||||
|
"generalv3",
|
||||||
|
"pro-128k",
|
||||||
|
"generalv3.5",
|
||||||
|
"4.0Ultra",
|
||||||
|
];
|
||||||
|
|
||||||
|
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||||
export const DEFAULT_MODELS = [
|
export const DEFAULT_MODELS = [
|
||||||
...openaiModels.map((name) => ({
|
...openaiModels.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++, // Global sequence sort(index)
|
||||||
provider: {
|
provider: {
|
||||||
id: "openai",
|
id: "openai",
|
||||||
providerName: "OpenAI",
|
providerName: "OpenAI",
|
||||||
providerType: "openai",
|
providerType: "openai",
|
||||||
|
sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
...openaiModels.map((name) => ({
|
...openaiModels.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "azure",
|
id: "azure",
|
||||||
providerName: "Azure",
|
providerName: "Azure",
|
||||||
providerType: "azure",
|
providerType: "azure",
|
||||||
|
sorted: 2,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
...googleModels.map((name) => ({
|
...googleModels.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "google",
|
id: "google",
|
||||||
providerName: "Google",
|
providerName: "Google",
|
||||||
providerType: "google",
|
providerType: "google",
|
||||||
|
sorted: 3,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
...anthropicModels.map((name) => ({
|
...anthropicModels.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "anthropic",
|
id: "anthropic",
|
||||||
providerName: "Anthropic",
|
providerName: "Anthropic",
|
||||||
providerType: "anthropic",
|
providerType: "anthropic",
|
||||||
|
sorted: 4,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
...baiduModels.map((name) => ({
|
...baiduModels.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "baidu",
|
id: "baidu",
|
||||||
providerName: "Baidu",
|
providerName: "Baidu",
|
||||||
providerType: "baidu",
|
providerType: "baidu",
|
||||||
|
sorted: 5,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
...bytedanceModels.map((name) => ({
|
...bytedanceModels.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "bytedance",
|
id: "bytedance",
|
||||||
providerName: "ByteDance",
|
providerName: "ByteDance",
|
||||||
providerType: "bytedance",
|
providerType: "bytedance",
|
||||||
|
sorted: 6,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
...alibabaModes.map((name) => ({
|
...alibabaModes.map((name) => ({
|
||||||
name,
|
name,
|
||||||
available: true,
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
provider: {
|
provider: {
|
||||||
id: "alibaba",
|
id: "alibaba",
|
||||||
providerName: "Alibaba",
|
providerName: "Alibaba",
|
||||||
providerType: "alibaba",
|
providerType: "alibaba",
|
||||||
|
sorted: 7,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...tencentModels.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "tencent",
|
||||||
|
providerName: "Tencent",
|
||||||
|
providerType: "tencent",
|
||||||
|
sorted: 8,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...moonshotModes.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "moonshot",
|
||||||
|
providerName: "Moonshot",
|
||||||
|
providerType: "moonshot",
|
||||||
|
sorted: 9,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...iflytekModels.map((name) => ({
|
||||||
|
name,
|
||||||
|
available: true,
|
||||||
|
sorted: seq++,
|
||||||
|
provider: {
|
||||||
|
id: "iflytek",
|
||||||
|
providerName: "Iflytek",
|
||||||
|
providerType: "iflytek",
|
||||||
|
sorted: 10,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
] as const;
|
] as const;
|
||||||
@@ -327,3 +471,5 @@ export const internalAllowedWebDavEndpoints = [
|
|||||||
"https://webdav.yandex.com",
|
"https://webdav.yandex.com",
|
||||||
"https://app.koofr.net/dav/Koofr",
|
"https://app.koofr.net/dav/Koofr",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
|
||||||
|
7
app/icons/discovery.svg
Normal file
7
app/icons/discovery.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path
|
||||||
|
d="M11.307 9.739L15 9l-.739 3.693a2 2 0 0 1-1.568 1.569L9 15l.739-3.693a2 2 0 0 1 1.568-1.568" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 371 B |
10
app/icons/history.svg
Normal file
10
app/icons/history.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.81836 6.72729V14H13.0911" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C16.598 4 10.1351 8.02111 6.67677 13.9981"
|
||||||
|
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M24.005 12L24.0038 24.0088L32.4832 32.4882" stroke="#333" stroke-width="4"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 660 B |
12
app/icons/sd.svg
Normal file
12
app/icons/sd.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.21em" height="1em" viewBox="0 0 256 213">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logosStabilityAiIcon0" x1="50%" x2="50%" y1="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#9d39ff" />
|
||||||
|
<stop offset="100%" stop-color="#a380ff" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="url(#logosStabilityAiIcon0)"
|
||||||
|
d="M72.418 212.45c49.478 0 81.658-26.205 81.658-65.626c0-30.572-19.572-49.998-54.569-58.043l-22.469-6.74c-19.71-4.424-31.215-9.738-28.505-23.312c2.255-11.292 9.002-17.667 24.69-17.667c49.872 0 68.35 17.667 68.35 17.667V16.237S123.583 0 73.223 0C25.757 0 0 24.424 0 62.236c0 30.571 17.85 48.35 54.052 56.798q3.802.95 3.885.976q8.26 2.556 22.293 6.755c18.504 4.425 23.262 9.121 23.262 23.2c0 12.872-13.374 20.19-31.074 20.19C21.432 170.154 0 144.36 0 144.36v47.078s13.402 21.01 72.418 21.01" />
|
||||||
|
<path fill="#e80000"
|
||||||
|
d="M225.442 209.266c17.515 0 30.558-12.67 30.558-29.812c0-17.515-12.67-29.813-30.558-29.813c-17.515 0-30.185 12.298-30.185 29.813s12.67 29.812 30.185 29.812" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
app/icons/size.svg
Normal file
1
app/icons/size.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V39C4 40.1046 4.89543 41 6 41H42C43.1046 41 44 40.1046 44 39V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#333" stroke-width="4"/><path d="M30 30V18L38 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 30V18L18 30V18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 20V21" stroke="#333" stroke-width="4" stroke-linecap="round"/><path d="M24 27V28" stroke="#333" stroke-width="4" stroke-linecap="round"/></svg>
|
After Width: | Height: | Size: 681 B |
@@ -37,7 +37,10 @@ export default function RootLayout({
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest"></link>
|
<link rel="manifest" href="/site.webmanifest"></link>
|
||||||
<script src="/serviceWorkerRegister.js" defer></script>
|
<script src="/serviceWorkerRegister.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
@@ -111,6 +111,11 @@ const ar: PartialLocaleType = {
|
|||||||
Title: "حجم الخط",
|
Title: "حجم الخط",
|
||||||
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
|
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "خط الدردشة",
|
||||||
|
SubTitle: "خط محتوى الدردشة، اتركه فارغًا لتطبيق الخط الافتراضي العالمي",
|
||||||
|
Placeholder: "اسم الخط",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "حقن تلميحات النظام",
|
Title: "حقن تلميحات النظام",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -136,6 +136,12 @@ const bn: PartialLocaleType = {
|
|||||||
Title: "ফন্ট সাইজ",
|
Title: "ফন্ট সাইজ",
|
||||||
SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন",
|
SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "চ্যাট ফন্ট",
|
||||||
|
SubTitle:
|
||||||
|
"চ্যাট সামগ্রীর ফন্ট, বিশ্বব্যাপী ডিফল্ট ফন্ট প্রয়োগ করতে খালি রাখুন",
|
||||||
|
Placeholder: "ফন্টের নাম",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "حقن تلميحات النظام",
|
Title: "حقن تلميحات النظام",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -104,6 +104,10 @@ const cn = {
|
|||||||
Toast: "正在生成截图",
|
Toast: "正在生成截图",
|
||||||
Modal: "长按或右键保存图片",
|
Modal: "长按或右键保存图片",
|
||||||
},
|
},
|
||||||
|
Artifacts: {
|
||||||
|
Title: "分享页面",
|
||||||
|
Error: "分享失败",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Select: {
|
Select: {
|
||||||
Search: "搜索消息",
|
Search: "搜索消息",
|
||||||
@@ -152,6 +156,11 @@ const cn = {
|
|||||||
Title: "字体大小",
|
Title: "字体大小",
|
||||||
SubTitle: "聊天内容的字体大小",
|
SubTitle: "聊天内容的字体大小",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "聊天字体",
|
||||||
|
SubTitle: "聊天内容的字体,若置空则应用全局默认字体",
|
||||||
|
Placeholder: "字体名称",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "注入系统级提示信息",
|
Title: "注入系统级提示信息",
|
||||||
SubTitle: "强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示",
|
SubTitle: "强制给每次请求的消息列表开头添加一个模拟 ChatGPT 的系统提示",
|
||||||
@@ -346,6 +355,10 @@ const cn = {
|
|||||||
Title: "API 版本(仅适用于 gemini-pro)",
|
Title: "API 版本(仅适用于 gemini-pro)",
|
||||||
SubTitle: "选择一个特定的 API 版本",
|
SubTitle: "选择一个特定的 API 版本",
|
||||||
},
|
},
|
||||||
|
GoogleSafetySettings: {
|
||||||
|
Title: "Google 安全过滤级别",
|
||||||
|
SubTitle: "设置内容过滤级别",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Baidu: {
|
Baidu: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
@@ -363,6 +376,22 @@ const cn = {
|
|||||||
SubTitle: "不支持自定义前往.env配置",
|
SubTitle: "不支持自定义前往.env配置",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tencent: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "API Key",
|
||||||
|
SubTitle: "使用自定义腾讯云API Key",
|
||||||
|
Placeholder: "Tencent API Key",
|
||||||
|
},
|
||||||
|
SecretKey: {
|
||||||
|
Title: "Secret Key",
|
||||||
|
SubTitle: "使用自定义腾讯云Secret Key",
|
||||||
|
Placeholder: "Tencent Secret Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "不支持自定义前往.env配置",
|
||||||
|
},
|
||||||
|
},
|
||||||
ByteDance: {
|
ByteDance: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "接口密钥",
|
Title: "接口密钥",
|
||||||
@@ -385,6 +414,44 @@ const cn = {
|
|||||||
SubTitle: "样例:",
|
SubTitle: "样例:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Moonshot: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义月之暗面API Key",
|
||||||
|
Placeholder: "Moonshot API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Stability: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义 Stability API Key",
|
||||||
|
Placeholder: "Stability API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Iflytek: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "ApiKey",
|
||||||
|
SubTitle: "从讯飞星火控制台获取的 APIKey",
|
||||||
|
Placeholder: "APIKey",
|
||||||
|
},
|
||||||
|
ApiSecret: {
|
||||||
|
Title: "ApiSecret",
|
||||||
|
SubTitle: "从讯飞星火控制台获取的 APISecret",
|
||||||
|
Placeholder: "APISecret",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
CustomModel: {
|
CustomModel: {
|
||||||
Title: "自定义模型名",
|
Title: "自定义模型名",
|
||||||
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
||||||
@@ -442,6 +509,10 @@ const cn = {
|
|||||||
},
|
},
|
||||||
Plugin: {
|
Plugin: {
|
||||||
Name: "插件",
|
Name: "插件",
|
||||||
|
Artifacts: "Artifacts",
|
||||||
|
},
|
||||||
|
Discovery: {
|
||||||
|
Name: "发现",
|
||||||
},
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "你是一个助手",
|
Sysmessage: "你是一个助手",
|
||||||
@@ -522,6 +593,61 @@ const cn = {
|
|||||||
Topic: "主题",
|
Topic: "主题",
|
||||||
Time: "时间",
|
Time: "时间",
|
||||||
},
|
},
|
||||||
|
SdPanel: {
|
||||||
|
Prompt: "画面提示",
|
||||||
|
NegativePrompt: "否定提示",
|
||||||
|
PleaseInput: (name: string) => `请输入${name}`,
|
||||||
|
AspectRatio: "横纵比",
|
||||||
|
ImageStyle: "图像风格",
|
||||||
|
OutFormat: "输出格式",
|
||||||
|
AIModel: "AI模型",
|
||||||
|
ModelVersion: "模型版本",
|
||||||
|
Submit: "提交生成",
|
||||||
|
ParamIsRequired: (name: string) => `${name}不能为空`,
|
||||||
|
Styles: {
|
||||||
|
D3Model: "3D模型",
|
||||||
|
AnalogFilm: "模拟电影",
|
||||||
|
Anime: "动漫",
|
||||||
|
Cinematic: "电影风格",
|
||||||
|
ComicBook: "漫画书",
|
||||||
|
DigitalArt: "数字艺术",
|
||||||
|
Enhance: "增强",
|
||||||
|
FantasyArt: "幻想艺术",
|
||||||
|
Isometric: "等角",
|
||||||
|
LineArt: "线描",
|
||||||
|
LowPoly: "低多边形",
|
||||||
|
ModelingCompound: "建模材料",
|
||||||
|
NeonPunk: "霓虹朋克",
|
||||||
|
Origami: "折纸",
|
||||||
|
Photographic: "摄影",
|
||||||
|
PixelArt: "像素艺术",
|
||||||
|
TileTexture: "贴图",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sd: {
|
||||||
|
SubTitle: (count: number) => `共 ${count} 条绘画`,
|
||||||
|
Actions: {
|
||||||
|
Params: "查看参数",
|
||||||
|
Copy: "复制提示词",
|
||||||
|
Delete: "删除",
|
||||||
|
Retry: "重试",
|
||||||
|
ReturnHome: "返回首页",
|
||||||
|
History: "查看历史",
|
||||||
|
},
|
||||||
|
EmptyRecord: "暂无绘画记录",
|
||||||
|
Status: {
|
||||||
|
Name: "状态",
|
||||||
|
Success: "成功",
|
||||||
|
Error: "失败",
|
||||||
|
Wait: "等待中",
|
||||||
|
Running: "运行中",
|
||||||
|
},
|
||||||
|
Danger: {
|
||||||
|
Delete: "确认删除?",
|
||||||
|
},
|
||||||
|
GenerateParams: "生成参数",
|
||||||
|
Detail: "详情",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeepPartial<T> = T extends object
|
type DeepPartial<T> = T extends object
|
||||||
|
@@ -71,6 +71,12 @@ const cs: PartialLocaleType = {
|
|||||||
Title: "Velikost písma",
|
Title: "Velikost písma",
|
||||||
SubTitle: "Nastavení velikosti písma obsahu chatu",
|
SubTitle: "Nastavení velikosti písma obsahu chatu",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Chatové Písmo",
|
||||||
|
SubTitle:
|
||||||
|
"Písmo obsahu chatu, ponechejte prázdné pro použití globálního výchozího písma",
|
||||||
|
Placeholder: "Název Písma",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Vložit systémové prompty",
|
Title: "Vložit systémové prompty",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -71,6 +71,12 @@ const de: PartialLocaleType = {
|
|||||||
Title: "Schriftgröße",
|
Title: "Schriftgröße",
|
||||||
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
|
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Chat-Schriftart",
|
||||||
|
SubTitle:
|
||||||
|
"Schriftart des Chat-Inhalts, leer lassen, um die globale Standardschriftart anzuwenden",
|
||||||
|
Placeholder: "Schriftartname",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "System-Prompts einfügen",
|
Title: "System-Prompts einfügen",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -106,6 +106,10 @@ const en: LocaleType = {
|
|||||||
Toast: "Capturing Image...",
|
Toast: "Capturing Image...",
|
||||||
Modal: "Long press or right click to save image",
|
Modal: "Long press or right click to save image",
|
||||||
},
|
},
|
||||||
|
Artifacts: {
|
||||||
|
Title: "Share Artifacts",
|
||||||
|
Error: "Share Error",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Select: {
|
Select: {
|
||||||
Search: "Search",
|
Search: "Search",
|
||||||
@@ -154,6 +158,12 @@ const en: LocaleType = {
|
|||||||
Title: "Font Size",
|
Title: "Font Size",
|
||||||
SubTitle: "Adjust font size of chat content",
|
SubTitle: "Adjust font size of chat content",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Chat Font Family",
|
||||||
|
SubTitle:
|
||||||
|
"Font Family of the chat content, leave empty to apply global default font",
|
||||||
|
Placeholder: "Font Family Name",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Inject System Prompts",
|
Title: "Inject System Prompts",
|
||||||
SubTitle: "Inject a global system prompt for every request",
|
SubTitle: "Inject a global system prompt for every request",
|
||||||
@@ -350,6 +360,22 @@ const en: LocaleType = {
|
|||||||
SubTitle: "not supported, configure in .env",
|
SubTitle: "not supported, configure in .env",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tencent: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Tencent API Key",
|
||||||
|
SubTitle: "Use a custom Tencent API Key",
|
||||||
|
Placeholder: "Tencent API Key",
|
||||||
|
},
|
||||||
|
SecretKey: {
|
||||||
|
Title: "Tencent Secret Key",
|
||||||
|
SubTitle: "Use a custom Tencent Secret Key",
|
||||||
|
Placeholder: "Tencent Secret Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "not supported, configure in .env",
|
||||||
|
},
|
||||||
|
},
|
||||||
ByteDance: {
|
ByteDance: {
|
||||||
ApiKey: {
|
ApiKey: {
|
||||||
Title: "ByteDance API Key",
|
Title: "ByteDance API Key",
|
||||||
@@ -372,6 +398,44 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Example: ",
|
SubTitle: "Example: ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Moonshot: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Moonshot API Key",
|
||||||
|
SubTitle: "Use a custom Moonshot API Key",
|
||||||
|
Placeholder: "Moonshot API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Stability: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Stability API Key",
|
||||||
|
SubTitle: "Use a custom Stability API Key",
|
||||||
|
Placeholder: "Stability API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Iflytek: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Iflytek API Key",
|
||||||
|
SubTitle: "Use a Iflytek API Key",
|
||||||
|
Placeholder: "Iflytek API Key",
|
||||||
|
},
|
||||||
|
ApiSecret: {
|
||||||
|
Title: "Iflytek API Secret",
|
||||||
|
SubTitle: "Use a Iflytek API Secret",
|
||||||
|
Placeholder: "Iflytek API Secret",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
CustomModel: {
|
CustomModel: {
|
||||||
Title: "Custom Models",
|
Title: "Custom Models",
|
||||||
SubTitle: "Custom model options, seperated by comma",
|
SubTitle: "Custom model options, seperated by comma",
|
||||||
@@ -392,6 +456,10 @@ const en: LocaleType = {
|
|||||||
Title: "API Version (specific to gemini-pro)",
|
Title: "API Version (specific to gemini-pro)",
|
||||||
SubTitle: "Select a specific API version",
|
SubTitle: "Select a specific API version",
|
||||||
},
|
},
|
||||||
|
GoogleSafetySettings: {
|
||||||
|
Title: "Google Safety Settings",
|
||||||
|
SubTitle: "Select a safety filtering level",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -449,6 +517,10 @@ const en: LocaleType = {
|
|||||||
},
|
},
|
||||||
Plugin: {
|
Plugin: {
|
||||||
Name: "Plugin",
|
Name: "Plugin",
|
||||||
|
Artifacts: "Artifacts",
|
||||||
|
},
|
||||||
|
Discovery: {
|
||||||
|
Name: "Discovery",
|
||||||
},
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "You are an assistant that",
|
Sysmessage: "You are an assistant that",
|
||||||
@@ -524,11 +596,65 @@ const en: LocaleType = {
|
|||||||
Topic: "Topic",
|
Topic: "Topic",
|
||||||
Time: "Time",
|
Time: "Time",
|
||||||
},
|
},
|
||||||
|
|
||||||
URLCommand: {
|
URLCommand: {
|
||||||
Code: "Detected access code from url, confirm to apply? ",
|
Code: "Detected access code from url, confirm to apply? ",
|
||||||
Settings: "Detected settings from url, confirm to apply?",
|
Settings: "Detected settings from url, confirm to apply?",
|
||||||
},
|
},
|
||||||
|
SdPanel: {
|
||||||
|
Prompt: "Prompt",
|
||||||
|
NegativePrompt: "Negative Prompt",
|
||||||
|
PleaseInput: (name: string) => `Please input ${name}`,
|
||||||
|
AspectRatio: "Aspect Ratio",
|
||||||
|
ImageStyle: "Image Style",
|
||||||
|
OutFormat: "Output Format",
|
||||||
|
AIModel: "AI Model",
|
||||||
|
ModelVersion: "Model Version",
|
||||||
|
Submit: "Submit",
|
||||||
|
ParamIsRequired: (name: string) => `${name} is required`,
|
||||||
|
Styles: {
|
||||||
|
D3Model: "3d-model",
|
||||||
|
AnalogFilm: "analog-film",
|
||||||
|
Anime: "anime",
|
||||||
|
Cinematic: "cinematic",
|
||||||
|
ComicBook: "comic-book",
|
||||||
|
DigitalArt: "digital-art",
|
||||||
|
Enhance: "enhance",
|
||||||
|
FantasyArt: "fantasy-art",
|
||||||
|
Isometric: "isometric",
|
||||||
|
LineArt: "line-art",
|
||||||
|
LowPoly: "low-poly",
|
||||||
|
ModelingCompound: "modeling-compound",
|
||||||
|
NeonPunk: "neon-punk",
|
||||||
|
Origami: "origami",
|
||||||
|
Photographic: "photographic",
|
||||||
|
PixelArt: "pixel-art",
|
||||||
|
TileTexture: "tile-texture",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sd: {
|
||||||
|
SubTitle: (count: number) => `${count} images`,
|
||||||
|
Actions: {
|
||||||
|
Params: "See Params",
|
||||||
|
Copy: "Copy Prompt",
|
||||||
|
Delete: "Delete",
|
||||||
|
Retry: "Retry",
|
||||||
|
ReturnHome: "Return Home",
|
||||||
|
History: "History",
|
||||||
|
},
|
||||||
|
EmptyRecord: "No images yet",
|
||||||
|
Status: {
|
||||||
|
Name: "Status",
|
||||||
|
Success: "Success",
|
||||||
|
Error: "Error",
|
||||||
|
Wait: "Waiting",
|
||||||
|
Running: "Running",
|
||||||
|
},
|
||||||
|
Danger: {
|
||||||
|
Delete: "Confirm to delete?",
|
||||||
|
},
|
||||||
|
GenerateParams: "Generate Params",
|
||||||
|
Detail: "Detail",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
@@ -71,6 +71,12 @@ const es: PartialLocaleType = {
|
|||||||
Title: "Tamaño de fuente",
|
Title: "Tamaño de fuente",
|
||||||
SubTitle: "Ajustar el tamaño de fuente del contenido del chat",
|
SubTitle: "Ajustar el tamaño de fuente del contenido del chat",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Fuente del Chat",
|
||||||
|
SubTitle:
|
||||||
|
"Fuente del contenido del chat, dejar vacío para aplicar la fuente predeterminada global",
|
||||||
|
Placeholder: "Nombre de la Fuente",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Inyectar Prompts del Sistema",
|
Title: "Inyectar Prompts del Sistema",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -111,6 +111,12 @@ const fr: PartialLocaleType = {
|
|||||||
Title: "Taille des polices",
|
Title: "Taille des polices",
|
||||||
SubTitle: "Ajuste la taille de police du contenu de la conversation",
|
SubTitle: "Ajuste la taille de police du contenu de la conversation",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Police de Chat",
|
||||||
|
SubTitle:
|
||||||
|
"Police du contenu du chat, laissez vide pour appliquer la police par défaut globale",
|
||||||
|
Placeholder: "Nom de la Police",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Injecter des invites système",
|
Title: "Injecter des invites système",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -140,6 +140,12 @@ const id: PartialLocaleType = {
|
|||||||
Title: "Ukuran Font",
|
Title: "Ukuran Font",
|
||||||
SubTitle: "Ubah ukuran font konten chat",
|
SubTitle: "Ubah ukuran font konten chat",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Font Obrolan",
|
||||||
|
SubTitle:
|
||||||
|
"Font dari konten obrolan, biarkan kosong untuk menerapkan font default global",
|
||||||
|
Placeholder: "Nama Font",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Suntikkan Petunjuk Sistem",
|
Title: "Suntikkan Petunjuk Sistem",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
@@ -369,8 +375,8 @@ const id: PartialLocaleType = {
|
|||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
Description: {
|
Description: {
|
||||||
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
|
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan",
|
||||||
},
|
},
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
Messages: "Pesan",
|
Messages: "Pesan",
|
||||||
Topic: "Topik",
|
Topic: "Topik",
|
||||||
|
@@ -71,6 +71,12 @@ const it: PartialLocaleType = {
|
|||||||
Title: "Dimensione carattere",
|
Title: "Dimensione carattere",
|
||||||
SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat",
|
SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Font della Chat",
|
||||||
|
SubTitle:
|
||||||
|
"Carattere del contenuto della chat, lascia vuoto per applicare il carattere predefinito globale",
|
||||||
|
Placeholder: "Nome del Font",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Inserisci Prompts di Sistema",
|
Title: "Inserisci Prompts di Sistema",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -118,6 +118,12 @@ const jp: PartialLocaleType = {
|
|||||||
Title: "フォントサイズ",
|
Title: "フォントサイズ",
|
||||||
SubTitle: "チャット内容のフォントサイズ",
|
SubTitle: "チャット内容のフォントサイズ",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "チャットフォント",
|
||||||
|
SubTitle:
|
||||||
|
"チャットコンテンツのフォント、空白の場合はグローバルデフォルトフォントを適用します",
|
||||||
|
Placeholder: "フォント名",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "システムプロンプトの挿入",
|
Title: "システムプロンプトの挿入",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -72,6 +72,11 @@ const ko: PartialLocaleType = {
|
|||||||
Title: "글꼴 크기",
|
Title: "글꼴 크기",
|
||||||
SubTitle: "채팅 내용의 글꼴 크기 조정",
|
SubTitle: "채팅 내용의 글꼴 크기 조정",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "채팅 폰트",
|
||||||
|
SubTitle: "채팅 내용의 폰트, 비워 두면 글로벌 기본 폰트를 적용",
|
||||||
|
Placeholder: "폰트 이름",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "시스템 프롬프트 주입",
|
Title: "시스템 프롬프트 주입",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -66,6 +66,12 @@ const no: PartialLocaleType = {
|
|||||||
Title: "Fontstørrelsen",
|
Title: "Fontstørrelsen",
|
||||||
SubTitle: "Juster fontstørrelsen for samtaleinnholdet.",
|
SubTitle: "Juster fontstørrelsen for samtaleinnholdet.",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Chat-skrifttype",
|
||||||
|
SubTitle:
|
||||||
|
"Skrifttypen for chatinnhold, la stå tom for å bruke global standardskrifttype",
|
||||||
|
Placeholder: "Skriftnavn",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Sett inn systemprompter",
|
Title: "Sett inn systemprompter",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -153,6 +153,12 @@ const pt: PartialLocaleType = {
|
|||||||
Title: "Tamanho da Fonte",
|
Title: "Tamanho da Fonte",
|
||||||
SubTitle: "Ajustar o tamanho da fonte do conteúdo do chat",
|
SubTitle: "Ajustar o tamanho da fonte do conteúdo do chat",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Fonte do Chat",
|
||||||
|
SubTitle:
|
||||||
|
"Fonte do conteúdo do chat, deixe vazio para aplicar a fonte padrão global",
|
||||||
|
Placeholder: "Nome da Fonte",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Inserir Prompts de Sistema",
|
Title: "Inserir Prompts de Sistema",
|
||||||
SubTitle: "Inserir um prompt de sistema global para cada requisição",
|
SubTitle: "Inserir um prompt de sistema global para cada requisição",
|
||||||
|
@@ -71,6 +71,12 @@ const ru: PartialLocaleType = {
|
|||||||
Title: "Размер шрифта",
|
Title: "Размер шрифта",
|
||||||
SubTitle: "Настроить размер шрифта контента чата",
|
SubTitle: "Настроить размер шрифта контента чата",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Шрифт чата",
|
||||||
|
SubTitle:
|
||||||
|
"Шрифт содержимого чата, оставьте пустым для применения глобального шрифта по умолчанию",
|
||||||
|
Placeholder: "Название шрифта",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Вставить системные подсказки",
|
Title: "Вставить системные подсказки",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -155,6 +155,12 @@ const sk: PartialLocaleType = {
|
|||||||
Title: "Veľkosť písma",
|
Title: "Veľkosť písma",
|
||||||
SubTitle: "Nastaviť veľkosť písma obsahu chatu",
|
SubTitle: "Nastaviť veľkosť písma obsahu chatu",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Chatové Písmo",
|
||||||
|
SubTitle:
|
||||||
|
"Písmo obsahu chatu, ponechajte prázdne pre použitie globálneho predvoleného písma",
|
||||||
|
Placeholder: "Názov Písma",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Vložiť systémové výzvy",
|
Title: "Vložiť systémové výzvy",
|
||||||
SubTitle: "Vložiť globálnu systémovú výzvu pre každú požiadavku",
|
SubTitle: "Vložiť globálnu systémovú výzvu pre každú požiadavku",
|
||||||
|
@@ -71,6 +71,12 @@ const tr: PartialLocaleType = {
|
|||||||
Title: "Yazı Boyutu",
|
Title: "Yazı Boyutu",
|
||||||
SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
|
SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Sohbet Yazı Tipi",
|
||||||
|
SubTitle:
|
||||||
|
"Sohbet içeriğinin yazı tipi, boş bırakıldığında küresel varsayılan yazı tipi uygulanır",
|
||||||
|
Placeholder: "Yazı Tipi Adı",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Sistem İpucu Ekleyin",
|
Title: "Sistem İpucu Ekleyin",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -153,6 +153,11 @@ const tw = {
|
|||||||
Title: "字型大小",
|
Title: "字型大小",
|
||||||
SubTitle: "聊天內容的字型大小",
|
SubTitle: "聊天內容的字型大小",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "聊天字體",
|
||||||
|
SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
|
||||||
|
Placeholder: "字體名稱",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "匯入系統提示",
|
Title: "匯入系統提示",
|
||||||
SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
|
SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
|
||||||
@@ -241,7 +246,7 @@ const tw = {
|
|||||||
},
|
},
|
||||||
List: "自訂提示詞列表",
|
List: "自訂提示詞列表",
|
||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`內建 ${builtin} 條,使用者自訂 ${custom} 條`,
|
`內建 ${builtin} 條,使用者自訂 ${custom} 條`,
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "提示詞列表",
|
Title: "提示詞列表",
|
||||||
|
@@ -71,6 +71,12 @@ const vi: PartialLocaleType = {
|
|||||||
Title: "Font chữ",
|
Title: "Font chữ",
|
||||||
SubTitle: "Thay đổi font chữ của nội dung trò chuyện",
|
SubTitle: "Thay đổi font chữ của nội dung trò chuyện",
|
||||||
},
|
},
|
||||||
|
FontFamily: {
|
||||||
|
Title: "Phông Chữ Trò Chuyện",
|
||||||
|
SubTitle:
|
||||||
|
"Phông chữ của nội dung trò chuyện, để trống để áp dụng phông chữ mặc định toàn cầu",
|
||||||
|
Placeholder: "Tên Phông Chữ",
|
||||||
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Tiêm Prompt Hệ thống",
|
Title: "Tiêm Prompt Hệ thống",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ApiPath,
|
ApiPath,
|
||||||
DEFAULT_API_HOST,
|
DEFAULT_API_HOST,
|
||||||
|
GoogleSafetySettingsThreshold,
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
StoreKey,
|
StoreKey,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
@@ -38,7 +39,21 @@ const DEFAULT_ALIBABA_URL = isApp
|
|||||||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
||||||
: ApiPath.Alibaba;
|
: ApiPath.Alibaba;
|
||||||
|
|
||||||
console.log("DEFAULT_ANTHROPIC_URL", DEFAULT_ANTHROPIC_URL);
|
const DEFAULT_TENCENT_URL = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||||
|
: ApiPath.Tencent;
|
||||||
|
|
||||||
|
const DEFAULT_MOONSHOT_URL = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
||||||
|
: ApiPath.Moonshot;
|
||||||
|
|
||||||
|
const DEFAULT_STABILITY_URL = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/stability"
|
||||||
|
: ApiPath.Stability;
|
||||||
|
|
||||||
|
const DEFAULT_IFLYTEK_URL = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/iflytek"
|
||||||
|
: ApiPath.Iflytek;
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
@@ -59,6 +74,7 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
googleUrl: DEFAULT_GOOGLE_URL,
|
googleUrl: DEFAULT_GOOGLE_URL,
|
||||||
googleApiKey: "",
|
googleApiKey: "",
|
||||||
googleApiVersion: "v1",
|
googleApiVersion: "v1",
|
||||||
|
googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH,
|
||||||
|
|
||||||
// anthropic
|
// anthropic
|
||||||
anthropicUrl: DEFAULT_ANTHROPIC_URL,
|
anthropicUrl: DEFAULT_ANTHROPIC_URL,
|
||||||
@@ -78,6 +94,24 @@ const DEFAULT_ACCESS_STATE = {
|
|||||||
alibabaUrl: DEFAULT_ALIBABA_URL,
|
alibabaUrl: DEFAULT_ALIBABA_URL,
|
||||||
alibabaApiKey: "",
|
alibabaApiKey: "",
|
||||||
|
|
||||||
|
// moonshot
|
||||||
|
moonshotUrl: DEFAULT_MOONSHOT_URL,
|
||||||
|
moonshotApiKey: "",
|
||||||
|
|
||||||
|
//stability
|
||||||
|
stabilityUrl: DEFAULT_STABILITY_URL,
|
||||||
|
stabilityApiKey: "",
|
||||||
|
|
||||||
|
// tencent
|
||||||
|
tencentUrl: DEFAULT_TENCENT_URL,
|
||||||
|
tencentSecretKey: "",
|
||||||
|
tencentSecretId: "",
|
||||||
|
|
||||||
|
// iflytek
|
||||||
|
iflytekUrl: DEFAULT_IFLYTEK_URL,
|
||||||
|
iflytekApiKey: "",
|
||||||
|
iflytekApiSecret: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
@@ -126,6 +160,17 @@ export const useAccessStore = createPersistStore(
|
|||||||
return ensure(get(), ["alibabaApiKey"]);
|
return ensure(get(), ["alibabaApiKey"]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isValidTencent() {
|
||||||
|
return ensure(get(), ["tencentSecretKey", "tencentSecretId"]);
|
||||||
|
},
|
||||||
|
|
||||||
|
isValidMoonshot() {
|
||||||
|
return ensure(get(), ["moonshotApiKey"]);
|
||||||
|
},
|
||||||
|
isValidIflytek() {
|
||||||
|
return ensure(get(), ["iflytekApiKey"]);
|
||||||
|
},
|
||||||
|
|
||||||
isAuthorized() {
|
isAuthorized() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
|
|
||||||
@@ -138,6 +183,9 @@ export const useAccessStore = createPersistStore(
|
|||||||
this.isValidBaidu() ||
|
this.isValidBaidu() ||
|
||||||
this.isValidByteDance() ||
|
this.isValidByteDance() ||
|
||||||
this.isValidAlibaba() ||
|
this.isValidAlibaba() ||
|
||||||
|
this.isValidTencent ||
|
||||||
|
this.isValidMoonshot() ||
|
||||||
|
this.isValidIflytek() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||||
);
|
);
|
||||||
|
@@ -9,8 +9,6 @@ import {
|
|||||||
DEFAULT_MODELS,
|
DEFAULT_MODELS,
|
||||||
DEFAULT_SYSTEM_TEMPLATE,
|
DEFAULT_SYSTEM_TEMPLATE,
|
||||||
KnowledgeCutOffDate,
|
KnowledgeCutOffDate,
|
||||||
ServiceProvider,
|
|
||||||
ModelProvider,
|
|
||||||
StoreKey,
|
StoreKey,
|
||||||
SUMMARIZE_MODEL,
|
SUMMARIZE_MODEL,
|
||||||
GEMINI_SUMMARIZE_MODEL,
|
GEMINI_SUMMARIZE_MODEL,
|
||||||
@@ -28,6 +26,7 @@ import { nanoid } from "nanoid";
|
|||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
|
import { isDalle3 } from "../utils";
|
||||||
|
|
||||||
export type ChatMessage = RequestMessage & {
|
export type ChatMessage = RequestMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -92,7 +91,7 @@ function createEmptySession(): ChatSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSummarizeModel(currentModel: string) {
|
function getSummarizeModel(currentModel: string) {
|
||||||
// if it is using gpt-* models, force to use 3.5 to summarize
|
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||||
if (currentModel.startsWith("gpt")) {
|
if (currentModel.startsWith("gpt")) {
|
||||||
const configStore = useAppConfig.getState();
|
const configStore = useAppConfig.getState();
|
||||||
const accessStore = useAccessStore.getState();
|
const accessStore = useAccessStore.getState();
|
||||||
@@ -543,8 +542,13 @@ export const useChatStore = createPersistStore(
|
|||||||
const config = useAppConfig.getState();
|
const config = useAppConfig.getState();
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const modelConfig = session.mask.modelConfig;
|
const modelConfig = session.mask.modelConfig;
|
||||||
|
// skip summarize when using dalle3?
|
||||||
|
if (isDalle3(modelConfig.model)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const api: ClientApi = getClientApi(modelConfig.providerName);
|
const providerName = modelConfig.providerName;
|
||||||
|
const api: ClientApi = getClientApi(providerName);
|
||||||
|
|
||||||
// remove error messages if any
|
// remove error messages if any
|
||||||
const messages = session.messages;
|
const messages = session.messages;
|
||||||
@@ -567,6 +571,7 @@ export const useChatStore = createPersistStore(
|
|||||||
config: {
|
config: {
|
||||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||||
stream: false,
|
stream: false,
|
||||||
|
providerName,
|
||||||
},
|
},
|
||||||
onFinish(message) {
|
onFinish(message) {
|
||||||
get().updateCurrentSession(
|
get().updateCurrentSession(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { LLMModel } from "../client/api";
|
import { LLMModel } from "../client/api";
|
||||||
|
import { DalleSize } from "../typing";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import {
|
import {
|
||||||
DEFAULT_INPUT_TEMPLATE,
|
DEFAULT_INPUT_TEMPLATE,
|
||||||
@@ -33,6 +34,7 @@ export const DEFAULT_CONFIG = {
|
|||||||
submitKey: SubmitKey.Enter,
|
submitKey: SubmitKey.Enter,
|
||||||
avatar: "1f603",
|
avatar: "1f603",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
fontFamily: "",
|
||||||
theme: Theme.Auto as Theme,
|
theme: Theme.Auto as Theme,
|
||||||
tightBorder: !!config?.isApp,
|
tightBorder: !!config?.isApp,
|
||||||
sendPreviewBubble: true,
|
sendPreviewBubble: true,
|
||||||
@@ -60,6 +62,7 @@ export const DEFAULT_CONFIG = {
|
|||||||
compressMessageLengthThreshold: 1000,
|
compressMessageLengthThreshold: 1000,
|
||||||
enableInjectSystemPrompts: true,
|
enableInjectSystemPrompts: true,
|
||||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||||
|
size: "1024x1024" as DalleSize,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
|
|||||||
import { getLang, Lang } from "../locales";
|
import { getLang, Lang } from "../locales";
|
||||||
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
||||||
import { ModelConfig, useAppConfig } from "./config";
|
import { ModelConfig, useAppConfig } from "./config";
|
||||||
import { StoreKey } from "../constant";
|
import { StoreKey, Plugin } from "../constant";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export type Mask = {
|
|||||||
modelConfig: ModelConfig;
|
modelConfig: ModelConfig;
|
||||||
lang: Lang;
|
lang: Lang;
|
||||||
builtin: boolean;
|
builtin: boolean;
|
||||||
|
plugin?: Plugin[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MASK_STATE = {
|
export const DEFAULT_MASK_STATE = {
|
||||||
@@ -37,6 +38,7 @@ export const createEmptyMask = () =>
|
|||||||
lang: getLang(),
|
lang: getLang(),
|
||||||
builtin: false,
|
builtin: false,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
plugin: [Plugin.Artifacts],
|
||||||
}) as Mask;
|
}) as Mask;
|
||||||
|
|
||||||
export const useMaskStore = createPersistStore(
|
export const useMaskStore = createPersistStore(
|
||||||
|
@@ -154,7 +154,7 @@ export const usePromptStore = createPersistStore(
|
|||||||
fetch(PROMPT_URL)
|
fetch(PROMPT_URL)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
let fetchPrompts = [res.en, res.cn];
|
let fetchPrompts = [res.en, res.tw, res.cn];
|
||||||
if (getLang() === "cn") {
|
if (getLang() === "cn") {
|
||||||
fetchPrompts = fetchPrompts.reverse();
|
fetchPrompts = fetchPrompts.reverse();
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,8 @@ export const usePromptStore = createPersistStore(
|
|||||||
const allPromptsForSearch = builtinPrompts
|
const allPromptsForSearch = builtinPrompts
|
||||||
.reduce((pre, cur) => pre.concat(cur), [])
|
.reduce((pre, cur) => pre.concat(cur), [])
|
||||||
.filter((v) => !!v.title && !!v.content);
|
.filter((v) => !!v.title && !!v.content);
|
||||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
SearchService.count.builtin =
|
||||||
|
res.en.length + res.cn.length + res.tw.length;
|
||||||
SearchService.init(allPromptsForSearch, userPrompts);
|
SearchService.init(allPromptsForSearch, userPrompts);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
163
app/store/sd.ts
Normal file
163
app/store/sd.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import {
|
||||||
|
Stability,
|
||||||
|
StoreKey,
|
||||||
|
ACCESS_CODE_PREFIX,
|
||||||
|
ApiPath,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { getBearerToken } from "@/app/client/api";
|
||||||
|
import { createPersistStore } from "@/app/utils/store";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { uploadImage, base64Image2Blob } from "@/app/utils/chat";
|
||||||
|
import { models, getModelParamBasicData } from "@/app/components/sd/sd-panel";
|
||||||
|
import { useAccessStore } from "./access";
|
||||||
|
|
||||||
|
const defaultModel = {
|
||||||
|
name: models[0].name,
|
||||||
|
value: models[0].value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultParams = getModelParamBasicData(models[0].params({}), {});
|
||||||
|
|
||||||
|
const DEFAULT_SD_STATE = {
|
||||||
|
currentId: 0,
|
||||||
|
draw: [],
|
||||||
|
currentModel: defaultModel,
|
||||||
|
currentParams: defaultParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSdStore = createPersistStore<
|
||||||
|
{
|
||||||
|
currentId: number;
|
||||||
|
draw: any[];
|
||||||
|
currentModel: typeof defaultModel;
|
||||||
|
currentParams: any;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getNextId: () => number;
|
||||||
|
sendTask: (data: any, okCall?: Function) => void;
|
||||||
|
updateDraw: (draw: any) => void;
|
||||||
|
setCurrentModel: (model: any) => void;
|
||||||
|
setCurrentParams: (data: any) => void;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
DEFAULT_SD_STATE,
|
||||||
|
(set, _get) => {
|
||||||
|
function get() {
|
||||||
|
return {
|
||||||
|
..._get(),
|
||||||
|
...methods,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
getNextId() {
|
||||||
|
const id = ++_get().currentId;
|
||||||
|
set({ currentId: id });
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
sendTask(data: any, okCall?: Function) {
|
||||||
|
data = { ...data, id: nanoid(), status: "running" };
|
||||||
|
set({ draw: [data, ..._get().draw] });
|
||||||
|
this.getNextId();
|
||||||
|
this.stabilityRequestCall(data);
|
||||||
|
okCall?.();
|
||||||
|
},
|
||||||
|
stabilityRequestCall(data: any) {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
let prefix: string = ApiPath.Stability as string;
|
||||||
|
let bearerToken = "";
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
prefix = accessStore.stabilityUrl || (ApiPath.Stability as string);
|
||||||
|
bearerToken = getBearerToken(accessStore.stabilityApiKey);
|
||||||
|
}
|
||||||
|
if (!bearerToken && accessStore.enabledAccessControl()) {
|
||||||
|
bearerToken = getBearerToken(
|
||||||
|
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: bearerToken,
|
||||||
|
};
|
||||||
|
const path = `${prefix}/${Stability.GeneratePath}/${data.model}`;
|
||||||
|
const formData = new FormData();
|
||||||
|
for (let paramsKey in data.params) {
|
||||||
|
formData.append(paramsKey, data.params[paramsKey]);
|
||||||
|
}
|
||||||
|
fetch(path, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((resData) => {
|
||||||
|
if (resData.errors && resData.errors.length > 0) {
|
||||||
|
this.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "error",
|
||||||
|
error: resData.errors[0],
|
||||||
|
});
|
||||||
|
this.getNextId();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const self = this;
|
||||||
|
if (resData.finish_reason === "SUCCESS") {
|
||||||
|
uploadImage(base64Image2Blob(resData.image, "image/png"))
|
||||||
|
.then((img_data) => {
|
||||||
|
console.debug("uploadImage success", img_data, self);
|
||||||
|
self.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "success",
|
||||||
|
img_data,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("uploadImage error", e);
|
||||||
|
self.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "error",
|
||||||
|
error: JSON.stringify(e),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "error",
|
||||||
|
error: JSON.stringify(resData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.getNextId();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.updateDraw({ ...data, status: "error", error: error.message });
|
||||||
|
console.error("Error:", error);
|
||||||
|
this.getNextId();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateDraw(_draw: any) {
|
||||||
|
const draw = _get().draw || [];
|
||||||
|
draw.some((item, index) => {
|
||||||
|
if (item.id === _draw.id) {
|
||||||
|
draw[index] = _draw;
|
||||||
|
set(() => ({ draw }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCurrentModel(model: any) {
|
||||||
|
set({ currentModel: model });
|
||||||
|
},
|
||||||
|
setCurrentParams(data: any) {
|
||||||
|
set({
|
||||||
|
currentParams: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return methods;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: StoreKey.SdList,
|
||||||
|
version: 1.0,
|
||||||
|
},
|
||||||
|
);
|
@@ -118,7 +118,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
--bar-width: 5px;
|
--bar-width: 10px;
|
||||||
width: var(--bar-width);
|
width: var(--bar-width);
|
||||||
height: var(--bar-width);
|
height: var(--bar-width);
|
||||||
}
|
}
|
||||||
|
@@ -7,3 +7,5 @@ export interface RequestMessage {
|
|||||||
role: MessageRole;
|
role: MessageRole;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DalleSize = "1024x1024" | "1792x1024" | "1024x1792";
|
||||||
|
@@ -194,6 +194,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
|||||||
measureDom.style.width = width + "px";
|
measureDom.style.width = width + "px";
|
||||||
measureDom.innerText = dom.value !== "" ? dom.value : "1";
|
measureDom.innerText = dom.value !== "" ? dom.value : "1";
|
||||||
measureDom.style.fontSize = dom.style.fontSize;
|
measureDom.style.fontSize = dom.style.fontSize;
|
||||||
|
measureDom.style.fontFamily = dom.style.fontFamily;
|
||||||
const endWithEmptyLine = dom.value.endsWith("\n");
|
const endWithEmptyLine = dom.value.endsWith("\n");
|
||||||
const height = parseFloat(window.getComputedStyle(measureDom).height);
|
const height = parseFloat(window.getComputedStyle(measureDom).height);
|
||||||
const singleLineHeight = parseFloat(
|
const singleLineHeight = parseFloat(
|
||||||
@@ -256,6 +257,7 @@ export function isVisionModel(model: string) {
|
|||||||
"gemini-1.5-pro",
|
"gemini-1.5-pro",
|
||||||
"gemini-1.5-flash",
|
"gemini-1.5-flash",
|
||||||
"gpt-4o",
|
"gpt-4o",
|
||||||
|
"gpt-4o-mini",
|
||||||
];
|
];
|
||||||
const isGpt4Turbo =
|
const isGpt4Turbo =
|
||||||
model.includes("gpt-4-turbo") && !model.includes("preview");
|
model.includes("gpt-4-turbo") && !model.includes("preview");
|
||||||
@@ -264,3 +266,7 @@ export function isVisionModel(model: string) {
|
|||||||
visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
|
visionKeywords.some((keyword) => model.includes(keyword)) || isGpt4Turbo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDalle3(model: string) {
|
||||||
|
return "dall-e-3" === model;
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import heic2any from "heic2any";
|
import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant";
|
||||||
|
import { RequestMessage } from "@/app/client/api";
|
||||||
|
|
||||||
export function compressImage(file: File, maxSize: number): Promise<string> {
|
export function compressImage(file: Blob, maxSize: number): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (readerEvent: any) => {
|
reader.onload = (readerEvent: any) => {
|
||||||
@@ -40,15 +41,104 @@ export function compressImage(file: File, maxSize: number): Promise<string> {
|
|||||||
reader.onerror = reject;
|
reader.onerror = reject;
|
||||||
|
|
||||||
if (file.type.includes("heic")) {
|
if (file.type.includes("heic")) {
|
||||||
heic2any({ blob: file, toType: "image/jpeg" })
|
try {
|
||||||
.then((blob) => {
|
const heic2any = require("heic2any");
|
||||||
reader.readAsDataURL(blob as Blob);
|
heic2any({ blob: file, toType: "image/jpeg" })
|
||||||
})
|
.then((blob: Blob) => {
|
||||||
.catch((e) => {
|
reader.readAsDataURL(blob);
|
||||||
reject(e);
|
})
|
||||||
});
|
.catch((e: any) => {
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function preProcessImageContent(
|
||||||
|
content: RequestMessage["content"],
|
||||||
|
) {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
const result = [];
|
||||||
|
for (const part of content) {
|
||||||
|
if (part?.type == "image_url" && part?.image_url?.url) {
|
||||||
|
try {
|
||||||
|
const url = await cacheImageToBase64Image(part?.image_url?.url);
|
||||||
|
result.push({ type: part.type, image_url: { url } });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing image URL:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push({ ...part });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageCaches: Record<string, string> = {};
|
||||||
|
export function cacheImageToBase64Image(imageUrl: string) {
|
||||||
|
if (imageUrl.includes(CACHE_URL_PREFIX)) {
|
||||||
|
if (!imageCaches[imageUrl]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
return fetch(imageUrl, {
|
||||||
|
method: "GET",
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.then(
|
||||||
|
async (blob) =>
|
||||||
|
(imageCaches[imageUrl] = await compressImage(blob, 256 * 1024)),
|
||||||
|
); // compressImage
|
||||||
|
}
|
||||||
|
return Promise.resolve(imageCaches[imageUrl]);
|
||||||
|
}
|
||||||
|
return Promise.resolve(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64Image2Blob(base64Data: string, contentType: string) {
|
||||||
|
const byteCharacters = atob(base64Data);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
return new Blob([byteArray], { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadImage(file: Blob): Promise<string> {
|
||||||
|
if (!window._SW_ENABLED) {
|
||||||
|
// if serviceWorker register error, using compressImage
|
||||||
|
return compressImage(file, 256 * 1024);
|
||||||
|
}
|
||||||
|
const body = new FormData();
|
||||||
|
body.append("file", file);
|
||||||
|
return fetch(UPLOAD_URL, {
|
||||||
|
method: "post",
|
||||||
|
body,
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
console.log("res", res);
|
||||||
|
if (res?.code == 0 && res?.data) {
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
throw Error(`upload Error: ${res?.msg}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeImage(imageUrl: string) {
|
||||||
|
return fetch(imageUrl, {
|
||||||
|
method: "DELETE",
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -14,8 +14,8 @@ export function createWebDavClient(store: SyncStore) {
|
|||||||
return {
|
return {
|
||||||
async check() {
|
async check() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(this.path(folder, proxyUrl), {
|
const res = await fetch(this.path(folder, proxyUrl, "MKCOL"), {
|
||||||
method: "MKCOL",
|
method: "GET",
|
||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
});
|
});
|
||||||
const success = [201, 200, 404, 405, 301, 302, 307, 308].includes(
|
const success = [201, 200, 404, 405, 301, 302, 307, 308].includes(
|
||||||
@@ -42,6 +42,10 @@ export function createWebDavClient(store: SyncStore) {
|
|||||||
|
|
||||||
console.log("[WebDav] get key = ", key, res.status, res.statusText);
|
console.log("[WebDav] get key = ", key, res.status, res.statusText);
|
||||||
|
|
||||||
|
if (404 == res.status) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
return await res.text();
|
return await res.text();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@ export function createWebDavClient(store: SyncStore) {
|
|||||||
authorization: `Basic ${auth}`,
|
authorization: `Basic ${auth}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
path(path: string, proxyUrl: string = "") {
|
path(path: string, proxyUrl: string = "", proxyMethod: string = "") {
|
||||||
if (path.startsWith("/")) {
|
if (path.startsWith("/")) {
|
||||||
path = path.slice(1);
|
path = path.slice(1);
|
||||||
}
|
}
|
||||||
@@ -78,9 +82,13 @@ export function createWebDavClient(store: SyncStore) {
|
|||||||
let u = new URL(proxyUrl + pathPrefix + path);
|
let u = new URL(proxyUrl + pathPrefix + path);
|
||||||
// add query params
|
// add query params
|
||||||
u.searchParams.append("endpoint", config.endpoint);
|
u.searchParams.append("endpoint", config.endpoint);
|
||||||
|
proxyMethod && u.searchParams.append("proxy_method", proxyMethod);
|
||||||
url = u.toString();
|
url = u.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
url = pathPrefix + path + "?endpoint=" + config.endpoint;
|
url = pathPrefix + path + "?endpoint=" + config.endpoint;
|
||||||
|
if (proxyMethod) {
|
||||||
|
url += "&proxy_method=" + proxyMethod;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
|
246
app/utils/hmac.ts
Normal file
246
app/utils/hmac.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// From https://gist.github.com/guillermodlpa/f6d955f838e9b10d1ef95b8e259b2c58
|
||||||
|
// From https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8
|
||||||
|
|
||||||
|
// To ensure cross-browser support even without a proper SubtleCrypto
|
||||||
|
// impelmentation (or without access to the impelmentation, as is the case with
|
||||||
|
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
|
||||||
|
// HMAC signatures using nothing but raw JavaScript
|
||||||
|
|
||||||
|
/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */
|
||||||
|
|
||||||
|
// By giving internal functions names that we can mangle, future calls to
|
||||||
|
// them are reduced to a single byte (minor space savings in minified file)
|
||||||
|
const uint8Array = Uint8Array;
|
||||||
|
const uint32Array = Uint32Array;
|
||||||
|
const pow = Math.pow;
|
||||||
|
|
||||||
|
// Will be initialized below
|
||||||
|
// Using a Uint32Array instead of a simple array makes the minified code
|
||||||
|
// a bit bigger (we lose our `unshift()` hack), but comes with huge
|
||||||
|
// performance gains
|
||||||
|
const DEFAULT_STATE = new uint32Array(8);
|
||||||
|
const ROUND_CONSTANTS: number[] = [];
|
||||||
|
|
||||||
|
// Reusable object for expanded message
|
||||||
|
// Using a Uint32Array instead of a simple array makes the minified code
|
||||||
|
// 7 bytes larger, but comes with huge performance gains
|
||||||
|
const M = new uint32Array(64);
|
||||||
|
|
||||||
|
// After minification the code to compute the default state and round
|
||||||
|
// constants is smaller than the output. More importantly, this serves as a
|
||||||
|
// good educational aide for anyone wondering where the magic numbers come
|
||||||
|
// from. No magic numbers FTW!
|
||||||
|
function getFractionalBits(n: number) {
|
||||||
|
return ((n - (n | 0)) * pow(2, 32)) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = 2;
|
||||||
|
let nPrime = 0;
|
||||||
|
while (nPrime < 64) {
|
||||||
|
// isPrime() was in-lined from its original function form to save
|
||||||
|
// a few bytes
|
||||||
|
let isPrime = true;
|
||||||
|
// Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
|
||||||
|
// var sqrtN = pow(n, 1 / 2);
|
||||||
|
// So technically to determine if a number is prime you only need to
|
||||||
|
// check numbers up to the square root. However this function only runs
|
||||||
|
// once and we're only computing the first 64 primes (up to 311), so on
|
||||||
|
// any modern CPU this whole function runs in a couple milliseconds.
|
||||||
|
// By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
|
||||||
|
// scaling performance cost
|
||||||
|
for (let factor = 2; factor <= n / 2; factor++) {
|
||||||
|
if (n % factor === 0) {
|
||||||
|
isPrime = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isPrime) {
|
||||||
|
if (nPrime < 8) {
|
||||||
|
DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
|
||||||
|
}
|
||||||
|
ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));
|
||||||
|
|
||||||
|
nPrime++;
|
||||||
|
}
|
||||||
|
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cross-platform support we need to ensure that all 32-bit words are
|
||||||
|
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
|
||||||
|
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
|
||||||
|
// if our system is LittleEndian (which is about 99% of CPUs)
|
||||||
|
const LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];
|
||||||
|
|
||||||
|
function convertEndian(word: number) {
|
||||||
|
if (LittleEndian) {
|
||||||
|
return (
|
||||||
|
// byte 1 -> byte 4
|
||||||
|
(word >>> 24) |
|
||||||
|
// byte 2 -> byte 3
|
||||||
|
(((word >>> 16) & 0xff) << 8) |
|
||||||
|
// byte 3 -> byte 2
|
||||||
|
((word & 0xff00) << 8) |
|
||||||
|
// byte 4 -> byte 1
|
||||||
|
(word << 24)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rightRotate(word: number, bits: number) {
|
||||||
|
return (word >>> bits) | (word << (32 - bits));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(data: Uint8Array) {
|
||||||
|
// Copy default state
|
||||||
|
const STATE = DEFAULT_STATE.slice();
|
||||||
|
|
||||||
|
// Caching this reduces occurrences of ".length" in minified JavaScript
|
||||||
|
// 3 more byte savings! :D
|
||||||
|
const legth = data.length;
|
||||||
|
|
||||||
|
// Pad data
|
||||||
|
const bitLength = legth * 8;
|
||||||
|
const newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65;
|
||||||
|
|
||||||
|
// "bytes" and "words" are stored BigEndian
|
||||||
|
const bytes = new uint8Array(newBitLength / 8);
|
||||||
|
const words = new uint32Array(bytes.buffer);
|
||||||
|
|
||||||
|
bytes.set(data, 0);
|
||||||
|
// Append a 1
|
||||||
|
bytes[legth] = 0b10000000;
|
||||||
|
// Store length in BigEndian
|
||||||
|
words[words.length - 1] = convertEndian(bitLength);
|
||||||
|
|
||||||
|
// Loop iterator (avoid two instances of "var") -- saves 2 bytes
|
||||||
|
let round;
|
||||||
|
|
||||||
|
// Process blocks (512 bits / 64 bytes / 16 words at a time)
|
||||||
|
for (let block = 0; block < newBitLength / 32; block += 16) {
|
||||||
|
const workingState = STATE.slice();
|
||||||
|
|
||||||
|
// Rounds
|
||||||
|
for (round = 0; round < 64; round++) {
|
||||||
|
let MRound;
|
||||||
|
// Expand message
|
||||||
|
if (round < 16) {
|
||||||
|
// Convert to platform Endianness for later math
|
||||||
|
MRound = convertEndian(words[block + round]);
|
||||||
|
} else {
|
||||||
|
const gamma0x = M[round - 15];
|
||||||
|
const gamma1x = M[round - 2];
|
||||||
|
MRound =
|
||||||
|
M[round - 7] +
|
||||||
|
M[round - 16] +
|
||||||
|
(rightRotate(gamma0x, 7) ^
|
||||||
|
rightRotate(gamma0x, 18) ^
|
||||||
|
(gamma0x >>> 3)) +
|
||||||
|
(rightRotate(gamma1x, 17) ^
|
||||||
|
rightRotate(gamma1x, 19) ^
|
||||||
|
(gamma1x >>> 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// M array matches platform endianness
|
||||||
|
M[round] = MRound |= 0;
|
||||||
|
|
||||||
|
// Computation
|
||||||
|
const t1 =
|
||||||
|
(rightRotate(workingState[4], 6) ^
|
||||||
|
rightRotate(workingState[4], 11) ^
|
||||||
|
rightRotate(workingState[4], 25)) +
|
||||||
|
((workingState[4] & workingState[5]) ^
|
||||||
|
(~workingState[4] & workingState[6])) +
|
||||||
|
workingState[7] +
|
||||||
|
MRound +
|
||||||
|
ROUND_CONSTANTS[round];
|
||||||
|
const t2 =
|
||||||
|
(rightRotate(workingState[0], 2) ^
|
||||||
|
rightRotate(workingState[0], 13) ^
|
||||||
|
rightRotate(workingState[0], 22)) +
|
||||||
|
((workingState[0] & workingState[1]) ^
|
||||||
|
(workingState[2] & (workingState[0] ^ workingState[1])));
|
||||||
|
for (let i = 7; i > 0; i--) {
|
||||||
|
workingState[i] = workingState[i - 1];
|
||||||
|
}
|
||||||
|
workingState[0] = (t1 + t2) | 0;
|
||||||
|
workingState[4] = (workingState[4] + t1) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
for (round = 0; round < 8; round++) {
|
||||||
|
STATE[round] = (STATE[round] + workingState[round]) | 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally the state needs to be converted to BigEndian for output
|
||||||
|
// And we want to return a Uint8Array, not a Uint32Array
|
||||||
|
return new uint8Array(
|
||||||
|
new uint32Array(
|
||||||
|
STATE.map(function (val) {
|
||||||
|
return convertEndian(val);
|
||||||
|
}),
|
||||||
|
).buffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hmac(key: Uint8Array, data: ArrayLike<number>) {
|
||||||
|
if (key.length > 64) key = sha256(key);
|
||||||
|
|
||||||
|
if (key.length < 64) {
|
||||||
|
const tmp = new Uint8Array(64);
|
||||||
|
tmp.set(key, 0);
|
||||||
|
key = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate inner and outer keys
|
||||||
|
const innerKey = new Uint8Array(64);
|
||||||
|
const outerKey = new Uint8Array(64);
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
innerKey[i] = 0x36 ^ key[i];
|
||||||
|
outerKey[i] = 0x5c ^ key[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the innerKey
|
||||||
|
const msg = new Uint8Array(data.length + 64);
|
||||||
|
msg.set(innerKey, 0);
|
||||||
|
msg.set(data, 64);
|
||||||
|
|
||||||
|
// Has the previous message and append the outerKey
|
||||||
|
const result = new Uint8Array(64 + 32);
|
||||||
|
result.set(outerKey, 0);
|
||||||
|
result.set(sha256(msg), 64);
|
||||||
|
|
||||||
|
// Hash the previous message
|
||||||
|
return sha256(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a string to a Uint8Array, SHA-256 it, and convert back to string
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
export function sign(
|
||||||
|
inputKey: string | Uint8Array,
|
||||||
|
inputData: string | Uint8Array,
|
||||||
|
) {
|
||||||
|
const key =
|
||||||
|
typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey;
|
||||||
|
const data =
|
||||||
|
typeof inputData === "string" ? encoder.encode(inputData) : inputData;
|
||||||
|
return hmac(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hex(bin: Uint8Array) {
|
||||||
|
return bin.reduce((acc, val) => {
|
||||||
|
const hexVal = "00" + val.toString(16);
|
||||||
|
return acc + hexVal.substring(hexVal.length - 2);
|
||||||
|
}, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hash(str: string) {
|
||||||
|
return hex(sha256(encoder.encode(str)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashWithSecret(str: string, secret: string) {
|
||||||
|
return hex(sign(secret, str)).toString();
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useAccessStore, useAppConfig } from "../store";
|
import { useAccessStore, useAppConfig } from "../store";
|
||||||
import { collectModels, collectModelsWithDefaultModel } from "./model";
|
import { collectModelsWithDefaultModel } from "./model";
|
||||||
|
|
||||||
export function useAllModels() {
|
export function useAllModels() {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
|
@@ -1,12 +1,42 @@
|
|||||||
import { DEFAULT_MODELS } from "../constant";
|
import { DEFAULT_MODELS } from "../constant";
|
||||||
import { LLMModel } from "../client/api";
|
import { LLMModel } from "../client/api";
|
||||||
|
|
||||||
|
const CustomSeq = {
|
||||||
|
val: -1000, //To ensure the custom model located at front, start from -1000, refer to constant.ts
|
||||||
|
cache: new Map<string, number>(),
|
||||||
|
next: (id: string) => {
|
||||||
|
if (CustomSeq.cache.has(id)) {
|
||||||
|
return CustomSeq.cache.get(id) as number;
|
||||||
|
} else {
|
||||||
|
let seq = CustomSeq.val++;
|
||||||
|
CustomSeq.cache.set(id, seq);
|
||||||
|
return seq;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const customProvider = (providerName: string) => ({
|
const customProvider = (providerName: string) => ({
|
||||||
id: providerName.toLowerCase(),
|
id: providerName.toLowerCase(),
|
||||||
providerName: providerName,
|
providerName: providerName,
|
||||||
providerType: "custom",
|
providerType: "custom",
|
||||||
|
sorted: CustomSeq.next(providerName),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts an array of models based on specified rules.
|
||||||
|
*
|
||||||
|
* First, sorted by provider; if the same, sorted by model
|
||||||
|
*/
|
||||||
|
const sortModelTable = (models: ReturnType<typeof collectModels>) =>
|
||||||
|
models.sort((a, b) => {
|
||||||
|
if (a.provider && b.provider) {
|
||||||
|
let cmp = a.provider.sorted - b.provider.sorted;
|
||||||
|
return cmp === 0 ? a.sorted - b.sorted : cmp;
|
||||||
|
} else {
|
||||||
|
return a.sorted - b.sorted;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function collectModelTable(
|
export function collectModelTable(
|
||||||
models: readonly LLMModel[],
|
models: readonly LLMModel[],
|
||||||
customModels: string,
|
customModels: string,
|
||||||
@@ -17,6 +47,7 @@ export function collectModelTable(
|
|||||||
available: boolean;
|
available: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
sorted: number;
|
||||||
provider?: LLMModel["provider"]; // Marked as optional
|
provider?: LLMModel["provider"]; // Marked as optional
|
||||||
isDefault?: boolean;
|
isDefault?: boolean;
|
||||||
}
|
}
|
||||||
@@ -84,6 +115,7 @@ export function collectModelTable(
|
|||||||
displayName: displayName || customModelName,
|
displayName: displayName || customModelName,
|
||||||
available,
|
available,
|
||||||
provider, // Use optional chaining
|
provider, // Use optional chaining
|
||||||
|
sorted: CustomSeq.next(`${customModelName}@${provider?.id}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,12 +131,21 @@ export function collectModelTableWithDefaultModel(
|
|||||||
) {
|
) {
|
||||||
let modelTable = collectModelTable(models, customModels);
|
let modelTable = collectModelTable(models, customModels);
|
||||||
if (defaultModel && defaultModel !== "") {
|
if (defaultModel && defaultModel !== "") {
|
||||||
modelTable[defaultModel] = {
|
if (defaultModel.includes("@")) {
|
||||||
...modelTable[defaultModel],
|
if (defaultModel in modelTable) {
|
||||||
name: defaultModel,
|
modelTable[defaultModel].isDefault = true;
|
||||||
available: true,
|
}
|
||||||
isDefault: true,
|
} else {
|
||||||
};
|
for (const key of Object.keys(modelTable)) {
|
||||||
|
if (
|
||||||
|
modelTable[key].available &&
|
||||||
|
key.split("@").shift() == defaultModel
|
||||||
|
) {
|
||||||
|
modelTable[key].isDefault = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return modelTable;
|
return modelTable;
|
||||||
}
|
}
|
||||||
@@ -117,7 +158,9 @@ export function collectModels(
|
|||||||
customModels: string,
|
customModels: string,
|
||||||
) {
|
) {
|
||||||
const modelTable = collectModelTable(models, customModels);
|
const modelTable = collectModelTable(models, customModels);
|
||||||
const allModels = Object.values(modelTable);
|
let allModels = Object.values(modelTable);
|
||||||
|
|
||||||
|
allModels = sortModelTable(allModels);
|
||||||
|
|
||||||
return allModels;
|
return allModels;
|
||||||
}
|
}
|
||||||
@@ -132,7 +175,10 @@ export function collectModelsWithDefaultModel(
|
|||||||
customModels,
|
customModels,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
);
|
);
|
||||||
const allModels = Object.values(modelTable);
|
let allModels = Object.values(modelTable);
|
||||||
|
|
||||||
|
allModels = sortModelTable(allModels);
|
||||||
|
|
||||||
return allModels;
|
return allModels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
102
app/utils/tencent.ts
Normal file
102
app/utils/tencent.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { sign, hash as getHash, hex } from "./hmac";
|
||||||
|
|
||||||
|
// 使用 SHA-256 和 secret 进行 HMAC 加密
|
||||||
|
function sha256(message: any, secret: any, encoding?: string) {
|
||||||
|
const result = sign(secret, message);
|
||||||
|
return encoding == "hex" ? hex(result).toString() : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDate(timestamp: number) {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
const month = ("0" + (date.getUTCMonth() + 1)).slice(-2);
|
||||||
|
const day = ("0" + date.getUTCDate()).slice(-2);
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHeader(
|
||||||
|
payload: any,
|
||||||
|
SECRET_ID: string,
|
||||||
|
SECRET_KEY: string,
|
||||||
|
) {
|
||||||
|
// https://cloud.tencent.com/document/api/1729/105701
|
||||||
|
|
||||||
|
const endpoint = "hunyuan.tencentcloudapi.com";
|
||||||
|
const service = "hunyuan";
|
||||||
|
const region = ""; // optional
|
||||||
|
const action = "ChatCompletions";
|
||||||
|
const version = "2023-09-01";
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
//时间处理, 获取世界时间日期
|
||||||
|
const date = getDate(timestamp);
|
||||||
|
|
||||||
|
// ************* 步骤 1:拼接规范请求串 *************
|
||||||
|
|
||||||
|
const hashedRequestPayload = getHash(payload);
|
||||||
|
const httpRequestMethod = "POST";
|
||||||
|
const contentType = "application/json";
|
||||||
|
const canonicalUri = "/";
|
||||||
|
const canonicalQueryString = "";
|
||||||
|
const canonicalHeaders =
|
||||||
|
`content-type:${contentType}\n` +
|
||||||
|
"host:" +
|
||||||
|
endpoint +
|
||||||
|
"\n" +
|
||||||
|
"x-tc-action:" +
|
||||||
|
action.toLowerCase() +
|
||||||
|
"\n";
|
||||||
|
const signedHeaders = "content-type;host;x-tc-action";
|
||||||
|
|
||||||
|
const canonicalRequest = [
|
||||||
|
httpRequestMethod,
|
||||||
|
canonicalUri,
|
||||||
|
canonicalQueryString,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
hashedRequestPayload,
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
// ************* 步骤 2:拼接待签名字符串 *************
|
||||||
|
const algorithm = "TC3-HMAC-SHA256";
|
||||||
|
const hashedCanonicalRequest = getHash(canonicalRequest);
|
||||||
|
const credentialScope = date + "/" + service + "/" + "tc3_request";
|
||||||
|
const stringToSign =
|
||||||
|
algorithm +
|
||||||
|
"\n" +
|
||||||
|
timestamp +
|
||||||
|
"\n" +
|
||||||
|
credentialScope +
|
||||||
|
"\n" +
|
||||||
|
hashedCanonicalRequest;
|
||||||
|
|
||||||
|
// ************* 步骤 3:计算签名 *************
|
||||||
|
const kDate = sha256(date, "TC3" + SECRET_KEY);
|
||||||
|
const kService = sha256(service, kDate);
|
||||||
|
const kSigning = sha256("tc3_request", kService);
|
||||||
|
const signature = sha256(stringToSign, kSigning, "hex");
|
||||||
|
|
||||||
|
// ************* 步骤 4:拼接 Authorization *************
|
||||||
|
const authorization =
|
||||||
|
algorithm +
|
||||||
|
" " +
|
||||||
|
"Credential=" +
|
||||||
|
SECRET_ID +
|
||||||
|
"/" +
|
||||||
|
credentialScope +
|
||||||
|
", " +
|
||||||
|
"SignedHeaders=" +
|
||||||
|
signedHeaders +
|
||||||
|
", " +
|
||||||
|
"Signature=" +
|
||||||
|
signature;
|
||||||
|
|
||||||
|
return {
|
||||||
|
Authorization: authorization,
|
||||||
|
"Content-Type": contentType,
|
||||||
|
Host: endpoint,
|
||||||
|
"X-TC-Action": action,
|
||||||
|
"X-TC-Timestamp": timestamp.toString(),
|
||||||
|
"X-TC-Version": version,
|
||||||
|
"X-TC-Region": region,
|
||||||
|
};
|
||||||
|
}
|
47
docs/images/ent.svg
Normal file
47
docs/images/ent.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 413 KiB |
11
package.json
11
package.json
@@ -4,14 +4,14 @@
|
|||||||
"license": "mit",
|
"license": "mit",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"mask": "npx tsx app/masks/build.ts",
|
"mask": "npx tsx app/masks/build.ts",
|
||||||
"mask:watch": "npx watch 'yarn mask' app/masks",
|
"mask:watch": "npx watch \"yarn mask\" app/masks",
|
||||||
"dev": "yarn run mask:watch & next dev",
|
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
|
||||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||||
"export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev",
|
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||||
"app:dev": "yarn mask:watch & yarn tauri dev",
|
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||||
"app:build": "yarn mask && yarn tauri build",
|
"app:build": "yarn mask && yarn tauri build",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"mermaid": "^10.6.1",
|
"mermaid": "^10.6.1",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "^14.1.1",
|
"next": "^14.1.1",
|
||||||
@@ -48,11 +49,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.30",
|
||||||
"@types/react": "^18.2.70",
|
"@types/react": "^18.2.70",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.4",
|
"@types/spark-md5": "^3.0.4",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-next": "13.4.19",
|
"eslint-config-next": "13.4.19",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user