mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-31 03:09:04 +08:00
Compare commits
414 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
571b6fdef7 | ||
|
3bc771a0d9 | ||
|
bdc25adb41 | ||
|
7ec29c2eea | ||
|
b9995e7f70 | ||
|
31d9d2efcd | ||
|
0cf6614186 | ||
|
ddc16cc2be | ||
|
616f0b3e73 | ||
|
bd1056b13f | ||
|
cc86923fd5 | ||
|
916d764477 | ||
|
4a21a8fdae | ||
|
49df5bd590 | ||
|
9eaf492d5b | ||
|
a80502f7db | ||
|
eade013138 | ||
|
912254751a | ||
|
c3c6f7f7ae | ||
|
5cf58d9446 | ||
|
3ba598633c | ||
|
406530ca69 | ||
|
f8b963df6d | ||
|
d17000975f | ||
|
64a8ba6212 | ||
|
350ddd2af1 | ||
|
c03abdac42 | ||
|
bb3cc2c821 | ||
|
0b814eff83 | ||
|
a91ac91977 | ||
|
422d70d928 | ||
|
a4cb859472 | ||
|
19137b79bc | ||
|
bd1e311674 | ||
|
c127db335a | ||
|
f5ed1604aa | ||
|
771cc9e583 | ||
|
7ad1e246a0 | ||
|
5c638251f8 | ||
|
0d189dc78e | ||
|
a9d7253143 | ||
|
199f29e63c | ||
|
c82efceae0 | ||
|
cad461b121 | ||
|
5af68ac545 | ||
|
c0ce70ccc7 | ||
|
753c518d33 | ||
|
b9ca6694f7 | ||
|
0c116251b1 | ||
|
e9def2cdc5 | ||
|
8ca525dc7a | ||
|
281fe6927a | ||
|
35471a41c8 | ||
|
bda3098a31 | ||
|
1e05eb1d60 | ||
|
6369b902bf | ||
|
aea794e522 | ||
|
7c3dfb7bae | ||
|
75057f9a91 | ||
|
7026bd926a | ||
|
ae19a0dc5f | ||
|
7d9a2132cb | ||
|
20f2f61349 | ||
|
4169431f2c | ||
|
45798f993d | ||
|
ae0d68c27e | ||
|
070b191ffe | ||
|
778e88cb56 | ||
|
d1fcfa05db | ||
|
f4754dc5fc | ||
|
0f885e73c3 | ||
|
75acd4c1aa | ||
|
3ef0621eb0 | ||
|
b1db9ead0d | ||
|
9cc6000d89 | ||
|
42515899d9 | ||
|
1d096eeb17 | ||
|
bc5b8f0ff9 | ||
|
bf412fd309 | ||
|
6e1f4246b8 | ||
|
ed88619623 | ||
|
37b8922348 | ||
|
4a23a0064b | ||
|
e012b1e5d1 | ||
|
2bc84ec908 | ||
|
8efe26ed57 | ||
|
24d7dc9bf9 | ||
|
4b7139d9ae | ||
|
1f35624121 | ||
|
860c5b8f20 | ||
|
a454c39ca0 | ||
|
6938910f08 | ||
|
2c63dde6c5 | ||
|
e69d20a209 | ||
|
0b731edd21 | ||
|
efdd61595e | ||
|
d676f88fed | ||
|
07f2cf7ad0 | ||
|
bcb520ed3b | ||
|
93cee18300 | ||
|
cae4655785 | ||
|
28c12606a4 | ||
|
54df355014 | ||
|
cf50299b14 | ||
|
2c12be62c4 | ||
|
4636a75b5e | ||
|
03756e364a | ||
|
ce1715c79e | ||
|
a62ab3c649 | ||
|
bfb7b988f4 | ||
|
84f41262f5 | ||
|
dda40e29f4 | ||
|
7df868e22a | ||
|
bf5e7aaa48 | ||
|
d76e744eab | ||
|
6f5699fe09 | ||
|
f9d916925e | ||
|
f9258878db | ||
|
ef9e86b50d | ||
|
b21931c667 | ||
|
06de3f5e69 | ||
|
261a8fd83b | ||
|
6527074cde | ||
|
f2485931d9 | ||
|
4f8a0b7711 | ||
|
2dde55050e | ||
|
6aade62ce2 | ||
|
45b88ebb2a | ||
|
dc7159a450 | ||
|
536ace8e10 | ||
|
16b2a3e66e | ||
|
6f135a0cce | ||
|
cf220dd2eb | ||
|
914f4fb862 | ||
|
7bdb68eecf | ||
|
3c510cfaf0 | ||
|
401fa198c9 | ||
|
600df4f63a | ||
|
74eb42c111 | ||
|
9876a1aeca | ||
|
d898ffce23 | ||
|
f1772f4625 | ||
|
9da455a7ea | ||
|
2bd6342309 | ||
|
5e73577088 | ||
|
5156a80738 | ||
|
a9d605ed30 | ||
|
7d1fae32cd | ||
|
9f17b45bc4 | ||
|
a64c9dae42 | ||
|
5fbf4c394c | ||
|
1e5153173c | ||
|
011b52d07d | ||
|
d033168d80 | ||
|
fdca9e59de | ||
|
549a2fd206 | ||
|
a0cd939bfd | ||
|
4f52679ec6 | ||
|
b52e237044 | ||
|
17b329bb63 | ||
|
3a654ba199 | ||
|
5ba3fc9321 | ||
|
a46f08154e | ||
|
0f6ed9c293 | ||
|
295864d36b | ||
|
be6d45e49f | ||
|
22b6987249 | ||
|
64647b0bb3 | ||
|
a5a1f2e8ad | ||
|
8bd39f33e0 | ||
|
be9774943b | ||
|
dd6e79922a | ||
|
bf84269520 | ||
|
943214c6a7 | ||
|
ca792669fc | ||
|
78d68a9949 | ||
|
6b2db97347 | ||
|
2bfb362832 | ||
|
cb140e482f | ||
|
e6b72ac1ff | ||
|
8032e6d68d | ||
|
c7e0a6f37f | ||
|
1141cd2e6e | ||
|
c9dd953817 | ||
|
b7ffca031e | ||
|
fd2f441e02 | ||
|
3b3ebda34b | ||
|
87e3d663a2 | ||
|
d0a1d910d4 | ||
|
33b97082fa | ||
|
d93f05f511 | ||
|
3dc29197e1 | ||
|
fbc0236748 | ||
|
1304a3943a | ||
|
8c0ba1aee2 | ||
|
47154773f2 | ||
|
deca9bf06d | ||
|
6ab4c9be2e | ||
|
3a519612a0 | ||
|
d1ec26ae83 | ||
|
9cb889c34f | ||
|
77f27b7948 | ||
|
dbee5bc515 | ||
|
54a5332834 | ||
|
8af164c99f | ||
|
c7321fddfb | ||
|
c5ca278253 | ||
|
638fdd8c3e | ||
|
3407b57a51 | ||
|
836bf836d3 | ||
|
6e52d14180 | ||
|
1096fe55e1 | ||
|
d2d615c84a | ||
|
9f26c8cecb | ||
|
cd1f082c52 | ||
|
b90dfb48ee | ||
|
70e67a686b | ||
|
d1c3d900ab | ||
|
e8a4ad1001 | ||
|
ccac85b079 | ||
|
f92fefb6cd | ||
|
b799213a14 | ||
|
cc565cf57e | ||
|
4a56e9c884 | ||
|
d11d906fa2 | ||
|
65c4a0c319 | ||
|
55bcf78efe | ||
|
f5a2ce52aa | ||
|
0dcbfd746e | ||
|
06e043d3f1 | ||
|
986d34fb3e | ||
|
5296ab1b09 | ||
|
b5d3348d99 | ||
|
1e77df381a | ||
|
ec33281ff5 | ||
|
ba67f13ad5 | ||
|
1604ed91fb | ||
|
74fcaab5e9 | ||
|
adee435ea6 | ||
|
ea59ab5176 | ||
|
f78008c1b2 | ||
|
f54db695af | ||
|
f21f922160 | ||
|
fd413c7b52 | ||
|
3e2c5af4b5 | ||
|
bdb49b1171 | ||
|
5933b3d7eb | ||
|
4c8d606fae | ||
|
13c1d2fd2b | ||
|
e35c807216 | ||
|
5a2cc6f154 | ||
|
88f8c43472 | ||
|
ef3e8e6fac | ||
|
1505372e20 | ||
|
ad5093ce05 | ||
|
b558d1afc6 | ||
|
ddfd05b008 | ||
|
d2ad01a9ff | ||
|
64a17abfe2 | ||
|
04b638aa06 | ||
|
31e30906d0 | ||
|
bc00be9065 | ||
|
4a599e986f | ||
|
f1ca03e378 | ||
|
f3d5fc7a84 | ||
|
3bfcdf9c41 | ||
|
144200e315 | ||
|
398e229c77 | ||
|
6a61fe5776 | ||
|
9835206452 | ||
|
70b0580fb7 | ||
|
23eb7732d7 | ||
|
26e50cefea | ||
|
588e907181 | ||
|
eae7d6260f | ||
|
175b4e7f92 | ||
|
b050417ab1 | ||
|
37b49400db | ||
|
ebcb2e7837 | ||
|
f1e7db6a88 | ||
|
2ba0929458 | ||
|
83fed42997 | ||
|
2c4626709c | ||
|
59fbadd9eb | ||
|
adb860b464 | ||
|
8d8790586d | ||
|
61ca60c550 | ||
|
d713d01600 | ||
|
372ea0f845 | ||
|
61888708f5 | ||
|
c900459f73 | ||
|
2c92f75c86 | ||
|
3e1514239c | ||
|
0707a1d49a | ||
|
9521f19507 | ||
|
bd69116df2 | ||
|
6535986484 | ||
|
e03db9c2d5 | ||
|
038790370c | ||
|
261bf0b298 | ||
|
5a7bdcfe59 | ||
|
4f3261b262 | ||
|
48e6087b1b | ||
|
28103c901d | ||
|
368701610f | ||
|
b589f48aa9 | ||
|
74e32efa7d | ||
|
dc555b2206 | ||
|
859cf6930f | ||
|
6f83fbd212 | ||
|
a7be4780ba | ||
|
b5e89d4440 | ||
|
89bc98d26b | ||
|
31a73273ac | ||
|
2a820ea6e7 | ||
|
6484d4f0bd | ||
|
461e48be32 | ||
|
ff60ffca3e | ||
|
1bbf310c46 | ||
|
8469f448b5 | ||
|
e36abc3ac6 | ||
|
605dd72354 | ||
|
9770b65146 | ||
|
57158890c3 | ||
|
6f8f490fdd | ||
|
415e9dc913 | ||
|
1487762925 | ||
|
c73a91a0f5 | ||
|
5dced28088 | ||
|
38f6956e71 | ||
|
93da9dd1f4 | ||
|
505c8cde81 | ||
|
f7a6fa9873 | ||
|
70a2d11b8c | ||
|
2c077aca5a | ||
|
efef6b098d | ||
|
885f2a3226 | ||
|
1f94ae7888 | ||
|
05aa520669 | ||
|
b0c32159e7 | ||
|
4a182517da | ||
|
107b584085 | ||
|
6d8416f838 | ||
|
876a0cb6f7 | ||
|
4904612523 | ||
|
3bd76b9156 | ||
|
dd757e7d04 | ||
|
71d06647d0 | ||
|
0ae57589a9 | ||
|
22a6819f7b | ||
|
5e23ad2db1 | ||
|
e009ea57b9 | ||
|
63c93a42b5 | ||
|
e6cc1625b5 | ||
|
f65b0128e7 | ||
|
507b7fee56 | ||
|
925d28495a | ||
|
19dd71eb05 | ||
|
b3fd56c2c1 | ||
|
d8b6ebf6cb | ||
|
bdaac17886 | ||
|
8cac51abbe | ||
|
0113d4499b | ||
|
7562ab3c1c | ||
|
d3de07ecf3 | ||
|
b146cd889f | ||
|
bc0f184098 | ||
|
1debde3046 | ||
|
4d3fdbdd80 | ||
|
90c201d438 | ||
|
1e22abab80 | ||
|
792c37735e | ||
|
6d8ece530f | ||
|
3499dfb285 | ||
|
30c656ceda | ||
|
a1c7f86ff3 | ||
|
4952e41132 | ||
|
e1142216ec | ||
|
a4040fc1ee | ||
|
f84572443f | ||
|
fbf6bc1979 | ||
|
3aeb2c1230 | ||
|
50eb7a5f98 | ||
|
484d1e7396 | ||
|
a632e13334 | ||
|
aa3f96f89c | ||
|
16685ddb6c | ||
|
e78b15b9f0 | ||
|
35b0bd76f8 | ||
|
1d7286c161 | ||
|
5a7ec38ecd | ||
|
ada4e3cdcd | ||
|
ed62c87156 | ||
|
840277f584 | ||
|
ffddb7fb2b | ||
|
b380421fd5 | ||
|
20882a7598 | ||
|
9d3dff47d2 | ||
|
db5c7aba78 | ||
|
e8e01aa60d | ||
|
ae8226907f | ||
|
803b66ae9d | ||
|
20a508e2d6 | ||
|
25ada1eae9 | ||
|
808e4b38a3 | ||
|
a496bc5a63 | ||
|
c94713475f | ||
|
d45c61109f | ||
|
99220d72da | ||
|
887eaef1aa | ||
|
836a00e104 | ||
|
8ee5061046 | ||
|
61852b4021 | ||
|
0b7de6f7b2 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# docker-compose env files
|
||||
.env
|
||||
|
||||
*.key
|
||||
*.key.pub
|
@@ -8,6 +8,16 @@ CODE=your-password
|
||||
# You can start service behind a proxy
|
||||
PROXY_URL=http://localhost:7890
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
||||
GOOGLE_API_KEY=
|
||||
|
||||
# (optional)
|
||||
# Default: https://generativelanguage.googleapis.com/
|
||||
# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
|
||||
GOOGLE_URL=
|
||||
|
||||
# Override openai api request base url. (optional)
|
||||
# Default: https://api.openai.com
|
||||
# Examples: http://your-openai-proxy.com
|
||||
@@ -15,14 +25,8 @@ BASE_URL=
|
||||
|
||||
# Specify OpenAI organization ID.(optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to input their own API key, set this value to 1.
|
||||
OPENAI_ORG_ID=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to input their own API key, set this value to 1.
|
||||
HIDE_USER_API_KEY=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to use GPT-4, set this value to 1.
|
||||
@@ -30,5 +34,16 @@ DISABLE_GPT4=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do not want users to query balance, set this value to 1.
|
||||
HIDE_BALANCE_QUERY=
|
||||
# If you do not want users to input their own API key, set this value to 1.
|
||||
HIDE_USER_API_KEY=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you do want users to query balance, set this value to 1.
|
||||
ENABLE_BALANCE_QUERY=
|
||||
|
||||
# (optional)
|
||||
# Default: Empty
|
||||
# If you want to disable parse settings from url, set this value to 1.
|
||||
DISABLE_FAST_LINK=
|
||||
|
||||
|
4
.github/workflows/app.yml
vendored
4
.github/workflows/app.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- name: get version
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
|
||||
- name: create release
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
85
.github/workflows/deploy_preview.yml
vendored
Normal file
85
.github/workflows/deploy_preview.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
name: VercelPreviewDeployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branchName:
|
||||
description: 'Branch to deploy'
|
||||
required: true
|
||||
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_DOMAIN_SUFFIX: ".pr.nextchat.dev"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
statuses: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deploy-preview:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branchName }}
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
|
||||
id: extract_branch
|
||||
|
||||
- name: Hash branch name
|
||||
uses: pplanel/hash-calculator-action@v1.3.1
|
||||
id: hash_branch
|
||||
with:
|
||||
input: ${{ steps.extract_branch.outputs.branch }}
|
||||
method: MD5
|
||||
|
||||
- name: Set Environment Variables
|
||||
id: set_env
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
echo "VERCEL_ALIAS_DOMAIN=${{ github.event.pull_request.number }}-${{ github.workflow }}.${VERCEL_DOMAIN_SUFFIX}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Vercel CLI
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
id: cache-npm
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: npm-
|
||||
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
|
||||
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: vercel
|
||||
env:
|
||||
META_TAG: ${{ steps.hash_branch.outputs.digest }}-${{ github.run_number }}-${{ github.run_attempt}}
|
||||
run: |
|
||||
env
|
||||
vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
|
||||
vercel build --token=${VERCEL_TOKEN}
|
||||
vercel deploy --prebuilt --archive=tgz --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }}
|
||||
|
||||
vercel ls --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }} &> vercel-output
|
||||
DEFAULT_URL=$(cat vercel-output | grep http | awk '{print $2}')
|
||||
ALIAS_URL=$(vercel alias set ${DEFAULT_URL} ${{ steps.set_env.outputs.VERCEL_ALIAS_DOMAIN }} --token=${VERCEL_TOKEN} | awk '{print $3}')
|
||||
|
||||
echo "New preview URL: ${DEFAULT_URL}"
|
||||
echo "New alias URL: ${ALIAS_URL}"
|
||||
echo "META_TAG=${META_TAG}"
|
||||
echo "VERCEL_URL=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
message: |
|
||||
Your build has completed!
|
||||
|
||||
[Preview deployment](${{ steps.vercel.outputs.VERCEL_URL }})
|
40
.github/workflows/remove_deploy_preview.yml
vendored
Normal file
40
.github/workflows/remove_deploy_preview.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Removedeploypreview
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
statuses: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
delete-deployments:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
|
||||
- name: Hash branch name
|
||||
uses: pplanel/hash-calculator-action@v1.3.1
|
||||
id: hash_branch
|
||||
with:
|
||||
input: ${{ steps.extract_branch.outputs.branch }}
|
||||
method: MD5
|
||||
|
||||
- name: Call the delete-deployment-preview.sh script
|
||||
env:
|
||||
META_TAG: ${{ steps.hash_branch.outputs.digest }}
|
||||
run: |
|
||||
bash ./scripts/delete-deployment-preview.sh
|
2
.github/workflows/sync.yml
vendored
2
.github/workflows/sync.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
id: sync
|
||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||
with:
|
||||
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
||||
upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
|
||||
upstream_sync_branch: main
|
||||
target_sync_branch: main
|
||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||
|
36
Dockerfile
36
Dockerfile
@@ -16,6 +16,7 @@ FROM base AS builder
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
ENV OPENAI_API_KEY=""
|
||||
ENV GOOGLE_API_KEY=""
|
||||
ENV CODE=""
|
||||
|
||||
WORKDIR /app
|
||||
@@ -31,6 +32,7 @@ RUN apk add proxychains-ng
|
||||
|
||||
ENV PROXY_URL=""
|
||||
ENV OPENAI_API_KEY=""
|
||||
ENV GOOGLE_API_KEY=""
|
||||
ENV CODE=""
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
@@ -41,22 +43,22 @@ COPY --from=builder /app/.next/server ./.next/server
|
||||
EXPOSE 3000
|
||||
|
||||
CMD if [ -n "$PROXY_URL" ]; then \
|
||||
export HOSTNAME="127.0.0.1"; \
|
||||
protocol=$(echo $PROXY_URL | cut -d: -f1); \
|
||||
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
|
||||
port=$(echo $PROXY_URL | cut -d: -f3); \
|
||||
conf=/etc/proxychains.conf; \
|
||||
echo "strict_chain" > $conf; \
|
||||
echo "proxy_dns" >> $conf; \
|
||||
echo "remote_dns_subnet 224" >> $conf; \
|
||||
echo "tcp_read_time_out 15000" >> $conf; \
|
||||
echo "tcp_connect_time_out 8000" >> $conf; \
|
||||
echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
|
||||
echo "localnet ::1/128" >> $conf; \
|
||||
echo "[ProxyList]" >> $conf; \
|
||||
echo "$protocol $host $port" >> $conf; \
|
||||
cat /etc/proxychains.conf; \
|
||||
proxychains -f $conf node server.js; \
|
||||
export HOSTNAME="127.0.0.1"; \
|
||||
protocol=$(echo $PROXY_URL | cut -d: -f1); \
|
||||
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
|
||||
port=$(echo $PROXY_URL | cut -d: -f3); \
|
||||
conf=/etc/proxychains.conf; \
|
||||
echo "strict_chain" > $conf; \
|
||||
echo "proxy_dns" >> $conf; \
|
||||
echo "remote_dns_subnet 224" >> $conf; \
|
||||
echo "tcp_read_time_out 15000" >> $conf; \
|
||||
echo "tcp_connect_time_out 8000" >> $conf; \
|
||||
echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
|
||||
echo "localnet ::1/128" >> $conf; \
|
||||
echo "[ProxyList]" >> $conf; \
|
||||
echo "$protocol $host $port" >> $conf; \
|
||||
cat /etc/proxychains.conf; \
|
||||
proxychains -f $conf node server.js; \
|
||||
else \
|
||||
node server.js; \
|
||||
node server.js; \
|
||||
fi
|
||||
|
118
README.md
118
README.md
@@ -1,22 +1,23 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/icon.svg" alt="icon"/>
|
||||
<img src="./docs/images/head-cover.png" alt="icon"/>
|
||||
|
||||
<h1 align="center">ChatGPT Next Web</h1>
|
||||
|
||||
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
||||
|
||||
English / [简体中文](./README_CN.md)
|
||||
|
||||
One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.
|
||||
|
||||
一键免费部署你的跨平台私人 ChatGPT 应用。
|
||||
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
|
||||
|
||||
[![Web][Web-image]][web-url]
|
||||
[![Windows][Windows-image]][download-url]
|
||||
[![MacOS][MacOS-image]][download-url]
|
||||
[![Linux][Linux-image]][download-url]
|
||||
|
||||
[Web App](https://chatgpt.nextweb.fun/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discord](https://discord.gg/YCkeafCafC) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
|
||||
[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)
|
||||
|
||||
[网页版](https://chatgpt.nextweb.fun/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
|
||||
|
||||
[web-url]: https://chatgpt.nextweb.fun
|
||||
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
|
||||
@@ -25,7 +26,9 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||
|
||||
[](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)
|
||||
[](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)
|
||||
|
||||
[](https://zeabur.com/templates/ZBUEFA)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
@@ -37,15 +40,15 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
|
||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||
- Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
|
||||
- Fully compatible with self-deployed llms, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI)
|
||||
- Privacy first, all data stored locally in the browser
|
||||
- Fully compatible with self-deployed LLMs, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI)
|
||||
- Privacy first, all data is stored locally in the browser
|
||||
- Markdown support: LaTex, mermaid, code highlight, etc.
|
||||
- Responsive design, dark mode and PWA
|
||||
- Fast first screen loading speed (~100kb), support streaming response
|
||||
- New in v2: create, share and debug your chat tools with prompt templates (mask)
|
||||
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
|
||||
- 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, 한국어
|
||||
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -59,9 +62,11 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
|
||||
## What's New
|
||||
|
||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
|
||||
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
|
||||
- 🚀 v2.10.1 support Google Gemini Pro model.
|
||||
- 🚀 v2.9.11 you can use azure endpoint now.
|
||||
- 🚀 v2.8 now we have a client that runs across all platforms!
|
||||
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
|
||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
|
||||
|
||||
## 主要功能
|
||||
|
||||
@@ -74,7 +79,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
|
||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
|
||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
|
||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
||||
|
||||
## 开发计划
|
||||
@@ -93,6 +98,7 @@ One-Click to get well-designed cross-platform ChatGPT web UI.
|
||||
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
|
||||
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
|
||||
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
|
||||
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
|
||||
|
||||
## Get Started
|
||||
|
||||
@@ -153,13 +159,13 @@ After adding or modifying this environment variable, please redeploy the project
|
||||
|
||||
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
|
||||
|
||||
### `OPENAI_API_KEY` (required)
|
||||
|
||||
Your openai api key.
|
||||
|
||||
### `CODE` (optional)
|
||||
|
||||
Access passsword, separated by comma.
|
||||
Access password, separated by comma.
|
||||
|
||||
### `OPENAI_API_KEY` (required)
|
||||
|
||||
Your openai api key, join multiple api keys with comma.
|
||||
|
||||
### `BASE_URL` (optional)
|
||||
|
||||
@@ -173,6 +179,28 @@ Override openai api request base url.
|
||||
|
||||
Specify OpenAI organization ID.
|
||||
|
||||
### `AZURE_URL` (optional)
|
||||
|
||||
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
|
||||
Azure deploy url.
|
||||
|
||||
### `AZURE_API_KEY` (optional)
|
||||
|
||||
Azure Api Key.
|
||||
|
||||
### `AZURE_API_VERSION` (optional)
|
||||
|
||||
Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
|
||||
|
||||
### `GOOGLE_API_KEY` (optional)
|
||||
|
||||
Google Gemini Pro Api Key.
|
||||
|
||||
### `GOOGLE_URL` (optional)
|
||||
|
||||
Google Gemini Pro Api Url.
|
||||
|
||||
### `HIDE_USER_API_KEY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
@@ -185,11 +213,26 @@ If you do not want users to input their own API key, set this value to 1.
|
||||
|
||||
If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
### `HIDE_BALANCE_QUERY` (optional)
|
||||
### `ENABLE_BALANCE_QUERY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do not want users to query balance, set this value to 1.
|
||||
If you do want users to query balance, set this value to 1, or you should set it to 0.
|
||||
|
||||
### `DISABLE_FAST_LINK` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you want to disable parse settings from url, set this to 1.
|
||||
|
||||
### `CUSTOM_MODELS` (optional)
|
||||
|
||||
> Default: Empty
|
||||
> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
|
||||
|
||||
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.
|
||||
|
||||
User `-all` to disable all default models, `+all` to enable all default models.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -230,8 +273,8 @@ yarn dev
|
||||
docker pull yidadaa/chatgpt-next-web
|
||||
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="your-password" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=your-password \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
@@ -239,9 +282,9 @@ You can start service behind a proxy:
|
||||
|
||||
```shell
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="your-password" \
|
||||
-e PROXY_URL="http://localhost:7890" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=your-password \
|
||||
-e PROXY_URL=http://localhost:7890 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
@@ -257,6 +300,20 @@ If your proxy needs password, use:
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
## Synchronizing Chat Records (UpStash)
|
||||
|
||||
| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
> Please go to the [docs][./docs] directory for more documentation instructions.
|
||||
|
||||
- [Deploy with cloudflare (Deprecated)](./docs/cloudflare-pages-en.md)
|
||||
- [Frequent Ask Questions](./docs/faq-en.md)
|
||||
- [How to add a new translation](./docs/translation.md)
|
||||
- [How to use Vercel (No English)](./docs/vercel-cn.md)
|
||||
- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
@@ -299,10 +356,17 @@ If you want to add a new translation, read this [document](./docs/translation.md
|
||||
[@AnsonHyq](https://github.com/AnsonHyq)
|
||||
[@synwith](https://github.com/synwith)
|
||||
[@piksonGit](https://github.com/piksonGit)
|
||||
[@ouyangzhiping](https://github.com/ouyangzhiping)
|
||||
[@wenjiavv](https://github.com/wenjiavv)
|
||||
[@LeXwDeX](https://github.com/LeXwDeX)
|
||||
[@Licoy](https://github.com/Licoy)
|
||||
[@shangmin2009](https://github.com/shangmin2009)
|
||||
|
||||
### Contributor
|
||||
### Contributors
|
||||
|
||||
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||
<a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ChatGPTNextWeb/ChatGPT-Next-Web" />
|
||||
</a>
|
||||
|
||||
## LICENSE
|
||||
|
||||
|
65
README_CN.md
65
README_CN.md
@@ -1,14 +1,16 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/icon.svg" alt="预览"/>
|
||||
|
||||
<h1 align="center">ChatGPT Next Web</h1>
|
||||
<h1 align="center">NextChat</h1>
|
||||
|
||||
一键免费部署你的私人 ChatGPT 网页应用。
|
||||
一键免费部署你的私人 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) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donate](#捐赠-donate-usdt)
|
||||
[演示 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)
|
||||
|
||||
[](https://zeabur.com/templates/ZBUEFA)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||

|
||||
@@ -19,7 +21,7 @@
|
||||
|
||||
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&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE;
|
||||
[](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 在某些区域被污染了,绑定自定义域名即可直连。
|
||||
|
||||
@@ -68,7 +70,7 @@ code1,code2,code3
|
||||
|
||||
### `OPENAI_API_KEY` (必填项)
|
||||
|
||||
OpanAI 密钥,你在 openai 账户页面申请的 api key。
|
||||
OpanAI 密钥,你在 openai 账户页面申请的 api key,使用英文逗号隔开多个 key,这样可以随机轮询这些 key。
|
||||
|
||||
### `CODE` (可选)
|
||||
|
||||
@@ -90,6 +92,28 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
指定 OpenAI 中的组织 ID。
|
||||
|
||||
### `AZURE_URL` (可选)
|
||||
|
||||
> 形如:https://{azure-resource-url}/openai/deployments/{deploy-name}
|
||||
|
||||
Azure 部署地址。
|
||||
|
||||
### `AZURE_API_KEY` (可选)
|
||||
|
||||
Azure 密钥。
|
||||
|
||||
### `AZURE_API_VERSION` (可选)
|
||||
|
||||
Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
|
||||
|
||||
### `GOOGLE_API_KEY` (optional)
|
||||
|
||||
Google Gemini Pro 密钥.
|
||||
|
||||
### `GOOGLE_URL` (optional)
|
||||
|
||||
Google Gemini Pro Api Url.
|
||||
|
||||
### `HIDE_USER_API_KEY` (可选)
|
||||
|
||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||
@@ -98,9 +122,20 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
|
||||
|
||||
### `HIDE_BALANCE_QUERY` (可选)
|
||||
### `ENABLE_BALANCE_QUERY` (可选)
|
||||
|
||||
如果你不想让用户查询余额,将此环境变量设置为 1 即可。
|
||||
如果你想启用余额查询功能,将此环境变量设置为 1 即可。
|
||||
|
||||
### `DISABLE_FAST_LINK` (可选)
|
||||
|
||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
||||
|
||||
### `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`
|
||||
|
||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -114,7 +149,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
|
||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
|
||||
BASE_URL=https://b.nextweb.fun/api/proxy
|
||||
```
|
||||
|
||||
### 本地开发
|
||||
@@ -135,8 +170,8 @@ BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
|
||||
docker pull yidadaa/chatgpt-next-web
|
||||
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="页面访问密码" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=页面访问密码 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
@@ -144,10 +179,10 @@ docker run -d -p 3000:3000 \
|
||||
|
||||
```shell
|
||||
docker run -d -p 3000:3000 \
|
||||
-e OPENAI_API_KEY="sk-xxxx" \
|
||||
-e CODE="页面访问密码" \
|
||||
-e OPENAI_API_KEY=sk-xxxx \
|
||||
-e CODE=页面访问密码 \
|
||||
--net=host \
|
||||
-e PROXY_URL="http://127.0.0.1:7890" \
|
||||
-e PROXY_URL=http://127.0.0.1:7890 \
|
||||
yidadaa/chatgpt-next-web
|
||||
```
|
||||
|
||||
@@ -179,6 +214,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||
|
||||
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||
|
||||
### 相关项目
|
||||
|
||||
- [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型
|
||||
|
||||
## 开源协议
|
||||
|
||||
[MIT](https://opensource.org/license/mit/)
|
||||
|
173
README_ES.md
173
README_ES.md
@@ -1,173 +0,0 @@
|
||||
<div align="center">
|
||||
<img src="./docs/images/icon.svg" alt="预览"/>
|
||||
|
||||
<h1 align="center">ChatGPT Next Web</h1>
|
||||
|
||||
Implemente su aplicación web privada ChatGPT de forma gratuita con un solo clic.
|
||||
|
||||
[Demo demo](https://chat-gpt-next-web.vercel.app/) / [Problemas de comentarios](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Únete a Discord](https://discord.gg/zrhvHCr79N) / [Grupo QQ](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [Desarrolladores de consejos](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donar](#捐赠-donate-usdt)
|
||||
|
||||
[](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)
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Comenzar
|
||||
|
||||
1. Prepara el tuyo [Clave API OpenAI](https://platform.openai.com/account/api-keys);
|
||||
2. Haga clic en el botón de la derecha para iniciar la implementación:
|
||||
[](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), inicie sesión directamente con su cuenta de Github y recuerde completar la clave API y la suma en la página de variables de entorno[Contraseña de acceso a la página](#配置页面访问密码) CÓDIGO;
|
||||
3. Una vez implementado, puede comenzar;
|
||||
4. (Opcional)[Enlazar un nombre de dominio personalizado](https://vercel.com/docs/concepts/projects/domains/add-a-domain): El nombre de dominio DNS asignado por Vercel está contaminado en algunas regiones y puede conectarse directamente enlazando un nombre de dominio personalizado.
|
||||
|
||||
## Manténgase actualizado
|
||||
|
||||
Si sigue los pasos anteriores para implementar su proyecto con un solo clic, es posible que siempre diga "La actualización existe" porque Vercel creará un nuevo proyecto para usted de forma predeterminada en lugar de bifurcar el proyecto, lo que evitará que la actualización se detecte correctamente.
|
||||
Le recomendamos que siga estos pasos para volver a implementar:
|
||||
|
||||
- Eliminar el repositorio original;
|
||||
- Utilice el botón de bifurcación en la esquina superior derecha de la página para bifurcar este proyecto;
|
||||
- En Vercel, vuelva a seleccionar e implementar,[Echa un vistazo al tutorial detallado](./docs/vercel-cn.md#如何新建项目)。
|
||||
|
||||
### Activar actualizaciones automáticas
|
||||
|
||||
> Si encuentra un error de ejecución de Upstream Sync, ¡Sync Fork manualmente una vez!
|
||||
|
||||
Cuando bifurca el proyecto, debido a las limitaciones de Github, debe ir manualmente a la página Acciones de su proyecto bifurcado para habilitar Flujos de trabajo y habilitar Upstream Sync Action, después de habilitarlo, puede activar las actualizaciones automáticas cada hora:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Actualizar el código manualmente
|
||||
|
||||
Si desea que el manual se actualice inmediatamente, puede consultarlo [Documentación para Github](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) Aprenda a sincronizar un proyecto bifurcado con código ascendente.
|
||||
|
||||
Puede destacar / ver este proyecto o seguir al autor para recibir notificaciones de nuevas actualizaciones de funciones.
|
||||
|
||||
## Configurar la contraseña de acceso a la página
|
||||
|
||||
> Después de configurar la contraseña, el usuario debe completar manualmente el código de acceso en la página de configuración para chatear normalmente, de lo contrario, se solicitará el estado no autorizado a través de un mensaje.
|
||||
|
||||
> **advertir**: Asegúrese de establecer el número de dígitos de la contraseña lo suficientemente largo, preferiblemente más de 7 dígitos, de lo contrario[Será volado](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
|
||||
|
||||
Este proyecto proporciona control de permisos limitado, agregue el nombre al nombre en la página Variables de entorno del Panel de control del proyecto Vercel `CODE` Variables de entorno con valores para contraseñas personalizadas separadas por comas:
|
||||
|
||||
code1,code2,code3
|
||||
|
||||
Después de agregar o modificar la variable de entorno, por favor**Redesplegar**proyecto para poner en vigor los cambios.
|
||||
|
||||
## Variable de entorno
|
||||
|
||||
> La mayoría de los elementos de configuración de este proyecto se establecen a través de variables de entorno, tutorial:[Cómo modificar las variables de entorno de Vercel](./docs/vercel-cn.md)。
|
||||
|
||||
### `OPENAI_API_KEY` (Requerido)
|
||||
|
||||
OpanAI key, la clave API que solicita en la página de su cuenta openai.
|
||||
|
||||
### `CODE` (Opcional)
|
||||
|
||||
Las contraseñas de acceso, opcionalmente, se pueden separar por comas.
|
||||
|
||||
**advertir**: Si no completa este campo, cualquiera puede usar directamente su sitio web implementado, lo que puede hacer que su token se consuma rápidamente, se recomienda completar esta opción.
|
||||
|
||||
### `BASE_URL` (Opcional)
|
||||
|
||||
> Predeterminado: `https://api.openai.com`
|
||||
|
||||
> Ejemplos: `http://your-openai-proxy.com`
|
||||
|
||||
URL del proxy de interfaz OpenAI, complete esta opción si configuró manualmente el proxy de interfaz openAI.
|
||||
|
||||
> Si encuentra problemas con el certificado SSL, establezca el `BASE_URL` El protocolo se establece en http.
|
||||
|
||||
### `OPENAI_ORG_ID` (Opcional)
|
||||
|
||||
Especifica el identificador de la organización en OpenAI.
|
||||
|
||||
### `HIDE_USER_API_KEY` (Opcional)
|
||||
|
||||
Si no desea que los usuarios rellenen la clave de API ellos mismos, establezca esta variable de entorno en 1.
|
||||
|
||||
### `DISABLE_GPT4` (Opcional)
|
||||
|
||||
Si no desea que los usuarios utilicen GPT-4, establezca esta variable de entorno en 1.
|
||||
|
||||
### `HIDE_BALANCE_QUERY` (Opcional)
|
||||
|
||||
Si no desea que los usuarios consulte el saldo, establezca esta variable de entorno en 1.
|
||||
|
||||
## explotación
|
||||
|
||||
> No se recomienda encarecidamente desarrollar o implementar localmente, debido a algunas razones técnicas, es difícil configurar el agente API de OpenAI localmente, a menos que pueda asegurarse de que puede conectarse directamente al servidor OpenAI.
|
||||
|
||||
Haga clic en el botón de abajo para iniciar el desarrollo secundario:
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
|
||||
Antes de empezar a escribir código, debe crear uno nuevo en la raíz del proyecto `.env.local` archivo, lleno de variables de entorno:
|
||||
|
||||
OPENAI_API_KEY=<your api key here>
|
||||
|
||||
### Desarrollo local
|
||||
|
||||
1. Instale nodejs 18 e hilo, pregunte a ChatGPT para obtener más detalles;
|
||||
2. ejecutar `yarn install && yarn dev` Enlatar. ⚠️ Nota: Este comando es solo para desarrollo local, no para implementación.
|
||||
3. Úselo si desea implementar localmente `yarn install && yarn start` comando, puede cooperar con pm2 a daemon para evitar ser asesinado, pregunte a ChatGPT para obtener más detalles.
|
||||
|
||||
## desplegar
|
||||
|
||||
### Implementación de contenedores (recomendado)
|
||||
|
||||
> La versión de Docker debe ser 20 o posterior, de lo contrario se indicará que no se puede encontrar la imagen.
|
||||
|
||||
> ⚠️ Nota: Las versiones de Docker están de 1 a 2 días por detrás de la última versión la mayor parte del tiempo, por lo que es normal que sigas diciendo "La actualización existe" después de la implementación.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
También puede especificar proxy:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Si necesita especificar otras variables de entorno, agréguelas usted mismo en el comando anterior `-e 环境变量=环境变量值` para especificar.
|
||||
|
||||
### Implementación local
|
||||
|
||||
Ejecute el siguiente comando en la consola:
|
||||
|
||||
```shell
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
⚠️ Nota: Si tiene problemas durante la instalación, utilice la implementación de Docker.
|
||||
|
||||
## Reconocimiento
|
||||
|
||||
### donante
|
||||
|
||||
> Ver versión en inglés.
|
||||
|
||||
### Colaboradores
|
||||
|
||||
[Ver la lista de colaboradores del proyecto](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
|
||||
|
||||
## Licencia de código abierto
|
||||
|
||||
[MIT](https://opensource.org/license/mit/)
|
@@ -1,7 +1,7 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import md5 from "spark-md5";
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
|
||||
|
||||
function getIP(req: NextRequest) {
|
||||
let ip = req.ip ?? req.headers.get("x-real-ip");
|
||||
@@ -16,19 +16,19 @@ function getIP(req: NextRequest) {
|
||||
|
||||
function parseApiKey(bearToken: string) {
|
||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
|
||||
const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
|
||||
|
||||
return {
|
||||
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
|
||||
apiKey: isOpenAiKey ? token : "",
|
||||
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
|
||||
apiKey: isApiKey ? token : "",
|
||||
};
|
||||
}
|
||||
|
||||
export function auth(req: NextRequest) {
|
||||
export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
const authToken = req.headers.get("Authorization") ?? "";
|
||||
|
||||
// check if it is openai api key or user token
|
||||
const { accessCode, apiKey: token } = parseApiKey(authToken);
|
||||
const { accessCode, apiKey } = parseApiKey(authToken);
|
||||
|
||||
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||
|
||||
@@ -39,19 +39,33 @@ export function auth(req: NextRequest) {
|
||||
console.log("[User IP] ", getIP(req));
|
||||
console.log("[Time] ", new Date().toLocaleString());
|
||||
|
||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
|
||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
|
||||
return {
|
||||
error: true,
|
||||
msg: !accessCode ? "empty access code" : "wrong access code",
|
||||
};
|
||||
}
|
||||
|
||||
if (serverConfig.hideUserApiKey && !!apiKey) {
|
||||
return {
|
||||
error: true,
|
||||
msg: "you are not allowed to access with your own api key",
|
||||
};
|
||||
}
|
||||
|
||||
// if user does not provide an api key, inject system api key
|
||||
if (!token) {
|
||||
const apiKey = serverConfig.apiKey;
|
||||
if (apiKey) {
|
||||
if (!apiKey) {
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
const systemApiKey =
|
||||
modelProvider === ModelProvider.GeminiPro
|
||||
? serverConfig.googleApiKey
|
||||
: serverConfig.isAzure
|
||||
? serverConfig.azureApiKey
|
||||
: serverConfig.apiKey;
|
||||
if (systemApiKey) {
|
||||
console.log("[Auth] use system api key");
|
||||
req.headers.set("Authorization", `Bearer ${apiKey}`);
|
||||
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
||||
} else {
|
||||
console.log("[Auth] admin did not provide an api key");
|
||||
}
|
||||
|
@@ -1,44 +1,78 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant";
|
||||
import { collectModelTable } from "../utils/model";
|
||||
import { makeAzurePath } from "../azure";
|
||||
|
||||
export const OPENAI_URL = "api.openai.com";
|
||||
const DEFAULT_PROTOCOL = "https";
|
||||
const PROTOCOL = process.env.PROTOCOL || DEFAULT_PROTOCOL;
|
||||
const BASE_URL = process.env.BASE_URL || OPENAI_URL;
|
||||
const DISABLE_GPT4 = !!process.env.DISABLE_GPT4;
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function requestOpenai(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
const authValue = req.headers.get("Authorization") ?? "";
|
||||
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
|
||||
var authValue,
|
||||
authHeaderName = "";
|
||||
if (serverConfig.isAzure) {
|
||||
authValue =
|
||||
req.headers
|
||||
.get("Authorization")
|
||||
?.trim()
|
||||
.replaceAll("Bearer ", "")
|
||||
.trim() ?? "";
|
||||
|
||||
authHeaderName = "api-key";
|
||||
} else {
|
||||
authValue = req.headers.get("Authorization") ?? "";
|
||||
authHeaderName = "Authorization";
|
||||
}
|
||||
|
||||
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
"/api/openai/",
|
||||
"",
|
||||
);
|
||||
|
||||
let baseUrl = BASE_URL;
|
||||
let baseUrl =
|
||||
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", openaiPath);
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
if (process.env.OPENAI_ORG_ID) {
|
||||
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
|
||||
// this fix [Org ID] undefined in server side if not using custom point
|
||||
if (serverConfig.openaiOrgId !== undefined) {
|
||||
console.log("[Org ID]", serverConfig.openaiOrgId);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 10 * 60 * 1000);
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}/${openaiPath}`;
|
||||
if (serverConfig.isAzure) {
|
||||
if (!serverConfig.azureApiVersion) {
|
||||
return NextResponse.json({
|
||||
error: true,
|
||||
message: `missing AZURE_API_VERSION in server env vars`,
|
||||
});
|
||||
}
|
||||
path = makeAzurePath(path, serverConfig.azureApiVersion);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
Authorization: authValue,
|
||||
...(process.env.OPENAI_ORG_ID && {
|
||||
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
||||
[authHeaderName]: authValue,
|
||||
...(serverConfig.openaiOrgId && {
|
||||
"OpenAI-Organization": serverConfig.openaiOrgId,
|
||||
}),
|
||||
},
|
||||
method: req.method,
|
||||
@@ -51,18 +85,23 @@ export async function requestOpenai(req: NextRequest) {
|
||||
};
|
||||
|
||||
// #1815 try to refuse gpt4 request
|
||||
if (DISABLE_GPT4 && req.body) {
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const modelTable = collectModelTable(
|
||||
DEFAULT_MODELS,
|
||||
serverConfig.customModels,
|
||||
);
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody);
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
if ((jsonBody?.model ?? "").includes("gpt-4")) {
|
||||
// not undefined and is false
|
||||
if (modelTable[jsonBody?.model ?? ""].available === false) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: "you are not allowed to use gpt-4 model",
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -83,6 +122,12 @@ export async function requestOpenai(req: NextRequest) {
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
|
||||
// So if the streaming is disabled, we need to remove the content-encoding header
|
||||
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
|
||||
// The browser will try to decode the response with brotli and fail
|
||||
newHeaders.delete("content-encoding");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
|
@@ -4,13 +4,15 @@ import { getServerSideConfig } from "../../config/server";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// Danger! Don not write any secret value here!
|
||||
// Danger! Do not hard code any secret value here!
|
||||
// 警告!不要在这里写入任何敏感信息!
|
||||
const DANGER_CONFIG = {
|
||||
needCode: serverConfig.needCode,
|
||||
hideUserApiKey: serverConfig.hideUserApiKey,
|
||||
disableGPT4: serverConfig.disableGPT4,
|
||||
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
||||
disableFastLink: serverConfig.disableFastLink,
|
||||
customModels: serverConfig.customModels,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
43
app/api/cors/[...path]/route.ts
Normal file
43
app/api/cors/[...path]/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const [protocol, ...subpath] = params.path;
|
||||
const targetUrl = `${protocol}://${subpath.join("/")}`;
|
||||
|
||||
const method = req.headers.get("method") ?? undefined;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
method?.toLowerCase() ?? "",
|
||||
);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
};
|
||||
|
||||
const fetchResult = await fetch(targetUrl, fetchOptions);
|
||||
|
||||
console.log("[Any Proxy]", targetUrl, {
|
||||
status: fetchResult.status,
|
||||
statusText: fetchResult.statusText,
|
||||
});
|
||||
|
||||
return fetchResult;
|
||||
}
|
||||
|
||||
export const POST = handle;
|
||||
export const GET = handle;
|
||||
export const OPTIONS = handle;
|
||||
|
||||
export const runtime = "nodejs";
|
121
app/api/google/[...path]/route.ts
Normal file
121
app/api/google/[...path]/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Google Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
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);
|
||||
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.googleApiKey;
|
||||
|
||||
if (!key) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `missing GOOGLE_API_KEY in server env vars`,
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const fetchUrl = `${baseUrl}/${path}?key=${key}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
];
|
@@ -1,6 +1,6 @@
|
||||
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { OpenaiPath } from "@/app/constant";
|
||||
import { ModelProvider, OpenaiPath } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
@@ -45,7 +45,7 @@ async function handle(
|
||||
);
|
||||
}
|
||||
|
||||
const authResult = auth(req);
|
||||
const authResult = auth(req, ModelProvider.GPT);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
@@ -75,3 +75,22 @@ 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",
|
||||
];
|
||||
|
9
app/azure.ts
Normal file
9
app/azure.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function makeAzurePath(path: string, apiVersion: string) {
|
||||
// should omit /v1 prefix
|
||||
path = path.replaceAll("v1/", "");
|
||||
|
||||
// should add api-key to query string
|
||||
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
|
||||
|
||||
return path;
|
||||
}
|
@@ -1,8 +1,13 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
||||
import {
|
||||
ACCESS_CODE_PREFIX,
|
||||
Azure,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
|
||||
import { GeminiProApi } from "./platforms/google";
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
@@ -41,6 +46,13 @@ export interface LLMUsage {
|
||||
export interface LLMModel {
|
||||
name: string;
|
||||
available: boolean;
|
||||
provider: LLMModelProvider;
|
||||
}
|
||||
|
||||
export interface LLMModelProvider {
|
||||
id: string;
|
||||
providerName: string;
|
||||
providerType: string;
|
||||
}
|
||||
|
||||
export abstract class LLMApi {
|
||||
@@ -73,7 +85,11 @@ interface ChatProvider {
|
||||
export class ClientApi {
|
||||
public llm: LLMApi;
|
||||
|
||||
constructor() {
|
||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||
if (provider === ModelProvider.GeminiPro) {
|
||||
this.llm = new GeminiProApi();
|
||||
return;
|
||||
}
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
|
||||
@@ -93,7 +109,7 @@ export class ClientApi {
|
||||
{
|
||||
from: "human",
|
||||
value:
|
||||
"Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web",
|
||||
"Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web",
|
||||
},
|
||||
]);
|
||||
// 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
|
||||
@@ -123,26 +139,34 @@ export class ClientApi {
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ClientApi();
|
||||
|
||||
export function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let headers: Record<string, string> = {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
"x-requested-with": "XMLHttpRequest",
|
||||
"Accept": "application/json",
|
||||
};
|
||||
const modelConfig = useChatStore.getState().currentSession().mask.modelConfig;
|
||||
const isGoogle = modelConfig.model === "gemini-pro";
|
||||
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||
const authHeader = isAzure ? "api-key" : "Authorization";
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
: isAzure
|
||||
? accessStore.azureApiKey
|
||||
: accessStore.openaiApiKey;
|
||||
|
||||
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
|
||||
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
|
||||
const validString = (x: string) => x && x.length > 0;
|
||||
|
||||
// use user's api key first
|
||||
if (validString(accessStore.token)) {
|
||||
headers.Authorization = makeBearer(accessStore.token);
|
||||
if (validString(apiKey)) {
|
||||
headers[authHeader] = makeBearer(apiKey);
|
||||
} else if (
|
||||
accessStore.enabledAccessControl() &&
|
||||
validString(accessStore.accessCode)
|
||||
) {
|
||||
headers.Authorization = makeBearer(
|
||||
headers[authHeader] = makeBearer(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
}
|
||||
|
226
app/client/platforms/google.ts
Normal file
226
app/client/platforms/google.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import Locale from "../../locales";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import de from "@/app/locales/de";
|
||||
export class GeminiProApi implements LLMApi {
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] gemini-pro response: ", res);
|
||||
|
||||
return (
|
||||
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
|
||||
res?.error?.message ||
|
||||
""
|
||||
);
|
||||
}
|
||||
async chat(options: ChatOptions): Promise<void> {
|
||||
const apiClient = this;
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role.replace("assistant", "model").replace("system", "user"),
|
||||
parts: [{ text: v.content }],
|
||||
}));
|
||||
|
||||
// google requires that role in neighboring messages must not be the same
|
||||
for (let i = 0; i < messages.length - 1; ) {
|
||||
// Check if current and next item both have the role "model"
|
||||
if (messages[i].role === messages[i + 1].role) {
|
||||
// Concatenate the 'parts' of the current and next item
|
||||
messages[i].parts = messages[i].parts.concat(messages[i + 1].parts);
|
||||
// Remove the next item
|
||||
messages.splice(i + 1, 1);
|
||||
} else {
|
||||
// Move to the next item
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
const requestPayload = {
|
||||
contents: messages,
|
||||
generationConfig: {
|
||||
// stopSequences: [
|
||||
// "Title"
|
||||
// ],
|
||||
temperature: modelConfig.temperature,
|
||||
maxOutputTokens: modelConfig.max_tokens,
|
||||
topP: modelConfig.top_p,
|
||||
// "topK": modelConfig.top_k,
|
||||
},
|
||||
safetySettings: [
|
||||
{
|
||||
category: "HARM_CATEGORY_HARASSMENT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_HATE_SPEECH",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "BLOCK_ONLY_HIGH",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
console.log("[Request] google payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
try {
|
||||
const chatPath = this.path(Google.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 streamChatPath = chatPath.replace(
|
||||
"generateContent",
|
||||
"streamGenerateContent",
|
||||
);
|
||||
let finished = false;
|
||||
|
||||
let existingTexts: string[] = [];
|
||||
const finish = () => {
|
||||
finished = true;
|
||||
options.onFinish(existingTexts.join(""));
|
||||
};
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
finish();
|
||||
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();
|
||||
fetch(streamChatPath, chatPayload)
|
||||
.then((response) => {
|
||||
const reader = response?.body?.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let partialData = "";
|
||||
|
||||
return reader?.read().then(function processText({
|
||||
done,
|
||||
value,
|
||||
}): Promise<any> {
|
||||
if (done) {
|
||||
console.log("Stream complete");
|
||||
// options.onFinish(responseText + remainText);
|
||||
finished = true;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
partialData += decoder.decode(value, { stream: true });
|
||||
|
||||
try {
|
||||
let data = JSON.parse(ensureProperEnding(partialData));
|
||||
|
||||
const textArray = data.reduce(
|
||||
(acc: string[], item: { candidates: any[] }) => {
|
||||
const texts = item.candidates.map((candidate) =>
|
||||
candidate.content.parts
|
||||
.map((part: { text: any }) => part.text)
|
||||
.join(""),
|
||||
);
|
||||
return acc.concat(texts);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (textArray.length > existingTexts.length) {
|
||||
const deltaArray = textArray.slice(existingTexts.length);
|
||||
existingTexts = textArray;
|
||||
remainText += deltaArray.join("");
|
||||
}
|
||||
} catch (error) {
|
||||
// console.log("[Response Animation] error: ", error,partialData);
|
||||
// skip error message when parsing json
|
||||
}
|
||||
|
||||
return reader.read().then(processText);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
|
||||
if (resJson?.promptFeedback?.blockReason) {
|
||||
// being blocked
|
||||
options.onError?.(
|
||||
new Error(
|
||||
"Message is being blocked for reason: " +
|
||||
resJson.promptFeedback.blockReason,
|
||||
),
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
usage(): Promise<LLMUsage> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
path(path: string): string {
|
||||
return "/api/google/" + path;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureProperEnding(str: string) {
|
||||
if (str.startsWith("[") && !str.endsWith("]")) {
|
||||
return str + "]";
|
||||
}
|
||||
return str;
|
||||
}
|
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
DEFAULT_MODELS,
|
||||
OpenaiPath,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
@@ -13,6 +15,8 @@ import {
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { makeAzurePath } from "@/app/azure";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -27,17 +31,35 @@ export class ChatGPTApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
let openaiUrl = useAccessStore.getState().openaiUrl;
|
||||
if (openaiUrl.length === 0) {
|
||||
openaiUrl = DEFAULT_API_HOST;
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
||||
|
||||
if (isAzure && !accessStore.isValidAzure()) {
|
||||
throw Error(
|
||||
"incomplete azure config, please check it in your settings page",
|
||||
);
|
||||
}
|
||||
if (openaiUrl.endsWith("/")) {
|
||||
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
|
||||
|
||||
let baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST : ApiPath.OpenAI;
|
||||
}
|
||||
if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith("/api/openai")) {
|
||||
openaiUrl = "https://" + openaiUrl;
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
return [openaiUrl, path].join("/");
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
if (isAzure) {
|
||||
path = makeAzurePath(path, accessStore.azureApiVersion);
|
||||
}
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
@@ -66,6 +88,8 @@ export class ChatGPTApi implements LLMApi {
|
||||
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);
|
||||
@@ -91,12 +115,35 @@ export class ChatGPTApi implements LLMApi {
|
||||
|
||||
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");
|
||||
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) {
|
||||
options.onFinish(responseText);
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,14 +197,19 @@ export class ChatGPTApi implements LLMApi {
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const delta = json.choices[0].delta.content;
|
||||
const json = JSON.parse(text) as {
|
||||
choices: Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
const delta = json.choices[0]?.delta?.content;
|
||||
if (delta) {
|
||||
responseText += delta;
|
||||
options.onUpdate?.(responseText, delta);
|
||||
remainText += delta;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
console.error("[Request] parse error", text);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
@@ -271,6 +323,11 @@ export class ChatGPTApi implements LLMApi {
|
||||
return chatModels.map((m) => ({
|
||||
name: m.id,
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -7,12 +7,28 @@ import { useAccessStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import { useEffect } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export function AuthPage() {
|
||||
const navigate = useNavigate();
|
||||
const access = useAccessStore();
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const goHome = () => navigate(Path.Home);
|
||||
const goChat = () => navigate(Path.Chat);
|
||||
const resetAccessCode = () => {
|
||||
accessStore.update((access) => {
|
||||
access.openaiApiKey = "";
|
||||
access.accessCode = "";
|
||||
});
|
||||
}; // Reset access code to empty string
|
||||
|
||||
useEffect(() => {
|
||||
if (getClientConfig()?.isApp) {
|
||||
navigate(Path.Settings);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles["auth-page"]}>
|
||||
@@ -27,19 +43,54 @@ export function AuthPage() {
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Auth.Input}
|
||||
value={access.accessCode}
|
||||
value={accessStore.accessCode}
|
||||
onChange={(e) => {
|
||||
access.updateCode(e.currentTarget.value);
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
value={accessStore.openaiApiKey}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
value={accessStore.googleApiKey}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.googleApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className={styles["auth-actions"]}>
|
||||
<IconButton
|
||||
text={Locale.Auth.Confirm}
|
||||
type="primary"
|
||||
onClick={goHome}
|
||||
onClick={goChat}
|
||||
/>
|
||||
<IconButton
|
||||
text={Locale.Auth.Later}
|
||||
onClick={() => {
|
||||
resetAccessCode();
|
||||
goHome();
|
||||
}}
|
||||
/>
|
||||
<IconButton text={Locale.Auth.Later} onClick={goHome} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -18,6 +18,7 @@ import { MaskAvatar } from "./mask";
|
||||
import { Mask } from "../store/mask";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useMobileScreen } from "../utils";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
@@ -60,7 +61,10 @@ export function ChatItem(props: {
|
||||
{props.narrow ? (
|
||||
<div className={styles["chat-item-narrow"]}>
|
||||
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["chat-item-narrow-count"]}>
|
||||
{props.count}
|
||||
@@ -80,7 +84,11 @@ export function ChatItem(props: {
|
||||
|
||||
<div
|
||||
className={styles["chat-item-delete"]}
|
||||
onClickCapture={props.onDelete}
|
||||
onClickCapture={(e) => {
|
||||
props.onDelete?.();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
@@ -101,6 +109,7 @@ export function ChatList(props: { narrow?: boolean }) {
|
||||
);
|
||||
const chatStore = useChatStore();
|
||||
const navigate = useNavigate();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
const { destination, source } = result;
|
||||
@@ -142,7 +151,7 @@ export function ChatList(props: { narrow?: boolean }) {
|
||||
}}
|
||||
onDelete={async () => {
|
||||
if (
|
||||
!props.narrow ||
|
||||
(!props.narrow && !isMobileScreen) ||
|
||||
(await showConfirm(Locale.Home.DeleteChat))
|
||||
) {
|
||||
chatStore.deleteSession(i);
|
||||
|
@@ -349,6 +349,14 @@
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
/* Specific styles for iOS devices */
|
||||
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.chat-message-edit {
|
||||
top: -8%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-status {
|
||||
|
@@ -73,13 +73,13 @@ import {
|
||||
showPrompt,
|
||||
showToast,
|
||||
} from "./ui-lib";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CHAT_PAGE_SIZE,
|
||||
LAST_INPUT_KEY,
|
||||
MAX_RENDER_MSG_COUNT,
|
||||
Path,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
} from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||
@@ -88,6 +88,7 @@ import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -143,6 +144,7 @@ export function SessionConfigModel(props: { onClose: () => void }) {
|
||||
extraListItems={
|
||||
session.mask.modelConfig.sendMemory ? (
|
||||
<ListItem
|
||||
className="copyable"
|
||||
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
|
||||
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||
></ListItem>
|
||||
@@ -429,16 +431,26 @@ export function ChatActions(props: {
|
||||
|
||||
// switch model
|
||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||
const allModels = useAllModels();
|
||||
const models = useMemo(
|
||||
() =>
|
||||
config
|
||||
.allModels()
|
||||
.filter((m) => m.available)
|
||||
.map((m) => m.name),
|
||||
[config],
|
||||
() => allModels.filter((m) => m.available),
|
||||
[allModels],
|
||||
);
|
||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// if current model is not available
|
||||
// switch to first available model
|
||||
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
|
||||
if (isUnavaliableModel && models.length > 0) {
|
||||
const nextModel = models[0].name as ModelType;
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.mask.modelConfig.model = nextModel),
|
||||
);
|
||||
showToast(nextModel);
|
||||
}
|
||||
}, [chatStore, currentModel, models]);
|
||||
|
||||
return (
|
||||
<div className={styles["chat-input-actions"]}>
|
||||
{couldStop && (
|
||||
@@ -518,8 +530,8 @@ export function ChatActions(props: {
|
||||
<Selector
|
||||
defaultSelectedValue={currentModel}
|
||||
items={models.map((m) => ({
|
||||
title: m,
|
||||
value: m,
|
||||
title: m.displayName,
|
||||
value: m.name,
|
||||
}))}
|
||||
onClose={() => setShowModelSelector(false)}
|
||||
onSelection={(s) => {
|
||||
@@ -802,7 +814,7 @@ function _Chat() {
|
||||
(m) => m.id === message.id,
|
||||
);
|
||||
|
||||
if (resendingIndex <= 0 || resendingIndex >= session.messages.length) {
|
||||
if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
|
||||
console.error("[Chat] failed to find resending message", message);
|
||||
return;
|
||||
}
|
||||
@@ -935,12 +947,13 @@ function _Chat() {
|
||||
|
||||
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
|
||||
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
|
||||
const isHitBottom = bottomHeight >= e.scrollHeight - 10;
|
||||
const isHitBottom =
|
||||
bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
|
||||
|
||||
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
|
||||
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
||||
|
||||
if (isTouchTopEdge) {
|
||||
if (isTouchTopEdge && !isTouchBottomEdge) {
|
||||
setMsgRenderIndex(prevPageMsgIndex);
|
||||
} else if (isTouchBottomEdge) {
|
||||
setMsgRenderIndex(nextPageMsgIndex);
|
||||
@@ -974,14 +987,17 @@ function _Chat() {
|
||||
doSubmit(text);
|
||||
},
|
||||
code: (text) => {
|
||||
if (accessStore.disableFastLink) return;
|
||||
console.log("[Command] got code from url: ", text);
|
||||
showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
|
||||
if (res) {
|
||||
accessStore.updateCode(text);
|
||||
accessStore.update((access) => (access.accessCode = text));
|
||||
}
|
||||
});
|
||||
},
|
||||
settings: (text) => {
|
||||
if (accessStore.disableFastLink) return;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(text) as {
|
||||
key?: string;
|
||||
@@ -997,10 +1013,12 @@ function _Chat() {
|
||||
).then((res) => {
|
||||
if (!res) return;
|
||||
if (payload.key) {
|
||||
accessStore.updateToken(payload.key);
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = payload.key!),
|
||||
);
|
||||
}
|
||||
if (payload.url) {
|
||||
accessStore.updateOpenAiUrl(payload.url);
|
||||
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1013,6 +1031,23 @@ function _Chat() {
|
||||
// edit / insert message modal
|
||||
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
||||
|
||||
// remember unfinished input
|
||||
useEffect(() => {
|
||||
// try to load from local storage
|
||||
const key = UNFINISHED_INPUT(session.id);
|
||||
const mayBeUnfinishedInput = localStorage.getItem(key);
|
||||
if (mayBeUnfinishedInput && userInput.length === 0) {
|
||||
setUserInput(mayBeUnfinishedInput);
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
const dom = inputRef.current;
|
||||
return () => {
|
||||
localStorage.setItem(key, dom?.value ?? "");
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -1123,9 +1158,9 @@ function _Chat() {
|
||||
10,
|
||||
);
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
const m = session.messages.find(
|
||||
(m) => m.id === message.id,
|
||||
);
|
||||
const m = session.mask.context
|
||||
.concat(session.messages)
|
||||
.find((m) => m.id === message.id);
|
||||
if (m) {
|
||||
m.content = newMessage;
|
||||
}
|
||||
@@ -1136,7 +1171,18 @@ function _Chat() {
|
||||
{isUser ? (
|
||||
<Avatar avatar={config.avatar} />
|
||||
) : (
|
||||
<MaskAvatar mask={session.mask} />
|
||||
<>
|
||||
{["system"].includes(message.role) ? (
|
||||
<Avatar avatar="2699-fe0f" />
|
||||
) : (
|
||||
<MaskAvatar
|
||||
avatar={session.mask.avatar}
|
||||
model={
|
||||
message.model || session.mask.modelConfig.model
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@@ -10,7 +10,10 @@ import BotIcon from "../icons/bot.svg";
|
||||
import BlackBotIcon from "../icons/black-bot.svg";
|
||||
|
||||
export function getEmojiUrl(unified: string, style: EmojiStyle) {
|
||||
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
|
||||
// Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
|
||||
// Old CDN broken, so I had to switch to this one
|
||||
// Author: https://github.com/H0llyW00dzZ
|
||||
return `https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`;
|
||||
}
|
||||
|
||||
export function AvatarPicker(props: {
|
||||
|
@@ -4,8 +4,8 @@ import GithubIcon from "../icons/github.svg";
|
||||
import ResetIcon from "../icons/reload.svg";
|
||||
import { ISSUE_URL } from "../constant";
|
||||
import Locale from "../locales";
|
||||
import { downloadAs } from "../utils";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -26,10 +26,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
|
||||
clearAndSaveData() {
|
||||
try {
|
||||
downloadAs(
|
||||
JSON.stringify(localStorage),
|
||||
"chatgpt-next-web-snapshot.json",
|
||||
);
|
||||
useSyncStore.getState().export();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
|
@@ -186,7 +186,8 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
border: var(--border-in-light);
|
||||
|
||||
*:not(li) {
|
||||
code,
|
||||
pre {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
import styles from "./exporter.module.scss";
|
||||
import {
|
||||
@@ -27,12 +27,13 @@ import { Avatar } from "./emoji";
|
||||
import dynamic from "next/dynamic";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import { toBlob, toJpeg, toPng } from "html-to-image";
|
||||
import { toBlob, toPng } from "html-to-image";
|
||||
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
||||
import { api } from "../client/api";
|
||||
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
|
||||
import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ClientApi } from "../client/api";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -41,7 +42,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
export function ExportMessageModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal title={Locale.Export.Title} onClose={props.onClose}>
|
||||
<Modal
|
||||
title={Locale.Export.Title}
|
||||
onClose={props.onClose}
|
||||
footer={
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
fontSize: 14,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
{Locale.Exporter.Description.Title}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ minHeight: "40vh" }}>
|
||||
<MessageExporter />
|
||||
</div>
|
||||
@@ -127,7 +143,7 @@ export function MessageExporter() {
|
||||
];
|
||||
const { currentStep, setCurrentStepIndex, currentStepIndex } =
|
||||
useSteps(steps);
|
||||
const formats = ["text", "image"] as const;
|
||||
const formats = ["text", "image", "json"] as const;
|
||||
type ExportFormat = (typeof formats)[number];
|
||||
|
||||
const [exportConfig, setExportConfig] = useState({
|
||||
@@ -149,7 +165,7 @@ export function MessageExporter() {
|
||||
if (exportConfig.includeContext) {
|
||||
ret.push(...session.mask.context);
|
||||
}
|
||||
ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
|
||||
ret.push(...session.messages.filter((m) => selection.has(m.id)));
|
||||
return ret;
|
||||
}, [
|
||||
exportConfig.includeContext,
|
||||
@@ -157,7 +173,21 @@ export function MessageExporter() {
|
||||
session.mask.context,
|
||||
selection,
|
||||
]);
|
||||
|
||||
function preview() {
|
||||
if (exportConfig.format === "text") {
|
||||
return (
|
||||
<MarkdownPreviewer messages={selectedMessages} topic={session.topic} />
|
||||
);
|
||||
} else if (exportConfig.format === "json") {
|
||||
return (
|
||||
<JsonPreviewer messages={selectedMessages} topic={session.topic} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ImagePreviewer messages={selectedMessages} topic={session.topic} />
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Steps
|
||||
@@ -212,16 +242,7 @@ export function MessageExporter() {
|
||||
/>
|
||||
</div>
|
||||
{currentStep.value === "preview" && (
|
||||
<div className={styles["message-exporter-body"]}>
|
||||
{exportConfig.format === "text" ? (
|
||||
<MarkdownPreviewer
|
||||
messages={selectedMessages}
|
||||
topic={session.topic}
|
||||
/>
|
||||
) : (
|
||||
<ImagePreviewer messages={selectedMessages} topic={session.topic} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles["message-exporter-body"]}>{preview()}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -255,7 +276,8 @@ export function RenderExport(props: {
|
||||
});
|
||||
|
||||
props.onRender(renderMsgs);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={domRef}>
|
||||
@@ -280,10 +302,17 @@ export function PreviewActions(props: {
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [shouldExport, setShouldExport] = useState(false);
|
||||
|
||||
const config = useAppConfig();
|
||||
const onRenderMsgs = (msgs: ChatMessage[]) => {
|
||||
setShouldExport(false);
|
||||
|
||||
var api: ClientApi;
|
||||
if (config.modelConfig.model === "gemini-pro") {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
api
|
||||
.share(msgs)
|
||||
.then((res) => {
|
||||
@@ -378,7 +407,7 @@ export function PreviewActions(props: {
|
||||
function ExportAvatar(props: { avatar: string }) {
|
||||
if (props.avatar === DEFAULT_MASK_AVATAR) {
|
||||
return (
|
||||
<NextImage
|
||||
<img
|
||||
src={BotIcon.src}
|
||||
width={30}
|
||||
height={30}
|
||||
@@ -388,7 +417,7 @@ function ExportAvatar(props: { avatar: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
return <Avatar avatar={props.avatar}></Avatar>;
|
||||
return <Avatar avatar={props.avatar} />;
|
||||
}
|
||||
|
||||
export function ImagePreviewer(props: {
|
||||
@@ -417,6 +446,7 @@ export function ImagePreviewer(props: {
|
||||
])
|
||||
.then(() => {
|
||||
showToast(Locale.Copy.Success);
|
||||
refreshPreview();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[Copy Image] ", e);
|
||||
@@ -427,24 +457,62 @@ export function ImagePreviewer(props: {
|
||||
|
||||
const isMobile = useMobileScreen();
|
||||
|
||||
const download = () => {
|
||||
const download = async () => {
|
||||
showToast(Locale.Export.Image.Toast);
|
||||
const dom = previewRef.current;
|
||||
if (!dom) return;
|
||||
toPng(dom)
|
||||
.then((blob) => {
|
||||
if (!blob) return;
|
||||
|
||||
if (isMobile || getClientConfig()?.isApp) {
|
||||
showImageModal(blob);
|
||||
const isApp = getClientConfig()?.isApp;
|
||||
|
||||
try {
|
||||
const blob = await toPng(dom);
|
||||
if (!blob) return;
|
||||
|
||||
if (isMobile || (isApp && window.__TAURI__)) {
|
||||
if (isApp && window.__TAURI__) {
|
||||
const result = await window.__TAURI__.dialog.save({
|
||||
defaultPath: `${props.topic}.png`,
|
||||
filters: [
|
||||
{
|
||||
name: "PNG Files",
|
||||
extensions: ["png"],
|
||||
},
|
||||
{
|
||||
name: "All Files",
|
||||
extensions: ["*"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (result !== null) {
|
||||
const response = await fetch(blob);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
|
||||
showToast(Locale.Download.Success);
|
||||
} else {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
} else {
|
||||
const link = document.createElement("a");
|
||||
link.download = `${props.topic}.png`;
|
||||
link.href = blob;
|
||||
link.click();
|
||||
showImageModal(blob);
|
||||
}
|
||||
})
|
||||
.catch((e) => console.log("[Export Image] ", e));
|
||||
} else {
|
||||
const link = document.createElement("a");
|
||||
link.download = `${props.topic}.png`;
|
||||
link.href = blob;
|
||||
link.click();
|
||||
refreshPreview();
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPreview = () => {
|
||||
const dom = previewRef.current;
|
||||
if (dom) {
|
||||
dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -470,7 +538,7 @@ export function ImagePreviewer(props: {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles["main-title"]}>ChatGPT Next Web</div>
|
||||
<div className={styles["main-title"]}>NextChat</div>
|
||||
<div className={styles["sub-title"]}>
|
||||
github.com/Yidadaa/ChatGPT-Next-Web
|
||||
</div>
|
||||
@@ -545,12 +613,12 @@ export function MarkdownPreviewer(props: {
|
||||
const download = () => {
|
||||
downloadAs(mdText, `${props.topic}.md`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewActions
|
||||
copy={copy}
|
||||
download={download}
|
||||
showCopy={true}
|
||||
messages={props.messages}
|
||||
/>
|
||||
<div className="markdown-body">
|
||||
@@ -559,3 +627,44 @@ export function MarkdownPreviewer(props: {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function JsonPreviewer(props: {
|
||||
messages: ChatMessage[];
|
||||
topic: string;
|
||||
}) {
|
||||
const msgs = {
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `${Locale.FineTuned.Sysmessage} ${props.topic}`,
|
||||
},
|
||||
...props.messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
],
|
||||
};
|
||||
const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```";
|
||||
const minifiedJson = JSON.stringify(msgs);
|
||||
|
||||
const copy = () => {
|
||||
copyToClipboard(minifiedJson);
|
||||
};
|
||||
const download = () => {
|
||||
downloadAs(JSON.stringify(msgs), `${props.topic}.json`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewActions
|
||||
copy={copy}
|
||||
download={download}
|
||||
showCopy={false}
|
||||
messages={props.messages}
|
||||
/>
|
||||
<div className="markdown-body" onClick={copy}>
|
||||
<Markdown content={mdText} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
color: var(--black);
|
||||
background-color: var(--white);
|
||||
min-width: 600px;
|
||||
min-height: 480px;
|
||||
min-height: 370px;
|
||||
max-width: 1200px;
|
||||
|
||||
display: flex;
|
||||
|
@@ -12,10 +12,10 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import { getCSSVar, useMobileScreen } from "../utils";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { Path, SlotID } from "../constant";
|
||||
import { ModelProvider, Path, SlotID } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
import { getLang } from "../locales";
|
||||
import { getISOLang, getLang } from "../locales";
|
||||
|
||||
import {
|
||||
HashRouter as Router,
|
||||
@@ -27,7 +27,7 @@ import { SideBar } from "./sidebar";
|
||||
import { useAppConfig } from "../store/config";
|
||||
import { AuthPage } from "./auth";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { api } from "../client/api";
|
||||
import { ClientApi } from "../client/api";
|
||||
import { useAccessStore } from "../store";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
@@ -86,6 +86,17 @@ export function useSwitchTheme() {
|
||||
}, [config.theme]);
|
||||
}
|
||||
|
||||
function useHtmlLang() {
|
||||
useEffect(() => {
|
||||
const lang = getISOLang();
|
||||
const htmlLang = document.documentElement.lang;
|
||||
|
||||
if (lang !== htmlLang) {
|
||||
document.documentElement.lang = lang;
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
const useHasHydrated = () => {
|
||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||
|
||||
@@ -104,7 +115,10 @@ const loadAsyncGoogleFont = () => {
|
||||
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
||||
linkEl.rel = "stylesheet";
|
||||
linkEl.href =
|
||||
googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap";
|
||||
googleFontUrl +
|
||||
"/css2?family=" +
|
||||
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
|
||||
"&display=swap";
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
@@ -114,6 +128,8 @@ function Screen() {
|
||||
const isHome = location.pathname === Path.Home;
|
||||
const isAuth = location.pathname === Path.Auth;
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldTightBorder =
|
||||
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||
|
||||
useEffect(() => {
|
||||
loadAsyncGoogleFont();
|
||||
@@ -123,11 +139,9 @@ function Screen() {
|
||||
<div
|
||||
className={
|
||||
styles.container +
|
||||
` ${
|
||||
config.tightBorder && !isMobileScreen
|
||||
? styles["tight-container"]
|
||||
: styles.container
|
||||
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`
|
||||
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
||||
getLang() === "ar" ? styles["rtl-screen"] : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{isAuth ? (
|
||||
@@ -156,6 +170,12 @@ function Screen() {
|
||||
export function useLoadData() {
|
||||
const config = useAppConfig();
|
||||
|
||||
var api: ClientApi;
|
||||
if (config.modelConfig.model === "gemini-pro") {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const models = await api.llm.models();
|
||||
@@ -168,6 +188,7 @@ export function useLoadData() {
|
||||
export function Home() {
|
||||
useSwitchTheme();
|
||||
useLoadData();
|
||||
useHtmlLang();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Config] got config from build time", getClientConfig());
|
||||
|
@@ -5,13 +5,13 @@ import RemarkBreaks from "remark-breaks";
|
||||
import RehypeKatex from "rehype-katex";
|
||||
import RemarkGfm from "remark-gfm";
|
||||
import RehypeHighlight from "rehype-highlight";
|
||||
import { useRef, useState, RefObject, useEffect } from "react";
|
||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import React from "react";
|
||||
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { showImageModal } from "./ui-lib";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
@@ -38,12 +38,6 @@ export function Mermaid(props: { code: string }) {
|
||||
if (!svg) return;
|
||||
const text = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([text], { type: "image/svg+xml" });
|
||||
console.log(blob);
|
||||
// const url = URL.createObjectURL(blob);
|
||||
// const win = window.open(url);
|
||||
// if (win) {
|
||||
// win.onload = () => URL.revokeObjectURL(url);
|
||||
// }
|
||||
showImageModal(URL.createObjectURL(blob));
|
||||
}
|
||||
|
||||
@@ -105,7 +99,29 @@ export function PreCode(props: { children: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
function escapeDollarNumber(text: string) {
|
||||
let escapedText = "";
|
||||
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
let char = text[i];
|
||||
const nextChar = text[i + 1] || " ";
|
||||
|
||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
||||
char = "\\$";
|
||||
}
|
||||
|
||||
escapedText += char;
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(
|
||||
() => escapeDollarNumber(props.content),
|
||||
[props.content],
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
@@ -121,6 +137,7 @@ function _MarkDownContent(props: { content: string }) {
|
||||
]}
|
||||
components={{
|
||||
pre: PreCode,
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
a: (aProps) => {
|
||||
const href = aProps.href || "";
|
||||
const isInternal = /^\/#/i.test(href);
|
||||
@@ -129,7 +146,7 @@ function _MarkDownContent(props: { content: string }) {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.content}
|
||||
{escapedContent}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
@@ -152,11 +169,11 @@ export function Markdown(
|
||||
className="markdown-body"
|
||||
style={{
|
||||
fontSize: `${props.fontSize ?? 14}px`,
|
||||
direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr",
|
||||
}}
|
||||
ref={mdRef}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||
dir="auto"
|
||||
>
|
||||
{props.loading ? (
|
||||
<LoadingIcon />
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
ChatMessage,
|
||||
createMessage,
|
||||
ModelConfig,
|
||||
ModelType,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
} from "../store";
|
||||
@@ -58,11 +59,11 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function MaskAvatar(props: { mask: Mask }) {
|
||||
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||
<Avatar avatar={props.mask.avatar} />
|
||||
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
|
||||
return props.avatar !== DEFAULT_MASK_AVATAR ? (
|
||||
<Avatar avatar={props.avatar} />
|
||||
) : (
|
||||
<Avatar model={props.mask.modelConfig.model} />
|
||||
<Avatar model={props.model} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +124,10 @@ export function MaskConfig(props: {
|
||||
onClick={() => setShowPicker(true)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</ListItem>
|
||||
@@ -393,11 +397,13 @@ export function MaskPage() {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const masks = searchText.length > 0 ? searchMasks : allMasks;
|
||||
|
||||
// simple search, will refactor later
|
||||
// refactored already, now it accurate
|
||||
const onSearch = (text: string) => {
|
||||
setSearchText(text);
|
||||
if (text.length > 0) {
|
||||
const result = allMasks.filter((m) => m.name.includes(text));
|
||||
const result = allMasks.filter((m) =>
|
||||
m.name.toLowerCase().includes(text.toLowerCase()),
|
||||
);
|
||||
setSearchMasks(result);
|
||||
} else {
|
||||
setSearchMasks(allMasks);
|
||||
@@ -410,7 +416,7 @@ export function MaskPage() {
|
||||
const closeMaskModal = () => setEditingMaskId(undefined);
|
||||
|
||||
const downloadAll = () => {
|
||||
downloadAs(JSON.stringify(masks), FileName.Masks);
|
||||
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
|
||||
};
|
||||
|
||||
const importFromFile = () => {
|
||||
@@ -452,11 +458,13 @@ export function MaskPage() {
|
||||
icon={<DownloadIcon />}
|
||||
bordered
|
||||
onClick={downloadAll}
|
||||
text={Locale.UI.Export}
|
||||
/>
|
||||
</div>
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<UploadIcon />}
|
||||
text={Locale.UI.Import}
|
||||
bordered
|
||||
onClick={() => importFromFile()}
|
||||
/>
|
||||
@@ -519,7 +527,7 @@ export function MaskPage() {
|
||||
<div className={styles["mask-item"]} key={m.id}>
|
||||
<div className={styles["mask-header"]}>
|
||||
<div className={styles["mask-icon"]}>
|
||||
<MaskAvatar mask={m} />
|
||||
<MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
|
||||
</div>
|
||||
<div className={styles["mask-title"]}>
|
||||
<div className={styles["mask-name"]}>{m.name}</div>
|
||||
@@ -604,7 +612,7 @@ export function MaskPage() {
|
||||
<MaskConfig
|
||||
mask={editingMask}
|
||||
updateMask={(updater) =>
|
||||
maskStore.update(editingMaskId!, updater)
|
||||
maskStore.updateMask(editingMaskId!, updater)
|
||||
}
|
||||
readonly={editingMask.builtin}
|
||||
/>
|
||||
|
@@ -58,8 +58,8 @@
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
max-width: calc(100% - 40px);
|
||||
flex: 1;
|
||||
max-width: calc(100% - 80px);
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
@@ -71,6 +71,12 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import { Updater } from "../typing";
|
||||
import { IconButton } from "./button";
|
||||
@@ -73,11 +73,23 @@ export function MessageSelector(props: {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
||||
const messages = session.messages.filter(
|
||||
(m, i) =>
|
||||
m.id && // message must have id
|
||||
isValid(m) &&
|
||||
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
|
||||
const allMessages = useMemo(() => {
|
||||
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
|
||||
if (startIndex === session.messages.length - 1) {
|
||||
startIndex = 0;
|
||||
}
|
||||
return session.messages.slice(startIndex);
|
||||
}, [session.messages, session.clearContextIndex]);
|
||||
|
||||
const messages = useMemo(
|
||||
() =>
|
||||
allMessages.filter(
|
||||
(m, i) =>
|
||||
m.id && // message must have id
|
||||
isValid(m) &&
|
||||
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
|
||||
),
|
||||
[allMessages],
|
||||
);
|
||||
const messageCount = messages.length;
|
||||
const config = useAppConfig();
|
||||
@@ -176,6 +188,8 @@ export function MessageSelector(props: {
|
||||
<div className={styles["messages"]}>
|
||||
{messages.map((m, i) => {
|
||||
if (!isInSearchResult(m.id!)) return null;
|
||||
const id = m.id ?? i;
|
||||
const isSelected = props.selection.has(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -185,7 +199,6 @@ export function MessageSelector(props: {
|
||||
key={i}
|
||||
onClick={() => {
|
||||
props.updateSelection((selection) => {
|
||||
const id = m.id ?? i;
|
||||
selection.has(id) ? selection.delete(id) : selection.add(id);
|
||||
});
|
||||
onClickIndex(i);
|
||||
@@ -195,7 +208,10 @@ export function MessageSelector(props: {
|
||||
{m.role === "user" ? (
|
||||
<Avatar avatar={config.avatar}></Avatar>
|
||||
) : (
|
||||
<MaskAvatar mask={session.mask} />
|
||||
<MaskAvatar
|
||||
avatar={session.mask.avatar}
|
||||
model={m.model || session.mask.modelConfig.model}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles["body"]}>
|
||||
@@ -206,6 +222,10 @@ export function MessageSelector(props: {
|
||||
{m.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles["checkbox"]}>
|
||||
<input type="checkbox" checked={isSelected}></input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import { ModalConfigValidator, ModelConfig, useAppConfig } from "../store";
|
||||
import { ModalConfigValidator, ModelConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { InputRange } from "./input-range";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
|
||||
export function ModelConfigList(props: {
|
||||
modelConfig: ModelConfig;
|
||||
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const allModels = useAllModels();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -24,11 +25,13 @@ export function ModelConfigList(props: {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{config.allModels().map((v, i) => (
|
||||
<option value={v.name} key={i} disabled={!v.available}>
|
||||
{v.name}
|
||||
</option>
|
||||
))}
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={v.name} key={i}>
|
||||
{v.displayName}({v.provider?.providerName})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
@@ -75,8 +78,8 @@ export function ModelConfigList(props: {
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min={100}
|
||||
max={100000}
|
||||
min={1024}
|
||||
max={512000}
|
||||
value={props.modelConfig.max_tokens}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
@@ -88,79 +91,84 @@ export function ModelConfigList(props: {
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.PresencePenalty.Title}
|
||||
subTitle={Locale.Settings.PresencePenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
value={props.modelConfig.presence_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.presence_penalty =
|
||||
ModalConfigValidator.presence_penalty(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.FrequencyPenalty.Title}
|
||||
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
value={props.modelConfig.frequency_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.frequency_penalty =
|
||||
ModalConfigValidator.frequency_penalty(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
{props.modelConfig.model === "gemini-pro" ? null : (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.PresencePenalty.Title}
|
||||
subTitle={Locale.Settings.PresencePenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
value={props.modelConfig.presence_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.presence_penalty =
|
||||
ModalConfigValidator.presence_penalty(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.InjectSystemPrompts.Title}
|
||||
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.modelConfig.enableInjectSystemPrompts}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.enableInjectSystemPrompts = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.FrequencyPenalty.Title}
|
||||
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
value={props.modelConfig.frequency_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.frequency_penalty =
|
||||
ModalConfigValidator.frequency_penalty(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.InputTemplate.Title}
|
||||
subTitle={Locale.Settings.InputTemplate.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={props.modelConfig.template}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.template = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.InjectSystemPrompts.Title}
|
||||
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.modelConfig.enableInjectSystemPrompts}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.enableInjectSystemPrompts =
|
||||
e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.InputTemplate.Title}
|
||||
subTitle={Locale.Settings.InputTemplate.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={props.modelConfig.template}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.template = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
<ListItem
|
||||
title={Locale.Settings.HistoryCount.Title}
|
||||
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
||||
|
@@ -17,21 +17,13 @@ import { useCommand } from "../command";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
|
||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
||||
const xmin = Math.max(aRect.x, bRect.x);
|
||||
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
|
||||
const ymin = Math.max(aRect.y, bRect.y);
|
||||
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
|
||||
const width = xmax - xmin;
|
||||
const height = ymax - ymin;
|
||||
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
|
||||
return intersectionArea;
|
||||
}
|
||||
|
||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||||
return (
|
||||
<div className={styles["mask"]} onClick={props.onClick}>
|
||||
<MaskAvatar mask={props.mask} />
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -10,6 +10,15 @@ import ClearIcon from "../icons/clear.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import EditIcon from "../icons/edit.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import UploadIcon from "../icons/upload.svg";
|
||||
import ConfigIcon from "../icons/config.svg";
|
||||
import ConfirmIcon from "../icons/confirm.svg";
|
||||
|
||||
import ConnectionIcon from "../icons/connection.svg";
|
||||
import CloudSuccessIcon from "../icons/cloud-success.svg";
|
||||
import CloudFailIcon from "../icons/cloud-fail.svg";
|
||||
|
||||
import {
|
||||
Input,
|
||||
List,
|
||||
@@ -19,6 +28,7 @@ import {
|
||||
Popover,
|
||||
Select,
|
||||
showConfirm,
|
||||
showToast,
|
||||
} from "./ui-lib";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
|
||||
@@ -40,7 +50,17 @@ import Locale, {
|
||||
} from "../locales";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import Link from "next/link";
|
||||
import { Path, RELEASE_URL, UPDATE_URL } from "../constant";
|
||||
import {
|
||||
Azure,
|
||||
Google,
|
||||
OPENAI_BASE_URL,
|
||||
Path,
|
||||
RELEASE_URL,
|
||||
STORAGE_KEY,
|
||||
ServiceProvider,
|
||||
SlotID,
|
||||
UPDATE_URL,
|
||||
} from "../constant";
|
||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||
import { ErrorBoundary } from "./error";
|
||||
import { InputRange } from "./input-range";
|
||||
@@ -49,6 +69,8 @@ import { Avatar, AvatarPicker } from "./emoji";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { ProviderType } from "../utils/cloud";
|
||||
|
||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@@ -75,7 +97,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
readOnly={!prompt.isUser}
|
||||
className={styles["edit-prompt-title"]}
|
||||
onInput={(e) =>
|
||||
promptStore.update(
|
||||
promptStore.updatePrompt(
|
||||
props.id,
|
||||
(prompt) => (prompt.title = e.currentTarget.value),
|
||||
)
|
||||
@@ -87,7 +109,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
className={styles["edit-prompt-content"]}
|
||||
rows={10}
|
||||
onInput={(e) =>
|
||||
promptStore.update(
|
||||
promptStore.updatePrompt(
|
||||
props.id,
|
||||
(prompt) => (prompt.content = e.currentTarget.value),
|
||||
)
|
||||
@@ -127,14 +149,15 @@ function UserPromptModal(props: { onClose?: () => void }) {
|
||||
actions={[
|
||||
<IconButton
|
||||
key="add"
|
||||
onClick={() =>
|
||||
promptStore.add({
|
||||
onClick={() => {
|
||||
const promptId = promptStore.add({
|
||||
id: nanoid(),
|
||||
createdAt: Date.now(),
|
||||
title: "Empty Prompt",
|
||||
content: "Empty Prompt Content",
|
||||
})
|
||||
}
|
||||
});
|
||||
setEditingPromptId(promptId);
|
||||
}}
|
||||
icon={<AddIcon />}
|
||||
bordered
|
||||
text={Locale.Settings.Prompt.Modal.Add}
|
||||
@@ -241,75 +264,297 @@ function DangerItems() {
|
||||
);
|
||||
}
|
||||
|
||||
function SyncItems() {
|
||||
function CheckButton() {
|
||||
const syncStore = useSyncStore();
|
||||
const webdav = syncStore.webDavConfig;
|
||||
|
||||
// not ready: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/920#issuecomment-1609866332
|
||||
return null;
|
||||
const couldCheck = useMemo(() => {
|
||||
return syncStore.coundSync();
|
||||
}, [syncStore]);
|
||||
|
||||
const [checkState, setCheckState] = useState<
|
||||
"none" | "checking" | "success" | "failed"
|
||||
>("none");
|
||||
|
||||
async function check() {
|
||||
setCheckState("checking");
|
||||
const valid = await syncStore.check();
|
||||
setCheckState(valid ? "success" : "failed");
|
||||
}
|
||||
|
||||
if (!couldCheck) return null;
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListItem
|
||||
title={"上次同步:" + new Date().toLocaleString()}
|
||||
subTitle={"20 次对话,100 条消息,200 提示词,20 面具"}
|
||||
<IconButton
|
||||
text={Locale.Settings.Sync.Config.Modal.Check}
|
||||
bordered
|
||||
onClick={check}
|
||||
icon={
|
||||
checkState === "none" ? (
|
||||
<ConnectionIcon />
|
||||
) : checkState === "checking" ? (
|
||||
<LoadingIcon />
|
||||
) : checkState === "success" ? (
|
||||
<CloudSuccessIcon />
|
||||
) : checkState === "failed" ? (
|
||||
<CloudFailIcon />
|
||||
) : (
|
||||
<ConnectionIcon />
|
||||
)
|
||||
}
|
||||
></IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SyncConfigModal(props: { onClose?: () => void }) {
|
||||
const syncStore = useSyncStore();
|
||||
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Settings.Sync.Config.Modal.Title}
|
||||
onClose={() => props.onClose?.()}
|
||||
actions={[
|
||||
<CheckButton key="check" />,
|
||||
<IconButton
|
||||
key="confirm"
|
||||
onClick={props.onClose}
|
||||
icon={<ConfirmIcon />}
|
||||
bordered
|
||||
text={Locale.UI.Confirm}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
icon={<ResetIcon />}
|
||||
text="同步"
|
||||
onClick={() => {
|
||||
syncStore.check().then(console.log);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<List>
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.SyncType.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
|
||||
>
|
||||
<select
|
||||
value={syncStore.provider}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.provider = e.target.value as ProviderType),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Object.entries(ProviderType).map(([k, v]) => (
|
||||
<option value={v} key={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={"本地备份"}
|
||||
subTitle={"20 次对话,100 条消息,200 提示词,20 面具"}
|
||||
></ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={syncStore.useProxy}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.useProxy = e.currentTarget.checked),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
{syncStore.useProxy ? (
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.ProxyUrl.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.proxyUrl}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.proxyUrl = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
) : null}
|
||||
</List>
|
||||
|
||||
<ListItem
|
||||
title={"Web Dav Server"}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
>
|
||||
<input
|
||||
value={webdav.server}
|
||||
type="text"
|
||||
placeholder={"https://example.com"}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.server = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{syncStore.provider === ProviderType.WebDAV && (
|
||||
<>
|
||||
<List>
|
||||
<ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.webdav.endpoint}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.webdav.endpoint = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title="Web Dav User Name" subTitle="user name here">
|
||||
<input
|
||||
value={webdav.username}
|
||||
type="text"
|
||||
placeholder={"username"}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.username = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.webdav.username}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.webdav.username = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
|
||||
<PasswordInput
|
||||
value={syncStore.webdav.password}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.webdav.password = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></PasswordInput>
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ListItem title="Web Dav Password" subTitle="password here">
|
||||
<input
|
||||
value={webdav.password}
|
||||
type="text"
|
||||
placeholder={"password"}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.password = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
{syncStore.provider === ProviderType.UpStash && (
|
||||
<List>
|
||||
<ListItem title={Locale.Settings.Sync.Config.UpStash.Endpoint}>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.upstash.endpoint}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.upstash.endpoint = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.upstash.username}
|
||||
placeholder={STORAGE_KEY}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.upstash.username = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
|
||||
<PasswordInput
|
||||
value={syncStore.upstash.apiKey}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.upstash.apiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></PasswordInput>
|
||||
</ListItem>
|
||||
</List>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SyncItems() {
|
||||
const syncStore = useSyncStore();
|
||||
const chatStore = useChatStore();
|
||||
const promptStore = usePromptStore();
|
||||
const maskStore = useMaskStore();
|
||||
const couldSync = useMemo(() => {
|
||||
return syncStore.coundSync();
|
||||
}, [syncStore]);
|
||||
|
||||
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
|
||||
|
||||
const stateOverview = useMemo(() => {
|
||||
const sessions = chatStore.sessions;
|
||||
const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
|
||||
|
||||
return {
|
||||
chat: sessions.length,
|
||||
message: messageCount,
|
||||
prompt: Object.keys(promptStore.prompts).length,
|
||||
mask: Object.keys(maskStore.masks).length,
|
||||
};
|
||||
}, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.CloudState}
|
||||
subTitle={
|
||||
syncStore.lastProvider
|
||||
? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
|
||||
syncStore.lastProvider
|
||||
}]`
|
||||
: Locale.Settings.Sync.NotSyncYet
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<IconButton
|
||||
icon={<ConfigIcon />}
|
||||
text={Locale.UI.Config}
|
||||
onClick={() => {
|
||||
setShowSyncConfigModal(true);
|
||||
}}
|
||||
/>
|
||||
{couldSync && (
|
||||
<IconButton
|
||||
icon={<ResetIcon />}
|
||||
text={Locale.UI.Sync}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await syncStore.sync();
|
||||
showToast(Locale.Settings.Sync.Success);
|
||||
} catch (e) {
|
||||
showToast(Locale.Settings.Sync.Fail);
|
||||
console.error("[Sync]", e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.LocalState}
|
||||
subTitle={Locale.Settings.Sync.Overview(stateOverview)}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<IconButton
|
||||
icon={<UploadIcon />}
|
||||
text={Locale.UI.Export}
|
||||
onClick={() => {
|
||||
syncStore.export();
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<DownloadIcon />}
|
||||
text={Locale.UI.Import}
|
||||
onClick={() => {
|
||||
syncStore.import();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{showSyncConfigModal && (
|
||||
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,13 +581,28 @@ export function Settings() {
|
||||
console.log("[Update] remote version ", updateStore.remoteVersion);
|
||||
}
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const shouldHideBalanceQuery = useMemo(() => {
|
||||
const isOpenAiUrl = accessStore.openaiUrl.includes(OPENAI_BASE_URL);
|
||||
|
||||
return (
|
||||
accessStore.hideBalanceQuery ||
|
||||
isOpenAiUrl ||
|
||||
accessStore.provider === ServiceProvider.Azure
|
||||
);
|
||||
}, [
|
||||
accessStore.hideBalanceQuery,
|
||||
accessStore.openaiUrl,
|
||||
accessStore.provider,
|
||||
]);
|
||||
|
||||
const usage = {
|
||||
used: updateStore.used,
|
||||
subscription: updateStore.subscription,
|
||||
};
|
||||
const [loadingUsage, setLoadingUsage] = useState(false);
|
||||
function checkUsage(force = false) {
|
||||
if (accessStore.hideBalanceQuery) {
|
||||
if (shouldHideBalanceQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -352,7 +612,6 @@ export function Settings() {
|
||||
});
|
||||
}
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const enabledAccessControl = useMemo(
|
||||
() => accessStore.enabledAccessControl(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -378,6 +637,12 @@ export function Settings() {
|
||||
navigate(Path.Home);
|
||||
}
|
||||
};
|
||||
if (clientConfig?.isApp) {
|
||||
// Force to set custom endpoint to true if it's app
|
||||
accessStore.update((state) => {
|
||||
state.useCustomConfig = true;
|
||||
});
|
||||
}
|
||||
document.addEventListener("keydown", keydownEvent);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", keydownEvent);
|
||||
@@ -518,7 +783,7 @@ export function Settings() {
|
||||
title={`${config.fontSize ?? 14}px`}
|
||||
value={config.fontSize}
|
||||
min="12"
|
||||
max="18"
|
||||
max="40"
|
||||
step="1"
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
@@ -529,6 +794,22 @@ export function Settings() {
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.AutoGenerateTitle.Title}
|
||||
subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enableAutoGenerateTitle}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.enableAutoGenerateTitle = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.SendPreviewBubble.Title}
|
||||
subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
|
||||
@@ -546,6 +827,8 @@ export function Settings() {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
title={Locale.Settings.Mask.Splash.Title}
|
||||
@@ -613,57 +896,235 @@ export function Settings() {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<List>
|
||||
{showAccessCode ? (
|
||||
<List id={SlotID.CustomModel}>
|
||||
{showAccessCode && (
|
||||
<ListItem
|
||||
title={Locale.Settings.AccessCode.Title}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
title={Locale.Settings.Access.AccessCode.Title}
|
||||
subTitle={Locale.Settings.Access.AccessCode.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.AccessCode.Placeholder}
|
||||
placeholder={Locale.Settings.Access.AccessCode.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateCode(e.currentTarget.value);
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
{!accessStore.hideUserApiKey && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Endpoint.Title}
|
||||
subTitle={Locale.Settings.Endpoint.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.openaiUrl}
|
||||
placeholder="https://api.openai.com/"
|
||||
onChange={(e) =>
|
||||
accessStore.updateOpenAiUrl(e.currentTarget.value)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Token.Title}
|
||||
subTitle={Locale.Settings.Token.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.token}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Token.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateToken(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
) : null}
|
||||
{
|
||||
// Conditionally render the following ListItem based on clientConfig.isApp
|
||||
!clientConfig?.isApp && ( // only show if isApp is false
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.CustomEndpoint.Title}
|
||||
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={accessStore.useCustomConfig}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.useCustomConfig = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
{accessStore.useCustomConfig && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Provider.Title}
|
||||
subTitle={Locale.Settings.Access.Provider.SubTitle}
|
||||
>
|
||||
<Select
|
||||
value={accessStore.provider}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.provider = e.target
|
||||
.value as ServiceProvider),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Object.entries(ServiceProvider).map(([k, v]) => (
|
||||
<option value={v} key={k}>
|
||||
{k}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
{!accessStore.hideBalanceQuery ? (
|
||||
{accessStore.provider === "OpenAI" ? (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.OpenAI.Endpoint.SubTitle
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.openaiUrl}
|
||||
placeholder={OPENAI_BASE_URL}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.openaiUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.OpenAI.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.openaiApiKey}
|
||||
type="text"
|
||||
placeholder={
|
||||
Locale.Settings.Access.OpenAI.ApiKey.Placeholder
|
||||
}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
) : accessStore.provider === "Azure" ? (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Azure.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Azure.Endpoint.SubTitle +
|
||||
Azure.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.azureUrl}
|
||||
placeholder={Azure.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.azureUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Azure.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.azureApiKey}
|
||||
type="text"
|
||||
placeholder={
|
||||
Locale.Settings.Access.Azure.ApiKey.Placeholder
|
||||
}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.azureApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Azure.ApiVerion.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Azure.ApiVerion.SubTitle
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.azureApiVersion}
|
||||
placeholder="2023-08-01-preview"
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.azureApiVersion =
|
||||
e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
) : accessStore.provider === "Google" ? (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Google.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Google.Endpoint.SubTitle +
|
||||
Google.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.googleUrl}
|
||||
placeholder={Google.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.googleUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Azure.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.googleApiKey}
|
||||
type="text"
|
||||
placeholder={
|
||||
Locale.Settings.Access.Google.ApiKey.Placeholder
|
||||
}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.googleApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Google.ApiVerion.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Google.ApiVerion.SubTitle
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.googleApiVersion}
|
||||
placeholder="2023-08-01-preview"
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.googleApiVersion =
|
||||
e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!shouldHideBalanceQuery && !clientConfig?.isApp ? (
|
||||
<ListItem
|
||||
title={Locale.Settings.Usage.Title}
|
||||
subTitle={
|
||||
@@ -690,8 +1151,8 @@ export function Settings() {
|
||||
) : null}
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.CustomModel.Title}
|
||||
subTitle={Locale.Settings.CustomModel.SubTitle}
|
||||
title={Locale.Settings.Access.CustomModel.Title}
|
||||
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
@@ -706,8 +1167,6 @@ export function Settings() {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
|
||||
<List>
|
||||
<ModelConfigList
|
||||
modelConfig={config.modelConfig}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
@@ -8,6 +8,7 @@ import GithubIcon from "../icons/github.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaskIcon from "../icons/mask.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import DragIcon from "../icons/drag.svg";
|
||||
@@ -17,6 +18,7 @@ import Locale from "../locales";
|
||||
import { useAppConfig, useChatStore } from "../store";
|
||||
|
||||
import {
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
} from "../constant";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useMobileScreen } from "../utils";
|
||||
import { isIOS, useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import { showConfirm, showToast } from "./ui-lib";
|
||||
|
||||
@@ -57,31 +59,57 @@ function useDragSideBar() {
|
||||
|
||||
const config = useAppConfig();
|
||||
const startX = useRef(0);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const lastUpdateTime = useRef(Date.now());
|
||||
|
||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 50) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => (config.sidebarWidth = nextWidth));
|
||||
});
|
||||
|
||||
const handleMouseUp = useRef(() => {
|
||||
startDragWidth.current = config.sidebarWidth ?? 300;
|
||||
window.removeEventListener("mousemove", handleMouseMove.current);
|
||||
window.removeEventListener("mouseup", handleMouseUp.current);
|
||||
});
|
||||
|
||||
const onDragMouseDown = (e: MouseEvent) => {
|
||||
startX.current = e.clientX;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove.current);
|
||||
window.addEventListener("mouseup", handleMouseUp.current);
|
||||
const toggleSideBar = () => {
|
||||
config.update((config) => {
|
||||
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDragStart = (e: MouseEvent) => {
|
||||
// Remembers the initial width each time the mouse is pressed
|
||||
startX.current = e.clientX;
|
||||
startDragWidth.current = config.sidebarWidth;
|
||||
const dragStartTime = Date.now();
|
||||
|
||||
const handleDragMove = (e: MouseEvent) => {
|
||||
if (Date.now() < lastUpdateTime.current + 20) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime.current = Date.now();
|
||||
const d = e.clientX - startX.current;
|
||||
const nextWidth = limit(startDragWidth.current + d);
|
||||
config.update((config) => {
|
||||
if (nextWidth < MIN_SIDEBAR_WIDTH) {
|
||||
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||
} else {
|
||||
config.sidebarWidth = nextWidth;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
|
||||
window.removeEventListener("pointermove", handleDragMove);
|
||||
window.removeEventListener("pointerup", handleDragEnd);
|
||||
|
||||
// if user click the drag icon, should toggle the sidebar
|
||||
const shouldFireClick = Date.now() - dragStartTime < 300;
|
||||
if (shouldFireClick) {
|
||||
toggleSideBar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleDragMove);
|
||||
window.addEventListener("pointerup", handleDragEnd);
|
||||
};
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldNarrow =
|
||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||
@@ -89,13 +117,13 @@ function useDragSideBar() {
|
||||
useEffect(() => {
|
||||
const barWidth = shouldNarrow
|
||||
? NARROW_SIDEBAR_WIDTH
|
||||
: limit(config.sidebarWidth ?? 300);
|
||||
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||
|
||||
return {
|
||||
onDragMouseDown,
|
||||
onDragStart,
|
||||
shouldNarrow,
|
||||
};
|
||||
}
|
||||
@@ -104,9 +132,14 @@ export function SideBar(props: { className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
// drag side bar
|
||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const isIOSMobile = useMemo(
|
||||
() => isIOS() && isMobileScreen,
|
||||
[isMobileScreen],
|
||||
);
|
||||
|
||||
useHotKey();
|
||||
|
||||
@@ -115,10 +148,14 @@ export function SideBar(props: { className?: string }) {
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
style={{
|
||||
// #3016 disable transition on ios mobile screen
|
||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||
}}
|
||||
>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
ChatGPT Next
|
||||
NextChat
|
||||
</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
@@ -133,7 +170,13 @@ export function SideBar(props: { className?: string }) {
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
<IconButton
|
||||
@@ -160,7 +203,7 @@ export function SideBar(props: { className?: string }) {
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
icon={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||
@@ -174,7 +217,7 @@ export function SideBar(props: { className?: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href={REPO_URL} target="_blank">
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
@@ -198,7 +241,7 @@ export function SideBar(props: { className?: string }) {
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
|
@@ -235,7 +235,7 @@
|
||||
.select-with-icon-select {
|
||||
height: 100%;
|
||||
border: var(--border-in-light);
|
||||
padding: 10px 25px 10px 10px;
|
||||
padding: 10px 35px 10px 10px;
|
||||
border-radius: 10px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
|
@@ -70,14 +70,12 @@ export function ListItem(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function List(props: {
|
||||
children:
|
||||
| Array<JSX.Element | null | undefined>
|
||||
| JSX.Element
|
||||
| null
|
||||
| undefined;
|
||||
}) {
|
||||
return <div className={styles.list}>{props.children}</div>;
|
||||
export function List(props: { children: React.ReactNode; id?: string }) {
|
||||
return (
|
||||
<div className={styles.list} id={props.id}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Loading() {
|
||||
@@ -99,8 +97,9 @@ export function Loading() {
|
||||
interface ModalProps {
|
||||
title: string;
|
||||
children?: any;
|
||||
actions?: JSX.Element[];
|
||||
actions?: React.ReactNode[];
|
||||
defaultMax?: boolean;
|
||||
footer?: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function Modal(props: ModalProps) {
|
||||
@@ -149,6 +148,7 @@ export function Modal(props: ModalProps) {
|
||||
<div className={styles["modal-content"]}>{props.children}</div>
|
||||
|
||||
<div className={styles["modal-footer"]}>
|
||||
{props.footer}
|
||||
<div className={styles["modal-actions"]}>
|
||||
{props.actions?.map((action, i) => (
|
||||
<div key={i} className={styles["modal-action"]}>
|
||||
|
@@ -1,18 +1,35 @@
|
||||
import md5 from "spark-md5";
|
||||
import { DEFAULT_MODELS } from "../constant";
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
PROXY_URL?: string; // docker only
|
||||
|
||||
OPENAI_API_KEY?: string;
|
||||
CODE?: string;
|
||||
|
||||
BASE_URL?: string;
|
||||
PROXY_URL?: string;
|
||||
OPENAI_ORG_ID?: string; // openai only
|
||||
|
||||
VERCEL?: string;
|
||||
HIDE_USER_API_KEY?: string; // disable user's api key input
|
||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||
BUILD_MODE?: "standalone" | "export";
|
||||
BUILD_APP?: string; // is building desktop app
|
||||
HIDE_BALANCE_QUERY?: string; // allow user to query balance or not
|
||||
|
||||
HIDE_USER_API_KEY?: string; // disable user's api key input
|
||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
|
||||
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
|
||||
CUSTOM_MODELS?: string; // to control custom models
|
||||
|
||||
// azure only
|
||||
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
||||
AZURE_API_KEY?: string;
|
||||
AZURE_API_VERSION?: string;
|
||||
|
||||
// google only
|
||||
GOOGLE_API_KEY?: string;
|
||||
GOOGLE_URL?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,16 +54,54 @@ export const getServerSideConfig = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const disableGPT4 = !!process.env.DISABLE_GPT4;
|
||||
let customModels = process.env.CUSTOM_MODELS ?? "";
|
||||
|
||||
if (disableGPT4) {
|
||||
if (customModels) customModels += ",";
|
||||
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
|
||||
.map((m) => "-" + m.name)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
|
||||
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||
const apiKey = apiKeys[randomIndex];
|
||||
console.log(
|
||||
`[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||
);
|
||||
|
||||
return {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
baseUrl: process.env.BASE_URL,
|
||||
apiKey,
|
||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||
|
||||
isAzure,
|
||||
azureUrl: process.env.AZURE_URL,
|
||||
azureApiKey: process.env.AZURE_API_KEY,
|
||||
azureApiVersion: process.env.AZURE_API_VERSION,
|
||||
|
||||
isGoogle,
|
||||
googleApiKey: process.env.GOOGLE_API_KEY,
|
||||
googleUrl: process.env.GOOGLE_URL,
|
||||
|
||||
gtmId: process.env.GTM_ID,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
code: process.env.CODE,
|
||||
codes: ACCESS_CODES,
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
baseUrl: process.env.BASE_URL,
|
||||
|
||||
proxyUrl: process.env.PROXY_URL,
|
||||
isVercel: !!process.env.VERCEL,
|
||||
|
||||
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
||||
disableGPT4: !!process.env.DISABLE_GPT4,
|
||||
hideBalanceQuery: !!process.env.HIDE_BALANCE_QUERY,
|
||||
disableGPT4,
|
||||
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
};
|
||||
};
|
||||
|
145
app/constant.ts
145
app/constant.ts
@@ -7,7 +7,12 @@ export const RELEASE_URL = `${REPO_URL}/releases`;
|
||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?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 DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy";
|
||||
|
||||
export const DEFAULT_CORS_HOST = "https://a.nextweb.fun";
|
||||
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
|
||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||
|
||||
export enum Path {
|
||||
Home = "/",
|
||||
@@ -18,8 +23,14 @@ export enum Path {
|
||||
Auth = "/auth",
|
||||
}
|
||||
|
||||
export enum ApiPath {
|
||||
Cors = "/api/cors",
|
||||
OpenAI = "/api/openai",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
AppBody = "app-body",
|
||||
CustomModel = "custom-model",
|
||||
}
|
||||
|
||||
export enum FileName {
|
||||
@@ -37,6 +48,7 @@ export enum StoreKey {
|
||||
Sync = "sync",
|
||||
}
|
||||
|
||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||
export const MAX_SIDEBAR_WIDTH = 500;
|
||||
export const MIN_SIDEBAR_WIDTH = 230;
|
||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||
@@ -44,11 +56,25 @@ export const NARROW_SIDEBAR_WIDTH = 100;
|
||||
export const ACCESS_CODE_PREFIX = "nk-";
|
||||
|
||||
export const LAST_INPUT_KEY = "last-input";
|
||||
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
|
||||
|
||||
export const STORAGE_KEY = "chatgpt-next-web";
|
||||
|
||||
export const REQUEST_TIMEOUT_MS = 60000;
|
||||
|
||||
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
||||
|
||||
export enum ServiceProvider {
|
||||
OpenAI = "OpenAI",
|
||||
Azure = "Azure",
|
||||
Google = "Google",
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
}
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
@@ -56,57 +82,170 @@ export const OpenaiPath = {
|
||||
ListModelPath: "v1/models",
|
||||
};
|
||||
|
||||
export const Azure = {
|
||||
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
|
||||
};
|
||||
|
||||
export const Google = {
|
||||
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
|
||||
ChatPath: "v1beta/models/gemini-pro:generateContent",
|
||||
|
||||
// /api/openai/v1/chat/completions
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||
You are ChatGPT, a large language model trained by OpenAI.
|
||||
Knowledge cutoff: 2021-09
|
||||
Knowledge cutoff: {{cutoff}}
|
||||
Current model: {{model}}
|
||||
Current time: {{time}}`;
|
||||
Current time: {{time}}
|
||||
Latex inline: $x^2$
|
||||
Latex block: $$e=mc^2$$
|
||||
`;
|
||||
|
||||
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
|
||||
|
||||
export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
default: "2021-09",
|
||||
"gpt-4-1106-preview": "2023-04",
|
||||
"gpt-4-vision-preview": "2023-04",
|
||||
};
|
||||
|
||||
export const DEFAULT_MODELS = [
|
||||
{
|
||||
name: "gpt-4",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0314",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0314",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-32k-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-1106-preview",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-4-vision-preview",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-1106",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gpt-3.5-turbo-16k-0613",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "openai",
|
||||
providerName: "OpenAI",
|
||||
providerType: "openai",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gemini-pro",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "google",
|
||||
providerName: "Google",
|
||||
providerType: "google",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
12
app/global.d.ts
vendored
12
app/global.d.ts
vendored
@@ -13,5 +13,17 @@ declare module "*.svg";
|
||||
declare interface Window {
|
||||
__TAURI__?: {
|
||||
writeText(text: string): Promise<void>;
|
||||
invoke(command: string, payload?: Record<string, unknown>): Promise<any>;
|
||||
dialog: {
|
||||
save(options?: Record<string, unknown>): Promise<string | null>;
|
||||
};
|
||||
fs: {
|
||||
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
|
||||
};
|
||||
notification:{
|
||||
requestPermission(): Promise<Permission>;
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
sendNotification(options: string | Options): void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
1
app/icons/cloud-fail.svg
Normal file
1
app/icons/cloud-fail.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.2 KiB |
1
app/icons/cloud-success.svg
Normal file
1
app/icons/cloud-success.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M4.00337,11.6633M4.00337,11.6633c-0.8391,0 -1.5514,-0.20763 -2.13691,-0.6229c-0.79984,-0.56727 -1.19975,-1.41519 -1.19975,-2.54377c0,-1.2521 0.52073,-2.20348 1.56218,-2.85415c0.68561,-0.42835 1.38711,-0.64252 2.10448,-0.64252v0.66667l-0.64163,-0.18098l0.64163,0.18098l-0.64163,-0.18098c0.65543,-2.32379 2.09264,-3.48569 4.31163,-3.48569c2.30635,0 3.74023,0.99523 4.30163,2.98569l-0.6416,0.18098l0.0729,-0.66267l-0.0729,0.66267l0.0729,-0.66267c2.3958,0.26354 3.5937,1.5411 3.5937,3.83267c0,2.21778 -1.10887,3.32667 -3.3266,3.32667c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.00853 -0.0846,-0.0212 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02427 -0.07007,-0.0519 -0.101,-0.0829c-0.031,-0.03093 -0.05863,-0.0646 -0.0829,-0.101c-0.02433,-0.0364 -0.04487,-0.07483 -0.0616,-0.1153c-0.0168,-0.0404 -0.02947,-0.08207 -0.038,-0.125c-0.00853,-0.04293 -0.0128,-0.0863 -0.0128,-0.1301c0,-0.04373 0.00427,-0.08707 0.0128,-0.13c0.00853,-0.04293 0.0212,-0.08463 0.038,-0.1251c0.01673,-0.04047 0.03727,-0.0789 0.0616,-0.1153c0.02427,-0.0364 0.0519,-0.07007 0.0829,-0.101c0.03093,-0.03093 0.0646,-0.05857 0.101,-0.0829c0.0364,-0.02433 0.07483,-0.04487 0.1153,-0.0616c0.0404,-0.01673 0.08207,-0.02937 0.125,-0.0379c0.04293,-0.00853 0.0863,-0.0128 0.1301,-0.0128c1.32887,0 1.9933,-0.66446 1.9933,-1.99337c0,-1.4951 -0.80207,-2.33088 -2.4062,-2.50733c-0.27,-0.02971 -0.495,-0.22026 0,0c-0.27,-0.02971 -0.495,-0.22026 -0.5688,-0.4817c-0.37873,-1.34287 -1.38484,-2.01431 -3.01833,-2.01431c-1.54613,0 -2.55559,0.8381 -3.02836,2.51431c-0.08103,0.28728 -0.34314,0.48569 0,0c-0.08103,0.28728 -0.34314,0.48569 -0.64164,0.48569c-0.46252,0 -0.92852,0.14666 -1.39801,0.43998c-0.62355,0.38957 -0.93532,0.96402 -0.93532,1.72336c0,0.66927 0.21258,1.15467 0.63775,1.45621c0.35449,0.25144 0.80969,0.37716 1.36558,0.37716c0.04378,0 0.08713,0.00427 0.13006,0.0128c0.04293,0.00853 0.08462,0.02117 0.12507,0.0379c0.04044,0.01673 0.07886,0.03727 0.11525,0.0616c0.0364,0.02433 0.07008,0.05197 0.10103,0.0829c0.03095,0.03093 0.05859,0.0646 0.08291,0.101c0.02432,0.0364 0.04485,0.07483 0.0616,0.1153c0.01675,0.04047 0.0294,0.08217 0.03794,0.1251c0.00854,0.04293 0.01281,0.08627 0.01281,0.13c0,0.0438 -0.00427,0.08717 -0.01281,0.1301c-0.00854,0.04293 -0.02119,0.0846 -0.03794,0.125c-0.01675,0.04047 -0.03728,0.0789 -0.0616,0.1153c-0.02432,0.0364 -0.05196,0.07007 -0.08291,0.101c-0.03095,0.031 -0.06463,0.05863 -0.10103,0.0829c-0.03639,0.02433 -0.07481,0.04487 -0.11525,0.0616c-0.04045,0.0168 -0.08214,0.02947 -0.12507,0.038c-0.04293,0.00853 -0.08628,0.0128 -0.13006,0.0128z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M6.42578,10.4903l2,1.66l-0.42578,0.513l-0.52012,-0.4171l2.67002,-3.32998c0.0274,-0.03415 0.05783,-0.06531 0.0913,-0.09346c0.03353,-0.02815 0.0695,-0.05277 0.1079,-0.07384c0.03833,-0.02107 0.07837,-0.0382 0.1201,-0.05138c0.04173,-0.01319 0.08437,-0.02217 0.1279,-0.02696c0.04353,-0.00479 0.0871,-0.00528 0.1307,-0.00149c0.0436,0.00379 0.0864,0.01181 0.1284,0.02404c0.04207,0.01223 0.0825,0.02844 0.1213,0.04863c0.03887,0.0202 0.07537,0.04399 0.1095,0.07137c0.0342,0.02738 0.06537,0.05783 0.0935,0.09135c0.02813,0.03352 0.05273,0.06946 0.0738,0.10783c0.02107,0.03837 0.0382,0.07843 0.0514,0.12017c0.0132,0.04174 0.0222,0.08437 0.027,0.12788c0.00473,0.04351 0.00523,0.08707 0.0015,0.13068c-0.0038,0.04361 -0.01183,0.08643 -0.0241,0.12846c-0.0122,0.04203 -0.0284,0.08247 -0.0486,0.1213c-0.0202,0.03884 -0.044,0.07534 -0.0714,0.10949l-2.66998,3.33001c-0.23308,0.2907 -0.65919,0.3339 -0.9459,0.0959l-2,-1.66c-0.03369,-0.02793 -0.06432,-0.0589 -0.0919,-0.0929c-0.02758,-0.034 -0.05158,-0.07037 -0.072,-0.1091c-0.02042,-0.03867 -0.03687,-0.079 -0.04934,-0.121c-0.01248,-0.04193 -0.02074,-0.0847 -0.02479,-0.1283c-0.00405,-0.0436 -0.0038,-0.08717 0.00073,-0.1307c0.00453,-0.04353 0.01326,-0.0862 0.0262,-0.128c0.01294,-0.0418 0.02983,-0.08197 0.05068,-0.1205c0.02085,-0.03847 0.04526,-0.07453 0.07321,-0.1082c0.02796,-0.03373 0.05893,-0.06437 0.09292,-0.0919c0.03399,-0.0276 0.07035,-0.0516 0.10907,-0.072c0.03872,-0.02047 0.07906,-0.03693 0.12102,-0.0494c0.04196,-0.01247 0.08473,-0.02073 0.12831,-0.0248c0.04359,-0.004 0.08715,-0.00373 0.13069,0.0008c0.04354,0.00453 0.08622,0.01327 0.12804,0.0262c0.04181,0.01293 0.08197,0.02983 0.12046,0.0507c0.03849,0.0208 0.07457,0.0452 0.10826,0.0732z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 4.7 KiB |
1
app/icons/config.svg
Normal file
1
app/icons/config.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
1
app/icons/connection.svg
Normal file
1
app/icons/connection.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.1 KiB |
@@ -3,10 +3,14 @@ import "./styles/globals.scss";
|
||||
import "./styles/markdown.scss";
|
||||
import "./styles/highlight.scss";
|
||||
import { getClientConfig } from "./config/client";
|
||||
import { type Metadata } from 'next';
|
||||
import { type Metadata } from "next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { getServerSideConfig } from "./config/server";
|
||||
import { GoogleTagManager } from "@next/third-parties/google";
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ChatGPT Next Web",
|
||||
title: "NextChat",
|
||||
description: "Your personal ChatGPT Chat Bot.",
|
||||
viewport: {
|
||||
width: "device-width",
|
||||
@@ -18,7 +22,7 @@ export const metadata: Metadata = {
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#151515" },
|
||||
],
|
||||
appleWebApp: {
|
||||
title: "ChatGPT Next Web",
|
||||
title: "NextChat",
|
||||
statusBarStyle: "default",
|
||||
},
|
||||
};
|
||||
@@ -35,7 +39,19 @@ export default function RootLayout({
|
||||
<link rel="manifest" href="/site.webmanifest"></link>
|
||||
<script src="/serviceWorkerRegister.js" defer></script>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
<body>
|
||||
{children}
|
||||
{serverConfig?.isVercel && (
|
||||
<>
|
||||
<SpeedInsights />
|
||||
</>
|
||||
)}
|
||||
{serverConfig?.gtmId && (
|
||||
<>
|
||||
<GoogleTagManager gtmId={serverConfig.gtmId} />
|
||||
</>
|
||||
)}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ const ar: PartialLocaleType = {
|
||||
Auth: {
|
||||
Title: "تحتاج إلى رمز الوصول",
|
||||
Tips: "يرجى إدخال رمز الوصول أدناه",
|
||||
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
|
||||
Input: "رمز الوصول",
|
||||
Confirm: "تأكيد",
|
||||
Later: "لاحقًا",
|
||||
@@ -166,11 +167,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
|
||||
Title: "حد الضغط للتاريخ",
|
||||
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
|
||||
},
|
||||
Token: {
|
||||
Title: "مفتاح API",
|
||||
SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
|
||||
Placeholder: "مفتاح OpenAI API",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "رصيد الحساب",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -180,15 +177,7 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
|
||||
Check: "التحقق",
|
||||
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "رمز الوصول",
|
||||
SubTitle: "تم تمكين التحكم في الوصول",
|
||||
Placeholder: "رمز الوصول المطلوب",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "نقطة النهاية",
|
||||
SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
|
||||
},
|
||||
|
||||
Model: "النموذج",
|
||||
Temperature: {
|
||||
Title: "الحرارة",
|
||||
@@ -233,6 +222,9 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
|
||||
Plugin: {
|
||||
Name: "المكوّن الإضافي",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "أنت مساعد ي",
|
||||
},
|
||||
Mask: {
|
||||
Name: "الأقنعة",
|
||||
Page: {
|
||||
|
@@ -10,6 +10,7 @@ const bn: PartialLocaleType = {
|
||||
Auth: {
|
||||
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
|
||||
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
|
||||
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন",
|
||||
Input: "অ্যাক্সেস কোড",
|
||||
Confirm: "নিশ্চিত করুন",
|
||||
Later: "পরে",
|
||||
@@ -198,11 +199,7 @@ const bn: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
|
||||
},
|
||||
Token: {
|
||||
Title: "অ্যাপি কী",
|
||||
SubTitle: "অ্যাক্সেস কোড সীমা উপেক্ষা করতে আপনার কীটি ব্যবহার করুন",
|
||||
Placeholder: "OpenAI API কী",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "একাউন্ট ব্যালেন্স",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -212,15 +209,7 @@ const bn: PartialLocaleType = {
|
||||
Check: "চেক",
|
||||
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "অ্যাক্সেস কোড",
|
||||
SubTitle: "অ্যাক্সেস নিয়ন্ত্রণ সক্রিয়",
|
||||
Placeholder: "অ্যাক্সেস কোড প্রয়োজন",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "ইনটারপয়েন্ট",
|
||||
SubTitle: "কাস্টম এন্ডপয়েন্টটি হতে হবে http(s):// দিয়ে শুরু হতে হবে",
|
||||
},
|
||||
|
||||
Model: "মডেল",
|
||||
Temperature: {
|
||||
Title: "তাপমাত্রা",
|
||||
@@ -271,6 +260,9 @@ const bn: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "প্লাগইন",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "আপনি একটি সহকারী যা",
|
||||
},
|
||||
Mask: {
|
||||
Name: "মাস্ক",
|
||||
Page: {
|
||||
|
@@ -1,14 +1,19 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const cn = {
|
||||
WIP: "该功能仍在开发中……",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。",
|
||||
Unauthorized: isApp
|
||||
? "检测到无效 API Key,请前往[设置](/#/settings)页检查 API Key 是否配置正确。"
|
||||
: "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。",
|
||||
},
|
||||
Auth: {
|
||||
Title: "需要密码",
|
||||
Tips: "管理员开启了密码验证,请在下方填入访问码",
|
||||
SubTips: "或者输入你的 OpenAI 或 Google API 密钥",
|
||||
Input: "在此处填写访问码",
|
||||
Confirm: "确认",
|
||||
Later: "稍后再说",
|
||||
@@ -80,8 +85,8 @@ const cn = {
|
||||
Copy: "全部复制",
|
||||
Download: "下载文件",
|
||||
Share: "分享到 ShareGPT",
|
||||
MessageFromYou: "来自你的消息",
|
||||
MessageFromChatGPT: "来自 ChatGPT 的消息",
|
||||
MessageFromYou: "用户",
|
||||
MessageFromChatGPT: "ChatGPT",
|
||||
Format: {
|
||||
Title: "导出格式",
|
||||
SubTitle: "可以导出 Markdown 文本或者 PNG 图片",
|
||||
@@ -170,6 +175,53 @@ const cn = {
|
||||
Title: "预览气泡",
|
||||
SubTitle: "在预览气泡中预览 Markdown 内容",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "自动生成标题",
|
||||
SubTitle: "根据对话内容生成合适的标题",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "云端数据",
|
||||
NotSyncYet: "还没有进行过同步",
|
||||
Success: "同步成功",
|
||||
Fail: "同步失败",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "配置云同步",
|
||||
Check: "检查可用性",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "同步类型",
|
||||
SubTitle: "选择喜爱的同步服务器",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "启用代理",
|
||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "代理地址",
|
||||
SubTitle: "仅适用于本项目自带的跨域代理",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "WebDAV 地址",
|
||||
UserName: "用户名",
|
||||
Password: "密码",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST Url",
|
||||
UserName: "备份名称",
|
||||
Password: "UpStash Redis REST Token",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "本地数据",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`;
|
||||
},
|
||||
ImportFailed: "导入失败",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "面具启动页",
|
||||
@@ -206,11 +258,6 @@ const cn = {
|
||||
Title: "历史消息长度压缩阈值",
|
||||
SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自己的 Key 可绕过密码访问限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "余额查询",
|
||||
@@ -221,19 +268,73 @@ const cn = {
|
||||
Check: "重新检查",
|
||||
NoAccess: "输入 API Key 或访问密码查看余额",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "访问密码",
|
||||
SubTitle: "管理员已开启加密访问",
|
||||
Placeholder: "请输入访问密码",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "自定义模型名",
|
||||
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
||||
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "访问密码",
|
||||
SubTitle: "管理员已开启加密访问",
|
||||
Placeholder: "请输入访问密码",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "自定义接口",
|
||||
SubTitle: "是否使用自定义 Azure 或 OpenAI 服务",
|
||||
},
|
||||
Provider: {
|
||||
Title: "模型服务商",
|
||||
SubTitle: "切换不同的服务商",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自定义 OpenAI Key 绕过密码访问限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "除默认地址外,必须包含 http(s)://",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 Azure Key 绕过密码访问限制",
|
||||
Placeholder: "Azure API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "接口版本 (azure api version)",
|
||||
SubTitle: "选择指定的部分版本",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 Google AI Studio API Key 绕过密码访问限制",
|
||||
Placeholder: "Google AI Studio API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "不包含请求路径,样例:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "接口版本 (gemini-pro api version)",
|
||||
SubTitle: "选择指定的部分版本",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "自定义模型名",
|
||||
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
@@ -263,7 +364,7 @@ const cn = {
|
||||
Prompt: {
|
||||
History: (content: string) => "这是历史聊天总结作为前情提要:" + content,
|
||||
Topic:
|
||||
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
|
||||
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,不要加粗,如果没有主题,请直接返回“闲聊”",
|
||||
Summarize:
|
||||
"简要总结一下对话内容,用作后续的上下文提示 prompt,控制在 200 字以内",
|
||||
},
|
||||
@@ -272,6 +373,10 @@ const cn = {
|
||||
Success: "已写入剪切板",
|
||||
Failed: "复制失败,请赋予剪切板权限",
|
||||
},
|
||||
Download: {
|
||||
Success: "内容已下载到您的目录。",
|
||||
Failed: "下载失败。",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `包含 ${x} 条预设提示词`,
|
||||
Edit: "当前对话设置",
|
||||
@@ -282,6 +387,9 @@ const cn = {
|
||||
Plugin: {
|
||||
Name: "插件",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "你是一个助手",
|
||||
},
|
||||
Mask: {
|
||||
Name: "面具",
|
||||
Page: {
|
||||
@@ -344,8 +452,15 @@ const cn = {
|
||||
Close: "关闭",
|
||||
Create: "新建",
|
||||
Edit: "编辑",
|
||||
Export: "导出",
|
||||
Import: "导入",
|
||||
Sync: "同步",
|
||||
Config: "配置",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "只有清除上下文之后的消息会被展示",
|
||||
},
|
||||
Model: "模型",
|
||||
Messages: "消息",
|
||||
Topic: "主题",
|
||||
|
@@ -124,11 +124,7 @@ const cs: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
|
||||
},
|
||||
Token: {
|
||||
Title: "API klíč",
|
||||
SubTitle: "Použitím klíče ignorujete omezení přístupového kódu",
|
||||
Placeholder: "Klíč API OpenAI",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Stav účtu",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -138,11 +134,7 @@ const cs: PartialLocaleType = {
|
||||
Check: "Zkontrolovat",
|
||||
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Přístupový kód",
|
||||
SubTitle: "Kontrola přístupu povolena",
|
||||
Placeholder: "Potřebujete přístupový kód",
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Teplota",
|
||||
@@ -188,6 +180,9 @@ const cs: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Jste asistent, který",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Maska",
|
||||
Page: {
|
||||
|
@@ -124,12 +124,7 @@ const de: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
|
||||
},
|
||||
Token: {
|
||||
Title: "API-Schlüssel",
|
||||
SubTitle:
|
||||
"Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
|
||||
Placeholder: "OpenAI API-Schlüssel",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Kontostand",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -139,11 +134,6 @@ const de: PartialLocaleType = {
|
||||
Check: "Erneut prüfen",
|
||||
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Zugangscode",
|
||||
SubTitle: "Zugangskontrolle aktiviert",
|
||||
Placeholder: "Zugangscode erforderlich",
|
||||
},
|
||||
Model: "Modell",
|
||||
Temperature: {
|
||||
Title: "Temperature", //Temperatur
|
||||
@@ -192,6 +182,9 @@ const de: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Du bist ein Assistent, der",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
Page: {
|
||||
|
@@ -1,16 +1,21 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { LocaleType } from "./index";
|
||||
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const en: LocaleType = {
|
||||
WIP: "Coming Soon...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Unauthorized access, please enter access code in [auth](/#/auth) page.",
|
||||
Unauthorized: isApp
|
||||
? "Invalid API Key, please check it in [Settings](/#/settings) page."
|
||||
: "Unauthorized access, please enter access code in [auth](/#/auth) page, or enter your OpenAI API Key.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Need Access Code",
|
||||
Tips: "Please enter access code below",
|
||||
SubTips: "Or enter your OpenAI or Google API Key",
|
||||
Input: "access code",
|
||||
Confirm: "Confirm",
|
||||
Later: "Later",
|
||||
@@ -172,6 +177,54 @@ const en: LocaleType = {
|
||||
Title: "Send Preview Bubble",
|
||||
SubTitle: "Preview markdown in bubble",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Auto Generate Title",
|
||||
SubTitle: "Generate a suitable title based on the conversation content",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Last Update",
|
||||
NotSyncYet: "Not sync yet",
|
||||
Success: "Sync Success",
|
||||
Fail: "Sync Fail",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Config Sync",
|
||||
Check: "Check Connection",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Sync Type",
|
||||
SubTitle: "Choose your favorite sync service",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Enable CORS Proxy",
|
||||
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Proxy Endpoint",
|
||||
SubTitle:
|
||||
"Only applicable to the built-in CORS proxy for this project",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "WebDAV Endpoint",
|
||||
UserName: "User Name",
|
||||
Password: "Password",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "UpStash Redis REST Url",
|
||||
UserName: "Backup Name",
|
||||
Password: "UpStash Redis REST Token",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Local Data",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`;
|
||||
},
|
||||
ImportFailed: "Failed to import from file",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Mask Splash Screen",
|
||||
@@ -209,11 +262,7 @@ const en: LocaleType = {
|
||||
SubTitle:
|
||||
"Will compress if uncompressed messages length exceeds the value",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "Use your key to ignore access code limit",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Account Balance",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -223,19 +272,73 @@ const en: LocaleType = {
|
||||
Check: "Check",
|
||||
NoAccess: "Enter API Key to check balance",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Access Code",
|
||||
SubTitle: "Access control enabled",
|
||||
Placeholder: "Need Access Code",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint",
|
||||
SubTitle: "Custom endpoint must start with http(s)://",
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Custom Models",
|
||||
SubTitle: "Add extra model options, separate by comma",
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Access Code",
|
||||
SubTitle: "Access control Enabled",
|
||||
Placeholder: "Enter Code",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Custom Endpoint",
|
||||
SubTitle: "Use custom Azure or OpenAI service",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Model Provider",
|
||||
SubTitle: "Select Azure or OpenAI",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "OpenAI API Key",
|
||||
SubTitle: "User custom OpenAI Api Key",
|
||||
Placeholder: "sk-xxx",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "OpenAI Endpoint",
|
||||
SubTitle: "Must starts with http(s):// or use /api/openai as default",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Azure Api Key",
|
||||
SubTitle: "Check your api key from Azure console",
|
||||
Placeholder: "Azure Api Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Azure Endpoint",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Azure Api Version",
|
||||
SubTitle: "Check your api version from azure console",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Custom Models",
|
||||
SubTitle: "Custom model options, seperated by comma",
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle:
|
||||
"Bypass password access restrictions using a custom Google AI Studio API Key",
|
||||
Placeholder: "Google AI Studio API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "API Version (gemini-pro api version)",
|
||||
SubTitle: "Select a specific part version",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
@@ -268,7 +371,7 @@ const en: LocaleType = {
|
||||
History: (content: string) =>
|
||||
"This is a summary of the chat history as a recap: " + content,
|
||||
Topic:
|
||||
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.",
|
||||
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, bold text, or additional text. Remove enclosing quotation marks.",
|
||||
Summarize:
|
||||
"Summarize the discussion briefly in 200 words or less to use as a prompt for future context.",
|
||||
},
|
||||
@@ -277,6 +380,10 @@ const en: LocaleType = {
|
||||
Success: "Copied to clipboard",
|
||||
Failed: "Copy failed, please grant permission to access clipboard",
|
||||
},
|
||||
Download: {
|
||||
Success: "Content downloaded to your directory.",
|
||||
Failed: "Download failed.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `With ${x} contextual prompts`,
|
||||
Edit: "Current Chat Settings",
|
||||
@@ -287,6 +394,9 @@ const en: LocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "You are an assistant that",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
Page: {
|
||||
@@ -344,8 +454,15 @@ const en: LocaleType = {
|
||||
Close: "Close",
|
||||
Create: "Create",
|
||||
Edit: "Edit",
|
||||
Export: "Export",
|
||||
Import: "Import",
|
||||
Sync: "Sync",
|
||||
Config: "Config",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Only messages after clearing the context will be displayed",
|
||||
},
|
||||
Model: "Model",
|
||||
Messages: "Messages",
|
||||
Topic: "Topic",
|
||||
|
@@ -124,11 +124,7 @@ const es: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Se comprimirán los mensajes si la longitud de los mensajes no comprimidos supera el valor",
|
||||
},
|
||||
Token: {
|
||||
Title: "Clave de API",
|
||||
SubTitle: "Utiliza tu clave para ignorar el límite de código de acceso",
|
||||
Placeholder: "Clave de la API de OpenAI",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Saldo de la cuenta",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -138,11 +134,7 @@ const es: PartialLocaleType = {
|
||||
Check: "Comprobar de nuevo",
|
||||
NoAccess: "Introduzca la clave API para comprobar el saldo",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Código de acceso",
|
||||
SubTitle: "Control de acceso habilitado",
|
||||
Placeholder: "Necesita código de acceso",
|
||||
},
|
||||
|
||||
Model: "Modelo",
|
||||
Temperature: {
|
||||
Title: "Temperatura",
|
||||
@@ -190,6 +182,9 @@ const es: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Eres un asistente que",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
Page: {
|
||||
|
@@ -173,11 +173,7 @@ const fr: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Comprimera si la longueur des messages non compressés dépasse cette valeur",
|
||||
},
|
||||
Token: {
|
||||
Title: "Clé API",
|
||||
SubTitle: "Utilisez votre clé pour ignorer la limite du code d'accès",
|
||||
Placeholder: "Clé OpenAI API",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Solde du compte",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -187,11 +183,7 @@ const fr: PartialLocaleType = {
|
||||
Check: "Vérifier",
|
||||
NoAccess: "Entrez la clé API pour vérifier le solde",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Code d'accès",
|
||||
SubTitle: "Contrôle d'accès activé",
|
||||
Placeholder: "Code d'accès requis",
|
||||
},
|
||||
|
||||
Model: "Modèle",
|
||||
Temperature: {
|
||||
Title: "Température",
|
||||
@@ -244,6 +236,9 @@ const fr: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Extension",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Eres un asistente que",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Masque",
|
||||
Page: {
|
||||
|
385
app/locales/id.ts
Normal file
385
app/locales/id.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "./index";
|
||||
|
||||
const id: PartialLocaleType = {
|
||||
WIP: "Coming Soon...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Diperlukan Kode Akses",
|
||||
Tips: "Masukkan kode akses di bawah",
|
||||
SubTips: "Atau masukkan kunci API OpenAI Anda",
|
||||
Input: "Kode Akses",
|
||||
Confirm: "Konfirmasi",
|
||||
Later: "Nanti",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} pesan`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} pesan`,
|
||||
Actions: {
|
||||
ChatList: "Buka Daftar Chat",
|
||||
CompressedHistory: "Ekspor Riwayat Terkompresi",
|
||||
Export: "Ekspor Semua Pesan sebagai Markdown",
|
||||
Copy: "Salin",
|
||||
Stop: "Berhenti",
|
||||
Retry: "Coba Lagi",
|
||||
Pin: "Pin",
|
||||
PinToastContent: "2 pesan telah ditandai",
|
||||
PinToastAction: "Lihat",
|
||||
Delete: "Hapus",
|
||||
Edit: "Edit",
|
||||
},
|
||||
Commands: {
|
||||
new: "Mulai Chat Baru",
|
||||
newm: "Mulai Chat Baru dengan Masks",
|
||||
next: "Chat Selanjutnya",
|
||||
prev: "Chat Sebelumnya",
|
||||
clear: "Bersihkan Percakapan",
|
||||
del: "Hapus Chat",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Berhenti",
|
||||
ToBottom: "Ke Bagian Bawah",
|
||||
Theme: {
|
||||
auto: "Otomatis",
|
||||
light: "Tema Terang",
|
||||
dark: "Tema Gelap",
|
||||
},
|
||||
Prompt: "Prompts",
|
||||
Masks: "Masks",
|
||||
Clear: "Bersihkan Percakapan",
|
||||
Settings: "Pengaturan",
|
||||
},
|
||||
Rename: "Ubah Nama Chat",
|
||||
Typing: "Mengetik...",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} untuk mengirim`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter untuk membalut";
|
||||
}
|
||||
return (
|
||||
inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"
|
||||
);
|
||||
},
|
||||
Send: "Kirim",
|
||||
Config: {
|
||||
Reset: "Reset ke Default",
|
||||
SaveAs: "Simpan sebagai Masks",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Ekspor Pesan",
|
||||
Copy: "Salin Semua",
|
||||
Download: "Unduh",
|
||||
MessageFromYou: "Pesan dari Anda",
|
||||
MessageFromChatGPT: "Pesan dari ChatGPT",
|
||||
Share: "Bagikan ke ShareGPT",
|
||||
Format: {
|
||||
Title: "Format Ekspor",
|
||||
SubTitle: "Markdown atau Gambar PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Sertakan Konteks",
|
||||
SubTitle: "Apakah akan menyertakan masks",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Pilih",
|
||||
Preview: "Pratinjau",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Cari",
|
||||
All: "Pilih Semua",
|
||||
Latest: "Pilih Terbaru",
|
||||
Clear: "Bersihkan",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Prompt Memori",
|
||||
EmptyContent: "Belum ada yang tersedia.",
|
||||
Send: "Kirim Memori",
|
||||
Copy: "Salin Memori",
|
||||
Reset: "Reset",
|
||||
ResetConfirm:
|
||||
"Jika Anda mereset, riwayat obrolan saat ini dan memori historis akan dihapus. Apakah Anda yakin ingin melakukan reset?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Obrolan Baru",
|
||||
DeleteChat: "Anda yakin ingin menghapus percakapan yang dipilih?",
|
||||
DeleteToast: "Percakapan telah dihapus",
|
||||
Revert: "Kembali",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Pengaturan",
|
||||
SubTitle: "Semua Pengaturan",
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Setel Ulang Semua Pengaturan",
|
||||
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
|
||||
Action: "Setel Ulang",
|
||||
Confirm:
|
||||
"Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Hapus Semua Data",
|
||||
SubTitle: "Semua data yang tersimpan secara lokal akan dihapus",
|
||||
Action: "Hapus",
|
||||
Confirm:
|
||||
"Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Semua Bahasa",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Ukuran Font",
|
||||
SubTitle: "Ubah ukuran font konten chat",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Suntikkan Petunjuk Sistem",
|
||||
SubTitle:
|
||||
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Template Input",
|
||||
SubTitle: "Pesan baru akan diisi menggunakan template ini",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Version: ${x}`,
|
||||
IsLatest: "Versi terbaru",
|
||||
CheckUpdate: "Periksa Pembaruan",
|
||||
IsChecking: "Memeriksa pembaruan...",
|
||||
FoundUpdate: (x: string) => `Versi terbaru ditemukan: ${x}`,
|
||||
GoToUpdate: "Perbarui Sekarang",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Hasilkan Judul Otomatis",
|
||||
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Pembaruan Terakhir",
|
||||
NotSyncYet: "Belum disinkronkan",
|
||||
Success: "Sinkronisasi Berhasil",
|
||||
Fail: "Sinkronisasi Gagal",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Konfigurasi Sinkronisasi",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Tipe Sinkronisasi",
|
||||
SubTitle: "Pilih layanan sinkronisasi favorit Anda",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Aktifkan Proxy CORS",
|
||||
SubTitle:
|
||||
"Aktifkan Proxy untuk menghindari pembatasan atau pemblokiran lintas sumber",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Lokasi Titik Akhir Proxy CORS",
|
||||
SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Lokasi Titik Akhir WebDAV",
|
||||
UserName: "User Pengguna",
|
||||
Password: "Kata Sandi",
|
||||
},
|
||||
},
|
||||
},
|
||||
SendKey: "Kirim",
|
||||
Theme: "Tema",
|
||||
TightBorder: "Batas Ketat",
|
||||
SendPreviewBubble: {
|
||||
Title: "Pratinjau Obrolan",
|
||||
SubTitle: "Pratinjau Obrolan dengan markdown",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Layar Pembuka Masks",
|
||||
SubTitle:
|
||||
"Tampilkan layar pembuka masks sebelum memulai percakapan baru",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Sembunyikan Masks Bawaan",
|
||||
SubTitle: "Sembunyikan Masks bawaan dari daftar masks",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Nonaktifkan Otomatisasi",
|
||||
SubTitle: "Aktifkan/Matikan otomatisasi",
|
||||
},
|
||||
List: "Daftar Prompt",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} bawaan, ${custom} penggunaan khusus`,
|
||||
Edit: "Edit",
|
||||
Modal: {
|
||||
Title: "Daftar Prompt",
|
||||
Add: "Tambahkan",
|
||||
Search: "Cari Prompt",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Edit Prompt",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Jumlah Pesan Riwayat",
|
||||
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Batas Kompresi Riwayat",
|
||||
SubTitle:
|
||||
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Saldo Akun",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
|
||||
},
|
||||
IsChecking: "Memeriksa...",
|
||||
Check: "Periksa",
|
||||
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Suhu",
|
||||
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
SubTitle: "Tidak mengubah nilai dengan suhu",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Token Maksimum",
|
||||
SubTitle: "Panjang maksimum token input dan output",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalti Kehadiran",
|
||||
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalti Frekuensi",
|
||||
SubTitle:
|
||||
"Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Percakapan Baru",
|
||||
BotHello: "Halo! Bagaimana saya bisa membantu Anda hari ini?",
|
||||
Error: "Terjadi kesalahan, silakan coba lagi nanti.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Ini adalah ringkasan singkat dari riwayat percakapan: " + content,
|
||||
Topic:
|
||||
"Buat judul berisi empat hingga lima kata untuk percakapan kita yang tidak akan disertakan dalam ringkasan percakapan, seperti instruksi, format, kutipan, tanda baca awal, tanda kutip pendahuluan, atau karakter tambahan. Silakan coba dengan kutipan berakhir.",
|
||||
Summarize:
|
||||
"Buat ringkasan percakapan dalam 200 kata yang akan digunakan sebagai promp di masa depan.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Tersalin ke clipboard",
|
||||
Failed:
|
||||
"Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
|
||||
},
|
||||
Download: {
|
||||
Success: "Konten berhasil diunduh ke direktori Anda.",
|
||||
Failed: "Unduhan gagal.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
|
||||
Edit: "Pengaturan Obrolan Saat Ini",
|
||||
Add: "Tambahkan Promp",
|
||||
Clear: "Bersihkan Konteks",
|
||||
Revert: "Kembali ke Posisi Sebelumnya",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Anda adalah asisten yang",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Masks",
|
||||
Page: {
|
||||
Title: "Template Promp",
|
||||
SubTitle: (count: number) => `${count} template prompt`,
|
||||
Search: "Cari template",
|
||||
Create: "Buat",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} prompt`,
|
||||
Chat: "Obrolan",
|
||||
View: "Lihat",
|
||||
Edit: "Edit",
|
||||
Delete: "Hapus",
|
||||
DeleteConfirm: "Anda yakin ingin menghapus?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Edit Template Prompt ${readonly ? "(hanya baca)" : ""}`,
|
||||
Download: "Unduh",
|
||||
Clone: "Duplikat",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar Bot",
|
||||
Name: "Nama Bot",
|
||||
Sync: {
|
||||
Title: "Gunakan Konfigurasi Global",
|
||||
SubTitle: "Gunakan konfigurasi global dalam percakapan ini",
|
||||
Confirm:
|
||||
"Pastikan untuk mengganti konfigurasi kustom dengan konfigurasi global?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Sembunyikan Prompt Konteks",
|
||||
SubTitle: "Tidak menampilkan prompt konteks dalam obrolan",
|
||||
},
|
||||
Share: {
|
||||
Title: "Bagikan Masks Ini",
|
||||
SubTitle: "Buat tautan untuk masks ini",
|
||||
Action: "Salin Tautan",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Kembali",
|
||||
Skip: "Lewati",
|
||||
Title: "Pilih Masks",
|
||||
SubTitle: "Berkonversasilah dengan diri Anda di balik masks",
|
||||
More: "Lebih Lanjut",
|
||||
NotShow: "Jangan Tampilkan Sekarang",
|
||||
ConfirmNoShow:
|
||||
"Pastikan untuk menonaktifkannya? Anda dapat mengaktifkannya nanti melalui pengaturan.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Konfirmasi",
|
||||
Cancel: "Batal",
|
||||
Close: "Tutup",
|
||||
Create: "Buat",
|
||||
Edit: "Edit",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Hanya pesan setelah menghapus konteks yang akan ditampilkan"
|
||||
},
|
||||
Model: "Model",
|
||||
Messages: "Pesan",
|
||||
Topic: "Topik",
|
||||
Time: "Tanggal & Waktu",
|
||||
},
|
||||
URLCommand: {
|
||||
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",
|
||||
Settings: "Pengaturan terdeteksi dari url, konfirmasi untuk diterapkan ?",
|
||||
},
|
||||
};
|
||||
|
||||
export default id;
|
@@ -1,6 +1,8 @@
|
||||
import cn from "./cn";
|
||||
import en from "./en";
|
||||
import pt from "./pt";
|
||||
import tw from "./tw";
|
||||
import id from "./id";
|
||||
import fr from "./fr";
|
||||
import es from "./es";
|
||||
import it from "./it";
|
||||
@@ -14,6 +16,7 @@ import cs from "./cs";
|
||||
import ko from "./ko";
|
||||
import ar from "./ar";
|
||||
import bn from "./bn";
|
||||
import sk from "./sk";
|
||||
import { merge } from "../utils/merge";
|
||||
|
||||
import type { LocaleType } from "./cn";
|
||||
@@ -23,8 +26,10 @@ const ALL_LANGS = {
|
||||
cn,
|
||||
en,
|
||||
tw,
|
||||
pt,
|
||||
jp,
|
||||
ko,
|
||||
id,
|
||||
fr,
|
||||
es,
|
||||
it,
|
||||
@@ -36,6 +41,7 @@ const ALL_LANGS = {
|
||||
no,
|
||||
ar,
|
||||
bn,
|
||||
sk,
|
||||
};
|
||||
|
||||
export type Lang = keyof typeof ALL_LANGS;
|
||||
@@ -45,9 +51,11 @@ export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
|
||||
export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
pt: "Português",
|
||||
tw: "繁體中文",
|
||||
jp: "日本語",
|
||||
ko: "한국어",
|
||||
id: "Indonesia",
|
||||
fr: "Français",
|
||||
es: "Español",
|
||||
it: "Italiano",
|
||||
@@ -59,6 +67,7 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
no: "Nynorsk",
|
||||
ar: "العربية",
|
||||
bn: "বাংলা",
|
||||
sk: "Slovensky",
|
||||
};
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
@@ -116,3 +125,13 @@ export function changeLang(lang: Lang) {
|
||||
setItem(LANG_KEY, lang);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
export function getISOLang() {
|
||||
const isoLangString: Record<string, string> = {
|
||||
cn: "zh-Hans",
|
||||
tw: "zh-Hant",
|
||||
};
|
||||
|
||||
const lang = getLang();
|
||||
return isoLangString[lang] ?? lang;
|
||||
}
|
||||
|
@@ -124,12 +124,7 @@ const it: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle:
|
||||
"Utilizzare la chiave per ignorare il limite del codice di accesso",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Bilancio Account",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -139,11 +134,7 @@ const it: PartialLocaleType = {
|
||||
Check: "Controlla ancora",
|
||||
NoAccess: "Inserire la chiave API per controllare il saldo",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Codice d'accesso",
|
||||
SubTitle: "Controllo d'accesso abilitato",
|
||||
Placeholder: "Inserisci il codice d'accesso",
|
||||
},
|
||||
|
||||
Model: "Modello GPT",
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
@@ -191,6 +182,9 @@ const it: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Sei un assistente che",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
Page: {
|
||||
|
@@ -19,7 +19,12 @@ const jp: PartialLocaleType = {
|
||||
Copy: "コピー",
|
||||
Stop: "停止",
|
||||
Retry: "リトライ",
|
||||
Pin: "ピン",
|
||||
PinToastContent:
|
||||
"コンテキストプロンプトに1つのメッセージをピン留めしました",
|
||||
PinToastAction: "表示",
|
||||
Delete: "削除",
|
||||
Edit: "編集",
|
||||
},
|
||||
Rename: "チャットの名前を変更",
|
||||
Typing: "入力中…",
|
||||
@@ -33,7 +38,7 @@ const jp: PartialLocaleType = {
|
||||
Send: "送信",
|
||||
Config: {
|
||||
Reset: "リセット",
|
||||
SaveAs: "另存为面具",
|
||||
SaveAs: "保存",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
@@ -142,11 +147,7 @@ const jp: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
|
||||
},
|
||||
Token: {
|
||||
Title: "APIキー",
|
||||
SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
|
||||
Placeholder: "OpenAI APIキー",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "残高照会",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -156,11 +157,7 @@ const jp: PartialLocaleType = {
|
||||
Check: "再確認",
|
||||
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "アクセスパスワード",
|
||||
SubTitle: "暗号化アクセスが有効になっています",
|
||||
Placeholder: "アクセスパスワードを入力してください",
|
||||
},
|
||||
|
||||
Model: "モデル (model)",
|
||||
Temperature: {
|
||||
Title: "ランダム性 (temperature)",
|
||||
@@ -204,6 +201,7 @@ const jp: PartialLocaleType = {
|
||||
Add: "追加",
|
||||
},
|
||||
Plugin: { Name: "プラグイン" },
|
||||
FineTuned: { Sysmessage: "あなたはアシスタントです" },
|
||||
Mask: {
|
||||
Name: "キャラクタープリセット",
|
||||
Page: {
|
||||
|
@@ -124,11 +124,7 @@ const ko: PartialLocaleType = {
|
||||
Title: "기록 압축 임계값",
|
||||
SubTitle: "미압축 메시지 길이가 임계값을 초과하면 압축됨",
|
||||
},
|
||||
Token: {
|
||||
Title: "API 키",
|
||||
SubTitle: "액세스 코드 제한을 무시하기 위해 키 사용",
|
||||
Placeholder: "OpenAI API 키",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "계정 잔액",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -138,11 +134,7 @@ const ko: PartialLocaleType = {
|
||||
Check: "확인",
|
||||
NoAccess: "잔액 확인을 위해 API 키를 입력하세요.",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "액세스 코드",
|
||||
SubTitle: "액세스 제어가 활성화됨",
|
||||
Placeholder: "액세스 코드 입력",
|
||||
},
|
||||
|
||||
Model: "모델",
|
||||
Temperature: {
|
||||
Title: "온도 (temperature)",
|
||||
@@ -186,6 +178,9 @@ const ko: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "플러그인",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "당신은 어시스턴트입니다",
|
||||
},
|
||||
Mask: {
|
||||
Name: "마스크",
|
||||
Page: {
|
||||
|
@@ -106,12 +106,7 @@ const no: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle:
|
||||
"Bruk din egen API-nøkkel for å ignorere tilgangskoden begrensning",
|
||||
Placeholder: "OpenAI API-nøkkel",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Saldo for konto",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -121,11 +116,7 @@ const no: PartialLocaleType = {
|
||||
Check: "Sjekk",
|
||||
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Tilgangskode",
|
||||
SubTitle: "Tilgangskontroll på",
|
||||
Placeholder: "Trenger tilgangskode",
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Temperatur",
|
||||
|
466
app/locales/pt.ts
Normal file
466
app/locales/pt.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "../locales/index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const pt: PartialLocaleType = {
|
||||
WIP: "Em breve...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Chave API inválida, por favor verifique em [Configurações](/#/settings)."
|
||||
: "Acesso não autorizado, por favor insira o código de acesso em [auth](/#/auth) ou insira sua Chave API OpenAI.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Necessário Código de Acesso",
|
||||
Tips: "Por favor, insira o código de acesso abaixo",
|
||||
SubTips: "Ou insira sua Chave API OpenAI",
|
||||
Input: "código de acesso",
|
||||
Confirm: "Confirmar",
|
||||
Later: "Depois",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} mensagens`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} mensagens`,
|
||||
EditMessage: {
|
||||
Title: "Editar Todas as Mensagens",
|
||||
Topic: {
|
||||
Title: "Tópico",
|
||||
SubTitle: "Mudar o tópico atual",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "Ir Para Lista de Chat",
|
||||
CompressedHistory: "Prompt de Memória Histórica Comprimida",
|
||||
Export: "Exportar Todas as Mensagens como Markdown",
|
||||
Copy: "Copiar",
|
||||
Stop: "Parar",
|
||||
Retry: "Tentar Novamente",
|
||||
Pin: "Fixar",
|
||||
PinToastContent: "Fixada 1 mensagem para prompts contextuais",
|
||||
PinToastAction: "Visualizar",
|
||||
Delete: "Deletar",
|
||||
Edit: "Editar",
|
||||
},
|
||||
Commands: {
|
||||
new: "Iniciar um novo chat",
|
||||
newm: "Iniciar um novo chat com máscara",
|
||||
next: "Próximo Chat",
|
||||
prev: "Chat Anterior",
|
||||
clear: "Limpar Contexto",
|
||||
del: "Deletar Chat",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Parar",
|
||||
ToBottom: "Para o Mais Recente",
|
||||
Theme: {
|
||||
auto: "Automático",
|
||||
light: "Tema Claro",
|
||||
dark: "Tema Escuro",
|
||||
},
|
||||
Prompt: "Prompts",
|
||||
Masks: "Máscaras",
|
||||
Clear: "Limpar Contexto",
|
||||
Settings: "Configurações",
|
||||
},
|
||||
Rename: "Renomear Chat",
|
||||
Typing: "Digitando…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} para enviar`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter para quebrar linha";
|
||||
}
|
||||
return inputHints + ", / para buscar prompts, : para usar comandos";
|
||||
},
|
||||
Send: "Enviar",
|
||||
Config: {
|
||||
Reset: "Redefinir para Padrão",
|
||||
SaveAs: "Salvar como Máscara",
|
||||
},
|
||||
IsContext: "Prompt Contextual",
|
||||
},
|
||||
Export: {
|
||||
Title: "Exportar Mensagens",
|
||||
Copy: "Copiar Tudo",
|
||||
Download: "Baixar",
|
||||
MessageFromYou: "Mensagem De Você",
|
||||
MessageFromChatGPT: "Mensagem De ChatGPT",
|
||||
Share: "Compartilhar para ShareGPT",
|
||||
Format: {
|
||||
Title: "Formato de Exportação",
|
||||
SubTitle: "Markdown ou Imagem PNG",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Incluindo Contexto",
|
||||
SubTitle: "Exportar prompts de contexto na máscara ou não",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Selecionar",
|
||||
Preview: "Pré-visualizar",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Capturando Imagem...",
|
||||
Modal:
|
||||
"Pressione longamente ou clique com o botão direito para salvar a imagem",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Buscar",
|
||||
All: "Selecionar Tudo",
|
||||
Latest: "Selecionar Mais Recente",
|
||||
Clear: "Limpar",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Prompt de Memória",
|
||||
EmptyContent: "Nada ainda.",
|
||||
Send: "Enviar Memória",
|
||||
Copy: "Copiar Memória",
|
||||
Reset: "Resetar Sessão",
|
||||
ResetConfirm:
|
||||
"Resetar irá limpar o histórico de conversa atual e a memória histórica. Você tem certeza que quer resetar?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Novo Chat",
|
||||
DeleteChat: "Confirmar para deletar a conversa selecionada?",
|
||||
DeleteToast: "Chat Deletado",
|
||||
Revert: "Reverter",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Configurações",
|
||||
SubTitle: "Todas as Configurações",
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Resetar Todas as Configurações",
|
||||
SubTitle: "Resetar todos os itens de configuração para o padrão",
|
||||
Action: "Resetar",
|
||||
Confirm: "Confirmar para resetar todas as configurações para o padrão?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Limpar Todos os Dados",
|
||||
SubTitle: "Limpar todas as mensagens e configurações",
|
||||
Action: "Limpar",
|
||||
Confirm: "Confirmar para limpar todas as mensagens e configurações?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language",
|
||||
All: "Todos os Idiomas",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Tamanho da Fonte",
|
||||
SubTitle: "Ajustar o tamanho da fonte do conteúdo do chat",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Inserir Prompts de Sistema",
|
||||
SubTitle: "Inserir um prompt de sistema global para cada requisição",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Modelo de Entrada",
|
||||
SubTitle: "A mensagem mais recente será preenchida neste modelo",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Versão: ${x}`,
|
||||
IsLatest: "Última versão",
|
||||
CheckUpdate: "Verificar Atualização",
|
||||
IsChecking: "Verificando atualização...",
|
||||
FoundUpdate: (x: string) => `Nova versão encontrada: ${x}`,
|
||||
GoToUpdate: "Atualizar",
|
||||
},
|
||||
SendKey: "Tecla de Envio",
|
||||
Theme: "Tema",
|
||||
TightBorder: "Borda Ajustada",
|
||||
SendPreviewBubble: {
|
||||
Title: "Bolha de Pré-visualização de Envio",
|
||||
SubTitle: "Pré-visualizar markdown na bolha",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Gerar Título Automaticamente",
|
||||
SubTitle: "Gerar um título adequado baseado no conteúdo da conversa",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Última Atualização",
|
||||
NotSyncYet: "Ainda não sincronizado",
|
||||
Success: "Sincronização bem sucedida",
|
||||
Fail: "Falha na sincronização",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Configurar Sincronização",
|
||||
Check: "Verificar Conexão",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Tipo de Sincronização",
|
||||
SubTitle: "Escolha seu serviço de sincronização favorito",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Habilitar Proxy CORS",
|
||||
SubTitle: "Habilitar um proxy para evitar restrições de cross-origin",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Endpoint de Proxy",
|
||||
SubTitle: "Apenas aplicável ao proxy CORS embutido para este projeto",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Endpoint WebDAV",
|
||||
UserName: "Nome de Usuário",
|
||||
Password: "Senha",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "URL REST Redis UpStash",
|
||||
UserName: "Nome do Backup",
|
||||
Password: "Token REST Redis UpStash",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Dados Locais",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} chats,${overview.message} mensagens,${overview.prompt} prompts,${overview.mask} máscaras`;
|
||||
},
|
||||
ImportFailed: "Falha ao importar do arquivo",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Tela de Início da Máscara",
|
||||
SubTitle:
|
||||
"Mostrar uma tela de início da máscara antes de iniciar novo chat",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Esconder Máscaras Embutidas",
|
||||
SubTitle: "Esconder máscaras embutidas na lista de máscaras",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Desabilitar auto-completar",
|
||||
SubTitle: "Digite / para acionar auto-completar",
|
||||
},
|
||||
List: "Lista de Prompts",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} embutidos, ${custom} definidos pelo usuário`,
|
||||
Edit: "Editar",
|
||||
Modal: {
|
||||
Title: "Lista de Prompts",
|
||||
Add: "Adicionar Um",
|
||||
Search: "Buscar Prompts",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Editar Prompt",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Contagem de Mensagens Anexadas",
|
||||
SubTitle: "Número de mensagens enviadas anexadas por requisição",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Limite de Compressão de Histórico",
|
||||
SubTitle:
|
||||
"Irá comprimir se o comprimento das mensagens não comprimidas exceder o valor",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Saldo da Conta",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Usado este mês ${used}, assinatura ${total}`;
|
||||
},
|
||||
IsChecking: "Verificando...",
|
||||
Check: "Verificar",
|
||||
NoAccess: "Insira a Chave API para verificar o saldo",
|
||||
},
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Código de Acesso",
|
||||
SubTitle: "Controle de Acesso Habilitado",
|
||||
Placeholder: "Insira o Código",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Endpoint Personalizado",
|
||||
SubTitle: "Use serviço personalizado Azure ou OpenAI",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Provedor do Modelo",
|
||||
SubTitle: "Selecione Azure ou OpenAI",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "Chave API OpenAI",
|
||||
SubTitle: "Usar Chave API OpenAI personalizada",
|
||||
Placeholder: "sk-xxx",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint OpenAI",
|
||||
SubTitle:
|
||||
"Deve começar com http(s):// ou usar /api/openai como padrão",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "Chave API Azure",
|
||||
SubTitle: "Verifique sua chave API do console Azure",
|
||||
Placeholder: "Chave API Azure",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint Azure",
|
||||
SubTitle: "Exemplo: ",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Versão API Azure",
|
||||
SubTitle: "Verifique sua versão API do console Azure",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Modelos Personalizados",
|
||||
SubTitle: "Opções de modelo personalizado, separados por vírgula",
|
||||
},
|
||||
},
|
||||
|
||||
Model: "Modelo",
|
||||
Temperature: {
|
||||
Title: "Temperatura",
|
||||
SubTitle: "Um valor maior torna a saída mais aleatória",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
SubTitle: "Não altere este valor junto com a temperatura",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Máximo de Tokens",
|
||||
SubTitle: "Comprimento máximo de tokens de entrada e tokens gerados",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalidade de Presença",
|
||||
SubTitle:
|
||||
"Um valor maior aumenta a probabilidade de falar sobre novos tópicos",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalidade de Frequência",
|
||||
SubTitle:
|
||||
"Um valor maior diminui a probabilidade de repetir a mesma linha",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Nova Conversa",
|
||||
BotHello: "Olá! Como posso ajudá-lo hoje?",
|
||||
Error: "Algo deu errado, por favor tente novamente mais tarde.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Este é um resumo do histórico de chat como um recapitulativo: " +
|
||||
content,
|
||||
Topic:
|
||||
"Por favor, gere um título de quatro a cinco palavras resumindo nossa conversa sem qualquer introdução, pontuação, aspas, períodos, símbolos ou texto adicional. Remova as aspas que o envolvem.",
|
||||
Summarize:
|
||||
"Resuma a discussão brevemente em 200 palavras ou menos para usar como um prompt para o contexto futuro.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Copiado para a área de transferência",
|
||||
Failed:
|
||||
"Falha na cópia, por favor conceda permissão para acessar a área de transferência",
|
||||
},
|
||||
Download: {
|
||||
Success: "Conteúdo baixado para seu diretório.",
|
||||
Failed: "Falha no download.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Com ${x} prompts contextuais`,
|
||||
Edit: "Configurações do Chat Atual",
|
||||
Add: "Adicionar um Prompt",
|
||||
Clear: "Contexto Limpo",
|
||||
Revert: "Reverter",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Você é um assistente que",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Máscara",
|
||||
Page: {
|
||||
Title: "Template de Prompt",
|
||||
SubTitle: (count: number) => `${count} templates de prompt`,
|
||||
Search: "Buscar Templates",
|
||||
Create: "Criar",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} prompts`,
|
||||
Chat: "Chat",
|
||||
View: "Visualizar",
|
||||
Edit: "Editar",
|
||||
Delete: "Deletar",
|
||||
DeleteConfirm: "Confirmar para deletar?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Editar Template de Prompt ${readonly ? "(somente leitura)" : ""}`,
|
||||
Download: "Baixar",
|
||||
Clone: "Clonar",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar do Bot",
|
||||
Name: "Nome do Bot",
|
||||
Sync: {
|
||||
Title: "Usar Configuração Global",
|
||||
SubTitle: "Usar configuração global neste chat",
|
||||
Confirm:
|
||||
"Confirmar para substituir a configuração personalizada pela configuração global?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Esconder Prompts de Contexto",
|
||||
SubTitle: "Não mostrar prompts de contexto no chat",
|
||||
},
|
||||
Share: {
|
||||
Title: "Compartilhar Esta Máscara",
|
||||
SubTitle: "Gerar um link para esta máscara",
|
||||
Action: "Copiar Link",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Retornar",
|
||||
Skip: "Apenas Começar",
|
||||
Title: "Escolher uma Máscara",
|
||||
SubTitle: "Converse com a Alma por trás da Máscara",
|
||||
More: "Encontre Mais",
|
||||
NotShow: "Nunca Mostrar Novamente",
|
||||
ConfirmNoShow:
|
||||
"Confirmar para desabilitar?Você pode habilitar nas configurações depois.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Confirmar",
|
||||
Cancel: "Cancelar",
|
||||
Close: "Fechar",
|
||||
Create: "Criar",
|
||||
Edit: "Editar",
|
||||
Export: "Exportar",
|
||||
Import: "Importar",
|
||||
Sync: "Sincronizar",
|
||||
Config: "Configurar",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Apenas mensagens após a limpeza do contexto serão exibidas",
|
||||
},
|
||||
Model: "Modelo",
|
||||
Messages: "Mensagens",
|
||||
Topic: "Tópico",
|
||||
Time: "Tempo",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Código de acesso detectado a partir da url, confirmar para aplicar? ",
|
||||
Settings:
|
||||
"Configurações detectadas a partir da url, confirmar para aplicar?",
|
||||
},
|
||||
};
|
||||
|
||||
export default pt;
|
@@ -125,11 +125,7 @@ const ru: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
|
||||
},
|
||||
Token: {
|
||||
Title: "API ключ",
|
||||
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
|
||||
Placeholder: "API ключ OpenAI",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Баланс аккаунта",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -139,11 +135,7 @@ const ru: PartialLocaleType = {
|
||||
Check: "Проверить",
|
||||
NoAccess: "Введите API ключ, чтобы проверить баланс",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Код доступа",
|
||||
SubTitle: "Контроль доступа включен",
|
||||
Placeholder: "Требуется код доступа",
|
||||
},
|
||||
|
||||
Model: "Модель",
|
||||
Temperature: {
|
||||
Title: "Температура",
|
||||
@@ -191,6 +183,9 @@ const ru: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Плагин",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Вы - ассистент, который",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Маска",
|
||||
Page: {
|
||||
|
482
app/locales/sk.ts
Normal file
482
app/locales/sk.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { LocaleType } from "./index";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const sk: PartialLocaleType = {
|
||||
WIP: "Už čoskoro...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Neplatný API kľúč, prosím skontrolujte ho na stránke [Nastavenia](/#/settings)."
|
||||
: "Neoprávnený prístup, prosím zadajte prístupový kód na stránke [auth](/#/auth), alebo zadajte váš OpenAI API kľúč.",
|
||||
},
|
||||
Auth: {
|
||||
Title: "Potrebný prístupový kód",
|
||||
Tips: "Prosím, zadajte prístupový kód nižšie",
|
||||
SubTips: "Alebo zadajte váš OpenAI alebo Google API kľúč",
|
||||
Input: "prístupový kód",
|
||||
Confirm: "Potvrdiť",
|
||||
Later: "Neskôr",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} správ`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} správ`,
|
||||
EditMessage: {
|
||||
Title: "Upraviť všetky správy",
|
||||
Topic: {
|
||||
Title: "Téma",
|
||||
SubTitle: "Zmeniť aktuálnu tému",
|
||||
},
|
||||
},
|
||||
Actions: {
|
||||
ChatList: "Prejsť na zoznam chatov",
|
||||
CompressedHistory: "Komprimovaná história výziev",
|
||||
Export: "Exportovať všetky správy ako Markdown",
|
||||
Copy: "Kopírovať",
|
||||
Stop: "Zastaviť",
|
||||
Retry: "Skúsiť znova",
|
||||
Pin: "Pripnúť",
|
||||
PinToastContent: "Pripnuté 1 správy do kontextových výziev",
|
||||
PinToastAction: "Zobraziť",
|
||||
Delete: "Vymazať",
|
||||
Edit: "Upraviť",
|
||||
},
|
||||
Commands: {
|
||||
new: "Začať nový chat",
|
||||
newm: "Začať nový chat s maskou",
|
||||
next: "Ďalší Chat",
|
||||
prev: "Predchádzajúci Chat",
|
||||
clear: "Vymazať kontext",
|
||||
del: "Vymazať Chat",
|
||||
},
|
||||
InputActions: {
|
||||
Stop: "Zastaviť",
|
||||
ToBottom: "Na najnovšie",
|
||||
Theme: {
|
||||
auto: "Automaticky",
|
||||
light: "Svetlý motív",
|
||||
dark: "Tmavý motív",
|
||||
},
|
||||
Prompt: "Výzvy",
|
||||
Masks: "Masky",
|
||||
Clear: "Vymazať kontext",
|
||||
Settings: "Nastavenia",
|
||||
},
|
||||
Rename: "Premenovať Chat",
|
||||
Typing: "Písanie…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} na odoslanie`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter na zalomenie";
|
||||
}
|
||||
return inputHints + ", / na vyhľadávanie výziev, : na použitie príkazov";
|
||||
},
|
||||
Send: "Odoslať",
|
||||
Config: {
|
||||
Reset: "Resetovať na predvolené",
|
||||
SaveAs: "Uložiť ako masku",
|
||||
},
|
||||
IsContext: "Kontextová výzva",
|
||||
},
|
||||
Export: {
|
||||
Title: "Export správ",
|
||||
Copy: "Kopírovať všetko",
|
||||
Download: "Stiahnuť",
|
||||
MessageFromYou: "Správa od vás",
|
||||
MessageFromChatGPT: "Správa od ChatGPT",
|
||||
Share: "Zdieľať na ShareGPT",
|
||||
Format: {
|
||||
Title: "Formát exportu",
|
||||
SubTitle: "Markdown alebo PNG obrázok",
|
||||
},
|
||||
IncludeContext: {
|
||||
Title: "Vrátane kontextu",
|
||||
SubTitle: "Exportovať kontextové výzvy v maske alebo nie",
|
||||
},
|
||||
Steps: {
|
||||
Select: "Vybrať",
|
||||
Preview: "Náhľad",
|
||||
},
|
||||
Image: {
|
||||
Toast: "Snímanie obrázka...",
|
||||
Modal:
|
||||
"Dlhým stlačením alebo kliknutím pravým tlačidlom myši uložte obrázok",
|
||||
},
|
||||
},
|
||||
Select: {
|
||||
Search: "Hľadať",
|
||||
All: "Vybrať všetko",
|
||||
Latest: "Vybrať najnovšie",
|
||||
Clear: "Vymazať",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Výzva pamäti",
|
||||
EmptyContent: "Zatiaľ nič.",
|
||||
Send: "Odoslať pamäť",
|
||||
Copy: "Kopírovať pamäť",
|
||||
Reset: "Resetovať reláciu",
|
||||
ResetConfirm:
|
||||
"Resetovaním sa vymaže aktuálna história konverzácie a historická pamäť. Ste si istí, že chcete resetovať?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Nový Chat",
|
||||
DeleteChat: "Potvrdiť vymazanie vybranej konverzácie?",
|
||||
DeleteToast: "Chat vymazaný",
|
||||
Revert: "Vrátiť späť",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Nastavenia",
|
||||
SubTitle: "Všetky nastavenia",
|
||||
Danger: {
|
||||
Reset: {
|
||||
Title: "Resetovať všetky nastavenia",
|
||||
SubTitle: "Resetovať všetky položky nastavení na predvolené",
|
||||
Action: "Resetovať",
|
||||
Confirm: "Potvrdiť resetovanie všetkých nastavení na predvolené?",
|
||||
},
|
||||
Clear: {
|
||||
Title: "Vymazať všetky údaje",
|
||||
SubTitle: "Vymazať všetky správy a nastavenia",
|
||||
Action: "Vymazať",
|
||||
Confirm: "Potvrdiť vymazanie všetkých správ a nastavení?",
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Jazyk", // POZOR: ak pridávate nový preklad, prosím neprekladajte túto hodnotu, nechajte ju ako "Jazyk"
|
||||
All: "Všetky jazyky",
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Veľkosť písma",
|
||||
SubTitle: "Nastaviť veľkosť písma obsahu chatu",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "Vložiť systémové výzvy",
|
||||
SubTitle: "Vložiť globálnu systémovú výzvu pre každú požiadavku",
|
||||
},
|
||||
InputTemplate: {
|
||||
Title: "Šablóna vstupu",
|
||||
SubTitle: "Najnovšia správa bude vyplnená do tejto šablóny",
|
||||
},
|
||||
|
||||
Update: {
|
||||
Version: (x: string) => `Verzia: ${x}`,
|
||||
IsLatest: "Najnovšia verzia",
|
||||
CheckUpdate: "Skontrolovať aktualizácie",
|
||||
IsChecking: "Kontrola aktualizácií...",
|
||||
FoundUpdate: (x: string) => `Nájdená nová verzia: ${x}`,
|
||||
GoToUpdate: "Aktualizovať",
|
||||
},
|
||||
SendKey: "Odoslať kľúč",
|
||||
Theme: "Motív",
|
||||
TightBorder: "Tesný okraj",
|
||||
SendPreviewBubble: {
|
||||
Title: "Bublina náhľadu odoslania",
|
||||
SubTitle: "Náhľad markdownu v bubline",
|
||||
},
|
||||
AutoGenerateTitle: {
|
||||
Title: "Automaticky generovať názov",
|
||||
SubTitle: "Generovať vhodný názov na základe obsahu konverzácie",
|
||||
},
|
||||
Sync: {
|
||||
CloudState: "Posledná aktualizácia",
|
||||
NotSyncYet: "Zatiaľ nesynchronizované",
|
||||
Success: "Synchronizácia úspešná",
|
||||
Fail: "Synchronizácia zlyhala",
|
||||
|
||||
Config: {
|
||||
Modal: {
|
||||
Title: "Konfigurácia synchronizácie",
|
||||
Check: "Skontrolovať pripojenie",
|
||||
},
|
||||
SyncType: {
|
||||
Title: "Typ synchronizácie",
|
||||
SubTitle: "Vyberte svoju obľúbenú službu synchronizácie",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Povoliť CORS Proxy",
|
||||
SubTitle: "Povoliť proxy na obídenie obmedzení cross-origin",
|
||||
},
|
||||
ProxyUrl: {
|
||||
Title: "Koncový bod Proxy",
|
||||
SubTitle: "Platné len pre vstavaný CORS proxy tohto projektu",
|
||||
},
|
||||
|
||||
WebDav: {
|
||||
Endpoint: "Koncový bod WebDAV",
|
||||
UserName: "Meno používateľa",
|
||||
Password: "Heslo",
|
||||
},
|
||||
|
||||
UpStash: {
|
||||
Endpoint: "URL REST služby UpStash Redis",
|
||||
UserName: "Názov zálohy",
|
||||
Password: "Token REST služby UpStash Redis",
|
||||
},
|
||||
},
|
||||
|
||||
LocalState: "Lokálne údaje",
|
||||
Overview: (overview: any) => {
|
||||
return `${overview.chat} chaty, ${overview.message} správy, ${overview.prompt} výzvy, ${overview.mask} masky`;
|
||||
},
|
||||
ImportFailed: "Import z súboru zlyhal",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "Úvodná obrazovka masky",
|
||||
SubTitle: "Zobraziť úvodnú obrazovku masky pred začatím nového chatu",
|
||||
},
|
||||
Builtin: {
|
||||
Title: "Skryť vstavané masky",
|
||||
SubTitle: "Skryť vstavané masky v zozname masiek",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Zakázať automatické dopĺňanie",
|
||||
SubTitle: "Zadajte / na spustenie automatického dopĺňania",
|
||||
},
|
||||
List: "Zoznam výziev",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} vstavaných, ${custom} užívateľsky definovaných`,
|
||||
Edit: "Upraviť",
|
||||
Modal: {
|
||||
Title: "Zoznam výziev",
|
||||
Add: "Pridať jednu",
|
||||
Search: "Hľadať výzvy",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Upraviť výzvu",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Počet pripojených správ",
|
||||
SubTitle: "Počet odoslaných správ pripojených na požiadavku",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Práh kompresie histórie",
|
||||
SubTitle:
|
||||
"Bude komprimované, ak dĺžka nekomprimovaných správ presiahne túto hodnotu",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Stav účtu",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Tento mesiac použité ${used}, predplatné ${total}`;
|
||||
},
|
||||
IsChecking: "Kontroluje sa...",
|
||||
Check: "Skontrolovať",
|
||||
NoAccess: "Zadajte API kľúč na skontrolovanie zostatku",
|
||||
},
|
||||
Access: {
|
||||
AccessCode: {
|
||||
Title: "Prístupový kód",
|
||||
SubTitle: "Povolený prístupový kód",
|
||||
Placeholder: "Zadajte kód",
|
||||
},
|
||||
CustomEndpoint: {
|
||||
Title: "Vlastný koncový bod",
|
||||
SubTitle: "Použiť vlastnú službu Azure alebo OpenAI",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Poskytovateľ modelu",
|
||||
SubTitle: "Vyberte Azure alebo OpenAI",
|
||||
},
|
||||
OpenAI: {
|
||||
ApiKey: {
|
||||
Title: "API kľúč OpenAI",
|
||||
SubTitle: "Použiť vlastný API kľúč OpenAI",
|
||||
Placeholder: "sk-xxx",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Koncový bod OpenAI",
|
||||
SubTitle:
|
||||
"Musí začínať http(s):// alebo použiť /api/openai ako predvolený",
|
||||
},
|
||||
},
|
||||
Azure: {
|
||||
ApiKey: {
|
||||
Title: "API kľúč Azure",
|
||||
SubTitle: "Skontrolujte svoj API kľúč v Azure konzole",
|
||||
Placeholder: "API kľúč Azure",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Koncový bod Azure",
|
||||
SubTitle: "Príklad: ",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Verzia API Azure",
|
||||
SubTitle: "Skontrolujte svoju verziu API v Azure konzole",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Vlastné modely",
|
||||
SubTitle: "Možnosti vlastného modelu, oddelené čiarkou",
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API kľúč",
|
||||
SubTitle:
|
||||
"Obísť obmedzenia prístupu heslom pomocou vlastného API kľúča Google AI Studio",
|
||||
Placeholder: "API kľúč Google AI Studio",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresa koncového bodu",
|
||||
SubTitle: "Príklad:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Verzia API (gemini-pro verzia API)",
|
||||
SubTitle: "Vyberte špecifickú verziu časti",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Teplota",
|
||||
SubTitle: "Vyššia hodnota robí výstup náhodnejším",
|
||||
},
|
||||
TopP: {
|
||||
Title: "Top P",
|
||||
SubTitle: "Neupravujte túto hodnotu spolu s teplotou",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Maximálny počet tokenov",
|
||||
SubTitle: "Maximálna dĺžka vstupných tokenov a generovaných tokenov",
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "Penalizácia za prítomnosť",
|
||||
SubTitle:
|
||||
"Vyššia hodnota zvyšuje pravdepodobnosť hovorenia o nových témach",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "Penalizácia za frekvenciu",
|
||||
SubTitle:
|
||||
"Vyššia hodnota znižuje pravdepodobnosť opakovania rovnakej línie",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Nová konverzácia",
|
||||
BotHello: "Ahoj! Ako vám dnes môžem pomôcť?",
|
||||
Error: "Niečo sa pokazilo, skúste to prosím neskôr znova.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Toto je zhrnutie histórie chatu ako rekapitulácia: " + content,
|
||||
Topic:
|
||||
"Prosím, vygenerujte štvor- až päťslovný titul, ktorý zhrnie našu konverzáciu bez akéhokoľvek úvodu, interpunkcie, úvodzoviek, bodiek, symbolov, tučného textu alebo ďalšieho textu. Odstráňte uzatváracie úvodzovky.",
|
||||
Summarize:
|
||||
"Stručne zhrňte diskusiu na menej ako 200 slov, aby ste ju mohli použiť ako výzvu pre budúci kontext.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Skopírované do schránky",
|
||||
Failed:
|
||||
"Kopírovanie zlyhalo, prosím udeľte povolenie na prístup k schránke",
|
||||
},
|
||||
Download: {
|
||||
Success: "Obsah stiahnutý do vášho adresára.",
|
||||
Failed: "Stiahnutie zlyhalo.",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `S ${x} kontextovými výzvami`,
|
||||
Edit: "Aktuálne nastavenia chatu",
|
||||
Add: "Pridať výzvu",
|
||||
Clear: "Kontext vyčistený",
|
||||
Revert: "Vrátiť späť",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Ste asistent, ktorý",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Maska",
|
||||
Page: {
|
||||
Title: "Šablóna výziev",
|
||||
SubTitle: (count: number) => `${count} šablón výziev`,
|
||||
Search: "Hľadať šablóny",
|
||||
Create: "Vytvoriť",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} výziev`,
|
||||
Chat: "Chat",
|
||||
View: "Zobraziť",
|
||||
Edit: "Upraviť",
|
||||
Delete: "Vymazať",
|
||||
DeleteConfirm: "Potvrdiť vymazanie?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Upraviť šablónu výziev ${readonly ? "(iba na čítanie)" : ""}`,
|
||||
Download: "Stiahnuť",
|
||||
Clone: "Klonovať",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar robota",
|
||||
Name: "Meno robota",
|
||||
Sync: {
|
||||
Title: "Použiť globálne nastavenia",
|
||||
SubTitle: "Použiť globálne nastavenia v tomto chate",
|
||||
Confirm: "Potvrdiť prepísanie vlastného nastavenia globálnym?",
|
||||
},
|
||||
HideContext: {
|
||||
Title: "Skryť kontextové výzvy",
|
||||
SubTitle: "Nezobrazovať kontextové výzvy v chate",
|
||||
},
|
||||
Share: {
|
||||
Title: "Zdieľať túto masku",
|
||||
SubTitle: "Vygenerovať odkaz na túto masku",
|
||||
Action: "Kopírovať odkaz",
|
||||
},
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Vrátiť sa",
|
||||
Skip: "Len začať",
|
||||
Title: "Vybrať masku",
|
||||
SubTitle: "Chatovať s dušou za maskou",
|
||||
More: "Nájsť viac",
|
||||
NotShow: "Už nezobrazovať",
|
||||
ConfirmNoShow:
|
||||
"Potvrdiť deaktiváciu? Môžete ju neskôr znova povoliť v nastaveniach.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Potvrdiť",
|
||||
Cancel: "Zrušiť",
|
||||
Close: "Zavrieť",
|
||||
Create: "Vytvoriť",
|
||||
Edit: "Upraviť",
|
||||
Export: "Exportovať",
|
||||
Import: "Importovať",
|
||||
Sync: "Synchronizovať",
|
||||
Config: "Konfigurácia",
|
||||
},
|
||||
Exporter: {
|
||||
Description: {
|
||||
Title: "Zobrazia sa len správy po vyčistení kontextu",
|
||||
},
|
||||
Model: "Model",
|
||||
Messages: "Správy",
|
||||
Topic: "Téma",
|
||||
Time: "Čas",
|
||||
},
|
||||
|
||||
URLCommand: {
|
||||
Code: "Zistený prístupový kód z URL, potvrdiť na aplikovanie?",
|
||||
Settings: "Zistené nastavenia z URL, potvrdiť na aplikovanie?",
|
||||
},
|
||||
};
|
||||
|
||||
export default sk;
|
@@ -124,11 +124,7 @@ const tr: PartialLocaleType = {
|
||||
SubTitle:
|
||||
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Anahtarı",
|
||||
SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın",
|
||||
Placeholder: "OpenAI API Anahtarı",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Hesap Bakiyesi",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -138,11 +134,7 @@ const tr: PartialLocaleType = {
|
||||
Check: "Tekrar Kontrol Et",
|
||||
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Erişim Kodu",
|
||||
SubTitle: "Erişim kontrolü etkinleştirme",
|
||||
Placeholder: "Erişim Kodu Gerekiyor",
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Gerçeklik",
|
||||
@@ -191,6 +183,9 @@ const tr: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Sen bir asistansın",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mask",
|
||||
Page: {
|
||||
|
@@ -7,32 +7,32 @@ const tw: PartialLocaleType = {
|
||||
Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 條對話`,
|
||||
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
|
||||
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 則對話`,
|
||||
Actions: {
|
||||
ChatList: "查看訊息列表",
|
||||
CompressedHistory: "查看壓縮後的歷史 Prompt",
|
||||
ChatList: "檢視訊息列表",
|
||||
CompressedHistory: "檢視壓縮後的歷史 Prompt",
|
||||
Export: "匯出聊天紀錄",
|
||||
Copy: "複製",
|
||||
Stop: "停止",
|
||||
Retry: "重試",
|
||||
Delete: "刪除",
|
||||
},
|
||||
Rename: "重命名對話",
|
||||
Rename: "重新命名對話",
|
||||
Typing: "正在輸入…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
|
||||
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可傳送`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ",Shift + Enter 鍵換行";
|
||||
}
|
||||
return inputHints;
|
||||
},
|
||||
Send: "發送",
|
||||
Send: "傳送",
|
||||
Config: {
|
||||
Reset: "重置默认",
|
||||
SaveAs: "另存为面具",
|
||||
Reset: "重設",
|
||||
SaveAs: "另存新檔",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
@@ -46,7 +46,7 @@ const tw: PartialLocaleType = {
|
||||
Title: "上下文記憶 Prompt",
|
||||
EmptyContent: "尚未記憶",
|
||||
Copy: "複製全部",
|
||||
Send: "發送記憶",
|
||||
Send: "傳送記憶",
|
||||
Reset: "重設對話",
|
||||
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
|
||||
},
|
||||
@@ -62,7 +62,7 @@ const tw: PartialLocaleType = {
|
||||
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "所有语言",
|
||||
All: "所有語言",
|
||||
},
|
||||
Avatar: "大頭貼",
|
||||
FontSize: {
|
||||
@@ -70,28 +70,28 @@ const tw: PartialLocaleType = {
|
||||
SubTitle: "聊天內容的字型大小",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "注入系統提示",
|
||||
SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示",
|
||||
Title: "匯入系統提示",
|
||||
SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
|
||||
},
|
||||
Update: {
|
||||
Version: (x: string) => `當前版本:${x}`,
|
||||
Version: (x: string) => `目前版本:${x}`,
|
||||
IsLatest: "已是最新版本",
|
||||
CheckUpdate: "檢查更新",
|
||||
IsChecking: "正在檢查更新...",
|
||||
FoundUpdate: (x: string) => `發現新版本:${x}`,
|
||||
GoToUpdate: "前往更新",
|
||||
},
|
||||
SendKey: "發送鍵",
|
||||
SendKey: "傳送鍵",
|
||||
Theme: "主題",
|
||||
TightBorder: "緊湊邊框",
|
||||
SendPreviewBubble: {
|
||||
Title: "預覽氣泡",
|
||||
SubTitle: "在预览气泡中预览 Markdown 内容",
|
||||
SubTitle: "在預覽氣泡中預覽 Markdown 內容",
|
||||
},
|
||||
Mask: {
|
||||
Splash: {
|
||||
Title: "面具启动页",
|
||||
SubTitle: "新建聊天时,展示面具启动页",
|
||||
Title: "面具啟動頁面",
|
||||
SubTitle: "新增聊天時,呈現面具啟動頁面",
|
||||
},
|
||||
},
|
||||
Prompt: {
|
||||
@@ -101,7 +101,7 @@ const tw: PartialLocaleType = {
|
||||
},
|
||||
List: "自定義提示詞列表",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`內建 ${builtin} 條,用戶定義 ${custom} 條`,
|
||||
`內建 ${builtin} 條,使用者定義 ${custom} 條`,
|
||||
Edit: "編輯",
|
||||
Modal: {
|
||||
Title: "提示詞列表",
|
||||
@@ -109,7 +109,7 @@ const tw: PartialLocaleType = {
|
||||
Search: "搜尋提示詞",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "编辑提示词",
|
||||
Title: "編輯提示詞",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
@@ -120,11 +120,7 @@ const tw: PartialLocaleType = {
|
||||
Title: "歷史訊息長度壓縮閾值",
|
||||
SubTitle: "當未壓縮的歷史訊息超過該值時,將進行壓縮",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "使用自己的 Key 可規避授權存取限制",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "帳戶餘額",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -132,13 +128,9 @@ const tw: PartialLocaleType = {
|
||||
},
|
||||
IsChecking: "正在檢查…",
|
||||
Check: "重新檢查",
|
||||
NoAccess: "輸入API Key查看餘額",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "授權碼",
|
||||
SubTitle: "目前是未授權存取狀態",
|
||||
Placeholder: "請輸入授權碼",
|
||||
NoAccess: "輸入 API Key 檢視餘額",
|
||||
},
|
||||
|
||||
Model: "模型 (model)",
|
||||
Temperature: {
|
||||
Title: "隨機性 (temperature)",
|
||||
@@ -150,7 +142,7 @@ const tw: PartialLocaleType = {
|
||||
},
|
||||
PresencePenalty: {
|
||||
Title: "話題新穎度 (presence_penalty)",
|
||||
SubTitle: "值越大,越有可能擴展到新話題",
|
||||
SubTitle: "值越大,越有可能拓展到新話題",
|
||||
},
|
||||
FrequencyPenalty: {
|
||||
Title: "頻率懲罰度 (frequency_penalty)",
|
||||
@@ -163,7 +155,7 @@ const tw: PartialLocaleType = {
|
||||
Error: "出錯了,請稍後再嘗試",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
||||
"這是 AI 與使用者的歷史聊天總結,作為前情提要:" + content,
|
||||
Topic:
|
||||
"Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.",
|
||||
Summarize:
|
||||
@@ -179,53 +171,54 @@ const tw: PartialLocaleType = {
|
||||
Edit: "前置上下文和歷史記憶",
|
||||
Add: "新增一條",
|
||||
},
|
||||
Plugin: { Name: "插件" },
|
||||
Plugin: { Name: "外掛" },
|
||||
FineTuned: { Sysmessage: "你是一個助手" },
|
||||
Mask: {
|
||||
Name: "面具",
|
||||
Page: {
|
||||
Title: "预设角色面具",
|
||||
SubTitle: (count: number) => `${count} 个预设角色定义`,
|
||||
Search: "搜索角色面具",
|
||||
Create: "新建",
|
||||
Title: "預設角色面具",
|
||||
SubTitle: (count: number) => `${count} 個預設角色定義`,
|
||||
Search: "搜尋角色面具",
|
||||
Create: "新增",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `包含 ${count} 条预设对话`,
|
||||
Chat: "对话",
|
||||
View: "查看",
|
||||
Edit: "编辑",
|
||||
Delete: "删除",
|
||||
DeleteConfirm: "确认删除?",
|
||||
Info: (count: number) => `包含 ${count} 條預設對話`,
|
||||
Chat: "對話",
|
||||
View: "檢視",
|
||||
Edit: "編輯",
|
||||
Delete: "刪除",
|
||||
DeleteConfirm: "確認刪除?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`编辑预设面具 ${readonly ? "(只读)" : ""}`,
|
||||
Download: "下载预设",
|
||||
Clone: "克隆预设",
|
||||
`編輯預設面具 ${readonly ? "(只讀)" : ""}`,
|
||||
Download: "下載預設",
|
||||
Clone: "複製預設",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "角色头像",
|
||||
Name: "角色名称",
|
||||
Avatar: "角色頭像",
|
||||
Name: "角色名稱",
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "返回",
|
||||
Skip: "跳过",
|
||||
Title: "挑选一个面具",
|
||||
SubTitle: "现在开始,与面具背后的灵魂思维碰撞",
|
||||
More: "搜索更多",
|
||||
NotShow: "不再展示",
|
||||
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。",
|
||||
Skip: "跳過",
|
||||
Title: "挑選一個面具",
|
||||
SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
|
||||
More: "搜尋更多",
|
||||
NotShow: "不再呈現",
|
||||
ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。",
|
||||
},
|
||||
UI: {
|
||||
Confirm: "确认",
|
||||
Confirm: "確認",
|
||||
Cancel: "取消",
|
||||
Close: "关闭",
|
||||
Create: "新建",
|
||||
Edit: "编辑",
|
||||
Close: "關閉",
|
||||
Create: "新增",
|
||||
Edit: "編輯",
|
||||
},
|
||||
Exporter: {
|
||||
Model: "模型",
|
||||
Messages: "消息",
|
||||
Messages: "訊息",
|
||||
Topic: "主題",
|
||||
Time: "時間",
|
||||
},
|
||||
|
@@ -123,11 +123,7 @@ const vi: PartialLocaleType = {
|
||||
Title: "Ngưỡng nén lịch sử tin nhắn",
|
||||
SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
|
||||
Usage: {
|
||||
Title: "Hạn mức tài khoản",
|
||||
SubTitle(used: any, total: any) {
|
||||
@@ -137,11 +133,7 @@ const vi: PartialLocaleType = {
|
||||
Check: "Kiểm tra",
|
||||
NoAccess: "Nhập API Key để kiểm tra hạn mức",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Mã truy cập",
|
||||
SubTitle: "Đã bật kiểm soát truy cập",
|
||||
Placeholder: "Nhập mã truy cập",
|
||||
},
|
||||
|
||||
Model: "Mô hình",
|
||||
Temperature: {
|
||||
Title: "Tính ngẫu nhiên (temperature)",
|
||||
@@ -186,6 +178,9 @@ const vi: PartialLocaleType = {
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
FineTuned: {
|
||||
Sysmessage: "Bạn là một trợ lý",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mẫu",
|
||||
Page: {
|
||||
|
@@ -35,7 +35,7 @@ export const EN_MASKS: BuiltinMask[] = [
|
||||
id: "prompt-improve-0",
|
||||
role: "user",
|
||||
content:
|
||||
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finsihed and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
|
||||
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finished and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
|
||||
date: "",
|
||||
},
|
||||
{
|
||||
|
@@ -10,7 +10,11 @@ export default async function App() {
|
||||
return (
|
||||
<>
|
||||
<Home />
|
||||
{serverConfig?.isVercel && <Analytics />}
|
||||
{serverConfig?.isVercel && (
|
||||
<>
|
||||
<Analytics />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -1,101 +1,121 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
ServiceProvider,
|
||||
StoreKey,
|
||||
} from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
import { BOT_HELLO } from "./chat";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export interface AccessControlStore {
|
||||
accessCode: string;
|
||||
token: string;
|
||||
|
||||
needCode: boolean;
|
||||
hideUserApiKey: boolean;
|
||||
hideBalanceQuery: boolean;
|
||||
disableGPT4: boolean;
|
||||
|
||||
openaiUrl: string;
|
||||
|
||||
updateToken: (_: string) => void;
|
||||
updateCode: (_: string) => void;
|
||||
updateOpenAiUrl: (_: string) => void;
|
||||
enabledAccessControl: () => boolean;
|
||||
isAuthorized: () => boolean;
|
||||
fetch: () => void;
|
||||
}
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { ensure } from "../utils/clone";
|
||||
|
||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
const DEFAULT_OPENAI_URL =
|
||||
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
|
||||
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
|
||||
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : ApiPath.OpenAI;
|
||||
|
||||
export const useAccessStore = create<AccessControlStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
token: "",
|
||||
accessCode: "",
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
hideBalanceQuery: false,
|
||||
disableGPT4: false,
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
useCustomConfig: false,
|
||||
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
provider: ServiceProvider.OpenAI,
|
||||
|
||||
enabledAccessControl() {
|
||||
get().fetch();
|
||||
// openai
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
openaiApiKey: "",
|
||||
|
||||
return get().needCode;
|
||||
},
|
||||
updateCode(code: string) {
|
||||
set(() => ({ accessCode: code?.trim() }));
|
||||
},
|
||||
updateToken(token: string) {
|
||||
set(() => ({ token: token?.trim() }));
|
||||
},
|
||||
updateOpenAiUrl(url: string) {
|
||||
set(() => ({ openaiUrl: url?.trim() }));
|
||||
},
|
||||
isAuthorized() {
|
||||
get().fetch();
|
||||
// azure
|
||||
azureUrl: "",
|
||||
azureApiKey: "",
|
||||
azureApiVersion: "2023-08-01-preview",
|
||||
|
||||
// has token or has code or disabled access control
|
||||
return (
|
||||
!!get().token || !!get().accessCode || !get().enabledAccessControl()
|
||||
);
|
||||
},
|
||||
fetch() {
|
||||
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
|
||||
fetchState = 1;
|
||||
fetch("/api/config", {
|
||||
method: "post",
|
||||
body: null,
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ ...res }));
|
||||
// google ai studio
|
||||
googleUrl: "",
|
||||
googleApiKey: "",
|
||||
googleApiVersion: "v1",
|
||||
|
||||
if (res.disableGPT4) {
|
||||
DEFAULT_MODELS.forEach(
|
||||
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("[Config] failed to fetch config");
|
||||
})
|
||||
.finally(() => {
|
||||
fetchState = 2;
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Access,
|
||||
version: 1,
|
||||
// server config
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
hideBalanceQuery: false,
|
||||
disableGPT4: false,
|
||||
disableFastLink: false,
|
||||
customModels: "",
|
||||
};
|
||||
|
||||
export const useAccessStore = createPersistStore(
|
||||
{ ...DEFAULT_ACCESS_STATE },
|
||||
|
||||
(set, get) => ({
|
||||
enabledAccessControl() {
|
||||
this.fetch();
|
||||
|
||||
return get().needCode;
|
||||
},
|
||||
),
|
||||
|
||||
isValidOpenAI() {
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
},
|
||||
|
||||
isValidAzure() {
|
||||
return ensure(get(), ["azureUrl", "azureApiKey", "azureApiVersion"]);
|
||||
},
|
||||
|
||||
isValidGoogle() {
|
||||
return ensure(get(), ["googleApiKey"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
|
||||
// has token or has code or disabled access control
|
||||
return (
|
||||
this.isValidOpenAI() ||
|
||||
this.isValidAzure() ||
|
||||
this.isValidGoogle() ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
},
|
||||
fetch() {
|
||||
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
|
||||
fetchState = 1;
|
||||
fetch("/api/config", {
|
||||
method: "post",
|
||||
body: null,
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ ...res }));
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("[Config] failed to fetch config");
|
||||
})
|
||||
.finally(() => {
|
||||
fetchState = 2;
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Access,
|
||||
version: 2,
|
||||
migrate(persistedState, version) {
|
||||
if (version < 2) {
|
||||
const state = persistedState as {
|
||||
token: string;
|
||||
openaiApiKey: string;
|
||||
azureApiVersion: string;
|
||||
googleApiKey: string;
|
||||
};
|
||||
state.openaiApiKey = state.token;
|
||||
state.azureApiVersion = "2023-08-01-preview";
|
||||
}
|
||||
|
||||
return persistedState as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@@ -1,6 +1,3 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
import { trimTopic } from "../utils";
|
||||
|
||||
import Locale, { getLang } from "../locales";
|
||||
@@ -10,13 +7,17 @@ import { createEmptyMask, Mask } from "./mask";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
DEFAULT_SYSTEM_TEMPLATE,
|
||||
KnowledgeCutOffDate,
|
||||
ModelProvider,
|
||||
StoreKey,
|
||||
SUMMARIZE_MODEL,
|
||||
} from "../constant";
|
||||
import { api, RequestMessage } from "../client/api";
|
||||
import { ClientApi, RequestMessage } from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export type ChatMessage = RequestMessage & {
|
||||
date: string;
|
||||
@@ -80,31 +81,9 @@ function createEmptySession(): ChatSession {
|
||||
};
|
||||
}
|
||||
|
||||
interface ChatStore {
|
||||
sessions: ChatSession[];
|
||||
currentSessionIndex: number;
|
||||
clearSessions: () => void;
|
||||
moveSession: (from: number, to: number) => void;
|
||||
selectSession: (index: number) => void;
|
||||
newSession: (mask?: Mask) => void;
|
||||
deleteSession: (index: number) => void;
|
||||
currentSession: () => ChatSession;
|
||||
nextSession: (delta: number) => void;
|
||||
onNewMessage: (message: ChatMessage) => void;
|
||||
onUserInput: (content: string) => Promise<void>;
|
||||
summarizeSession: () => void;
|
||||
updateStat: (message: ChatMessage) => void;
|
||||
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
|
||||
updateMessage: (
|
||||
sessionIndex: number,
|
||||
messageIndex: number,
|
||||
updater: (message?: ChatMessage) => void,
|
||||
) => void;
|
||||
resetSession: () => void;
|
||||
getMessagesWithMemory: () => ChatMessage[];
|
||||
getMemoryPrompt: () => ChatMessage;
|
||||
|
||||
clearAllData: () => void;
|
||||
function getSummarizeModel(currentModel: string) {
|
||||
// if it is using gpt-* models, force to use 3.5 to summarize
|
||||
return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel;
|
||||
}
|
||||
|
||||
function countMessages(msgs: ChatMessage[]) {
|
||||
@@ -112,7 +91,11 @@ function countMessages(msgs: ChatMessage[]) {
|
||||
}
|
||||
|
||||
function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
let cutoff =
|
||||
KnowledgeCutOffDate[modelConfig.model] ?? KnowledgeCutOffDate.default;
|
||||
|
||||
const vars = {
|
||||
cutoff,
|
||||
model: modelConfig.model,
|
||||
time: new Date().toLocaleString(),
|
||||
lang: getLang(),
|
||||
@@ -134,12 +117,22 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
return output;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
const DEFAULT_CHAT_STATE = {
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
};
|
||||
|
||||
export const useChatStore = createPersistStore(
|
||||
DEFAULT_CHAT_STATE,
|
||||
(set, _get) => {
|
||||
function get() {
|
||||
return {
|
||||
..._get(),
|
||||
...methods,
|
||||
};
|
||||
}
|
||||
|
||||
const methods = {
|
||||
clearSessions() {
|
||||
set(() => ({
|
||||
sessions: [createEmptySession()],
|
||||
@@ -178,7 +171,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
newSession(mask) {
|
||||
newSession(mask?: Mask) {
|
||||
const session = createEmptySession();
|
||||
|
||||
if (mask) {
|
||||
@@ -201,14 +194,14 @@ export const useChatStore = create<ChatStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
nextSession(delta) {
|
||||
nextSession(delta: number) {
|
||||
const n = get().sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = get().currentSessionIndex;
|
||||
get().selectSession(limit(i + delta));
|
||||
},
|
||||
|
||||
deleteSession(index) {
|
||||
deleteSession(index: number) {
|
||||
const deletingLastSession = get().sessions.length === 1;
|
||||
const deletedSession = get().sessions.at(index);
|
||||
|
||||
@@ -265,7 +258,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
return session;
|
||||
},
|
||||
|
||||
onNewMessage(message) {
|
||||
onNewMessage(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat();
|
||||
session.lastUpdate = Date.now();
|
||||
@@ -274,7 +267,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
get().summarizeSession();
|
||||
},
|
||||
|
||||
async onUserInput(content) {
|
||||
async onUserInput(content: string) {
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
|
||||
@@ -309,6 +302,13 @@ export const useChatStore = create<ChatStore>()(
|
||||
]);
|
||||
});
|
||||
|
||||
var api: ClientApi;
|
||||
if (modelConfig.model === "gemini-pro") {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
// make request
|
||||
api.llm.chat({
|
||||
messages: sendMessages,
|
||||
@@ -386,8 +386,12 @@ export const useChatStore = create<ChatStore>()(
|
||||
const contextPrompts = session.mask.context.slice();
|
||||
|
||||
// system prompts, to get close to OpenAI Web ChatGPT
|
||||
const shouldInjectSystemPrompts = modelConfig.enableInjectSystemPrompts;
|
||||
const systemPrompts = shouldInjectSystemPrompts
|
||||
const shouldInjectSystemPrompts =
|
||||
modelConfig.enableInjectSystemPrompts &&
|
||||
session.mask.modelConfig.model.startsWith("gpt-");
|
||||
|
||||
var systemPrompts: ChatMessage[] = [];
|
||||
systemPrompts = shouldInjectSystemPrompts
|
||||
? [
|
||||
createMessage({
|
||||
role: "system",
|
||||
@@ -479,7 +483,16 @@ export const useChatStore = create<ChatStore>()(
|
||||
},
|
||||
|
||||
summarizeSession() {
|
||||
const config = useAppConfig.getState();
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
|
||||
var api: ClientApi;
|
||||
if (modelConfig.model === "gemini-pro") {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
// remove error messages if any
|
||||
const messages = session.messages;
|
||||
@@ -487,6 +500,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
// should summarize topic after chating more than 50 words
|
||||
const SUMMARIZE_MIN_LEN = 50;
|
||||
if (
|
||||
config.enableAutoGenerateTitle &&
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(messages) >= SUMMARIZE_MIN_LEN
|
||||
) {
|
||||
@@ -499,7 +513,7 @@ export const useChatStore = create<ChatStore>()(
|
||||
api.llm.chat({
|
||||
messages: topicMessages,
|
||||
config: {
|
||||
model: "gpt-3.5-turbo",
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
},
|
||||
onFinish(message) {
|
||||
get().updateCurrentSession(
|
||||
@@ -510,8 +524,6 @@ export const useChatStore = create<ChatStore>()(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
const summarizeIndex = Math.max(
|
||||
session.lastSummarizeIndex,
|
||||
session.clearContextIndex ?? 0,
|
||||
@@ -553,13 +565,20 @@ export const useChatStore = create<ChatStore>()(
|
||||
date: "",
|
||||
}),
|
||||
),
|
||||
config: { ...modelConfig, stream: true, model: "gpt-3.5-turbo" },
|
||||
config: {
|
||||
...modelConfig,
|
||||
stream: true,
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
},
|
||||
onUpdate(message) {
|
||||
session.memoryPrompt = message;
|
||||
},
|
||||
onFinish(message) {
|
||||
console.log("[Memory] ", message);
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
get().updateCurrentSession((session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
},
|
||||
onError(err) {
|
||||
console.error("[Summarize] ", err);
|
||||
@@ -568,14 +587,14 @@ export const useChatStore = create<ChatStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
updateStat(message) {
|
||||
updateStat(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
session.stat.charCount += message.content.length;
|
||||
// TODO: should update chat count and word count
|
||||
});
|
||||
},
|
||||
|
||||
updateCurrentSession(updater) {
|
||||
updateCurrentSession(updater: (session: ChatSession) => void) {
|
||||
const sessions = get().sessions;
|
||||
const index = get().currentSessionIndex;
|
||||
updater(sessions[index]);
|
||||
@@ -586,56 +605,60 @@ export const useChatStore = create<ChatStore>()(
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Chat,
|
||||
version: 3.1,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as any;
|
||||
const newState = JSON.parse(JSON.stringify(state)) as ChatStore;
|
||||
};
|
||||
|
||||
if (version < 2) {
|
||||
newState.sessions = [];
|
||||
return methods;
|
||||
},
|
||||
{
|
||||
name: StoreKey.Chat,
|
||||
version: 3.1,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as any;
|
||||
const newState = JSON.parse(
|
||||
JSON.stringify(state),
|
||||
) as typeof DEFAULT_CHAT_STATE;
|
||||
|
||||
const oldSessions = state.sessions;
|
||||
for (const oldSession of oldSessions) {
|
||||
const newSession = createEmptySession();
|
||||
newSession.topic = oldSession.topic;
|
||||
newSession.messages = [...oldSession.messages];
|
||||
newSession.mask.modelConfig.sendMemory = true;
|
||||
newSession.mask.modelConfig.historyMessageCount = 4;
|
||||
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
|
||||
newState.sessions.push(newSession);
|
||||
if (version < 2) {
|
||||
newState.sessions = [];
|
||||
|
||||
const oldSessions = state.sessions;
|
||||
for (const oldSession of oldSessions) {
|
||||
const newSession = createEmptySession();
|
||||
newSession.topic = oldSession.topic;
|
||||
newSession.messages = [...oldSession.messages];
|
||||
newSession.mask.modelConfig.sendMemory = true;
|
||||
newSession.mask.modelConfig.historyMessageCount = 4;
|
||||
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
|
||||
newState.sessions.push(newSession);
|
||||
}
|
||||
}
|
||||
|
||||
if (version < 3) {
|
||||
// migrate id to nanoid
|
||||
newState.sessions.forEach((s) => {
|
||||
s.id = nanoid();
|
||||
s.messages.forEach((m) => (m.id = nanoid()));
|
||||
});
|
||||
}
|
||||
|
||||
// Enable `enableInjectSystemPrompts` attribute for old sessions.
|
||||
// Resolve issue of old sessions not automatically enabling.
|
||||
if (version < 3.1) {
|
||||
newState.sessions.forEach((s) => {
|
||||
if (
|
||||
// Exclude those already set by user
|
||||
!s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
|
||||
) {
|
||||
// Because users may have changed this configuration,
|
||||
// the user's current configuration is used instead of the default
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.enableInjectSystemPrompts =
|
||||
config.modelConfig.enableInjectSystemPrompts;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 3) {
|
||||
// migrate id to nanoid
|
||||
newState.sessions.forEach((s) => {
|
||||
s.id = nanoid();
|
||||
s.messages.forEach((m) => (m.id = nanoid()));
|
||||
});
|
||||
}
|
||||
|
||||
// Enable `enableInjectSystemPrompts` attribute for old sessions.
|
||||
// Resolve issue of old sessions not automatically enabling.
|
||||
if (version < 3.1) {
|
||||
newState.sessions.forEach((s) => {
|
||||
if (
|
||||
// Exclude those already set by user
|
||||
!s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
|
||||
) {
|
||||
// Because users may have changed this configuration,
|
||||
// the user's current configuration is used instead of the default
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.enableInjectSystemPrompts =
|
||||
config.modelConfig.enableInjectSystemPrompts;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
return newState as any;
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { LLMModel } from "../client/api";
|
||||
import { isMacOS } from "../utils";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
StoreKey,
|
||||
} from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||
|
||||
@@ -21,13 +26,16 @@ export enum Theme {
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
||||
lastUpdate: Date.now(), // timestamp, to merge state
|
||||
|
||||
submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter,
|
||||
avatar: "1f603",
|
||||
fontSize: 14,
|
||||
theme: Theme.Auto as Theme,
|
||||
tightBorder: !!getClientConfig()?.isApp,
|
||||
sendPreviewBubble: true,
|
||||
sidebarWidth: 300,
|
||||
enableAutoGenerateTitle: true,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
@@ -41,7 +49,7 @@ export const DEFAULT_CONFIG = {
|
||||
model: "gpt-3.5-turbo" as ModelType,
|
||||
temperature: 0.5,
|
||||
top_p: 1,
|
||||
max_tokens: 2000,
|
||||
max_tokens: 4000,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
sendMemory: true,
|
||||
@@ -54,13 +62,6 @@ export const DEFAULT_CONFIG = {
|
||||
|
||||
export type ChatConfig = typeof DEFAULT_CONFIG;
|
||||
|
||||
export type ChatConfigStore = ChatConfig & {
|
||||
reset: () => void;
|
||||
update: (updater: (config: ChatConfig) => void) => void;
|
||||
mergeModels: (newModels: LLMModel[]) => void;
|
||||
allModels: () => LLMModel[];
|
||||
};
|
||||
|
||||
export type ModelConfig = ChatConfig["modelConfig"];
|
||||
|
||||
export function limitNumber(
|
||||
@@ -69,7 +70,7 @@ export function limitNumber(
|
||||
max: number,
|
||||
defaultValue: number,
|
||||
) {
|
||||
if (typeof x !== "number" || isNaN(x)) {
|
||||
if (isNaN(x)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ export const ModalConfigValidator = {
|
||||
return x as ModelType;
|
||||
},
|
||||
max_tokens(x: number) {
|
||||
return limitNumber(x, 0, 100000, 2000);
|
||||
return limitNumber(x, 0, 512000, 1024);
|
||||
},
|
||||
presence_penalty(x: number) {
|
||||
return limitNumber(x, -2, 2, 0);
|
||||
@@ -97,81 +98,72 @@ export const ModalConfigValidator = {
|
||||
},
|
||||
};
|
||||
|
||||
export const useAppConfig = create<ChatConfigStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...DEFAULT_CONFIG,
|
||||
|
||||
reset() {
|
||||
set(() => ({ ...DEFAULT_CONFIG }));
|
||||
},
|
||||
|
||||
update(updater) {
|
||||
const config = { ...get() };
|
||||
updater(config);
|
||||
set(() => config);
|
||||
},
|
||||
|
||||
mergeModels(newModels) {
|
||||
if (!newModels || newModels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldModels = get().models;
|
||||
const modelMap: Record<string, LLMModel> = {};
|
||||
|
||||
for (const model of oldModels) {
|
||||
model.available = false;
|
||||
modelMap[model.name] = model;
|
||||
}
|
||||
|
||||
for (const model of newModels) {
|
||||
model.available = true;
|
||||
modelMap[model.name] = model;
|
||||
}
|
||||
|
||||
set(() => ({
|
||||
models: Object.values(modelMap),
|
||||
}));
|
||||
},
|
||||
|
||||
allModels() {
|
||||
const customModels = get()
|
||||
.customModels.split(",")
|
||||
.filter((v) => !!v && v.length > 0)
|
||||
.map((m) => ({ name: m, available: true }));
|
||||
|
||||
const models = get().models.concat(customModels);
|
||||
return models;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 3.6,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as ChatConfig;
|
||||
|
||||
if (version < 3.4) {
|
||||
state.modelConfig.sendMemory = true;
|
||||
state.modelConfig.historyMessageCount = 4;
|
||||
state.modelConfig.compressMessageLengthThreshold = 1000;
|
||||
state.modelConfig.frequency_penalty = 0;
|
||||
state.modelConfig.top_p = 1;
|
||||
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
|
||||
state.dontShowMaskSplashScreen = false;
|
||||
state.hideBuiltinMasks = false;
|
||||
}
|
||||
|
||||
if (version < 3.5) {
|
||||
state.customModels = "claude,claude-100k";
|
||||
}
|
||||
|
||||
if (version < 3.6) {
|
||||
state.modelConfig.enableInjectSystemPrompts = true;
|
||||
}
|
||||
|
||||
return state as any;
|
||||
},
|
||||
export const useAppConfig = createPersistStore(
|
||||
{ ...DEFAULT_CONFIG },
|
||||
(set, get) => ({
|
||||
reset() {
|
||||
set(() => ({ ...DEFAULT_CONFIG }));
|
||||
},
|
||||
),
|
||||
|
||||
mergeModels(newModels: LLMModel[]) {
|
||||
if (!newModels || newModels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldModels = get().models;
|
||||
const modelMap: Record<string, LLMModel> = {};
|
||||
|
||||
for (const model of oldModels) {
|
||||
model.available = false;
|
||||
modelMap[model.name] = model;
|
||||
}
|
||||
|
||||
for (const model of newModels) {
|
||||
model.available = true;
|
||||
modelMap[model.name] = model;
|
||||
}
|
||||
|
||||
set(() => ({
|
||||
models: Object.values(modelMap),
|
||||
}));
|
||||
},
|
||||
|
||||
allModels() {},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 3.8,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as ChatConfig;
|
||||
|
||||
if (version < 3.4) {
|
||||
state.modelConfig.sendMemory = true;
|
||||
state.modelConfig.historyMessageCount = 4;
|
||||
state.modelConfig.compressMessageLengthThreshold = 1000;
|
||||
state.modelConfig.frequency_penalty = 0;
|
||||
state.modelConfig.top_p = 1;
|
||||
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
|
||||
state.dontShowMaskSplashScreen = false;
|
||||
state.hideBuiltinMasks = false;
|
||||
}
|
||||
|
||||
if (version < 3.5) {
|
||||
state.customModels = "claude,claude-100k";
|
||||
}
|
||||
|
||||
if (version < 3.6) {
|
||||
state.modelConfig.enableInjectSystemPrompts = true;
|
||||
}
|
||||
|
||||
if (version < 3.7) {
|
||||
state.enableAutoGenerateTitle = true;
|
||||
}
|
||||
|
||||
if (version < 3.8) {
|
||||
state.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
return state as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { BUILTIN_MASKS } from "../masks";
|
||||
import { getLang, Lang } from "../locales";
|
||||
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
||||
import { ModelConfig, useAppConfig } from "./config";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export type Mask = {
|
||||
id: string;
|
||||
@@ -25,14 +24,6 @@ export const DEFAULT_MASK_STATE = {
|
||||
};
|
||||
|
||||
export type MaskState = typeof DEFAULT_MASK_STATE;
|
||||
type MaskStore = MaskState & {
|
||||
create: (mask?: Partial<Mask>) => Mask;
|
||||
update: (id: string, updater: (mask: Mask) => void) => void;
|
||||
delete: (id: string) => void;
|
||||
search: (text: string) => Mask[];
|
||||
get: (id?: string) => Mask | null;
|
||||
getAll: () => Mask[];
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_AVATAR = "gpt-bot";
|
||||
export const createEmptyMask = () =>
|
||||
@@ -46,89 +37,90 @@ export const createEmptyMask = () =>
|
||||
lang: getLang(),
|
||||
builtin: false,
|
||||
createdAt: Date.now(),
|
||||
} as Mask);
|
||||
}) as Mask;
|
||||
|
||||
export const useMaskStore = create<MaskStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...DEFAULT_MASK_STATE,
|
||||
export const useMaskStore = createPersistStore(
|
||||
{ ...DEFAULT_MASK_STATE },
|
||||
|
||||
create(mask) {
|
||||
const masks = get().masks;
|
||||
const id = nanoid();
|
||||
masks[id] = {
|
||||
...createEmptyMask(),
|
||||
...mask,
|
||||
id,
|
||||
builtin: false,
|
||||
};
|
||||
(set, get) => ({
|
||||
create(mask?: Partial<Mask>) {
|
||||
const masks = get().masks;
|
||||
const id = nanoid();
|
||||
masks[id] = {
|
||||
...createEmptyMask(),
|
||||
...mask,
|
||||
id,
|
||||
builtin: false,
|
||||
};
|
||||
|
||||
set(() => ({ masks }));
|
||||
set(() => ({ masks }));
|
||||
get().markUpdate();
|
||||
|
||||
return masks[id];
|
||||
},
|
||||
update(id, updater) {
|
||||
const masks = get().masks;
|
||||
const mask = masks[id];
|
||||
if (!mask) return;
|
||||
const updateMask = { ...mask };
|
||||
updater(updateMask);
|
||||
masks[id] = updateMask;
|
||||
set(() => ({ masks }));
|
||||
},
|
||||
delete(id) {
|
||||
const masks = get().masks;
|
||||
delete masks[id];
|
||||
set(() => ({ masks }));
|
||||
},
|
||||
|
||||
get(id) {
|
||||
return get().masks[id ?? 1145141919810];
|
||||
},
|
||||
getAll() {
|
||||
const userMasks = Object.values(get().masks).sort(
|
||||
(a, b) => b.createdAt - a.createdAt,
|
||||
);
|
||||
const config = useAppConfig.getState();
|
||||
if (config.hideBuiltinMasks) return userMasks;
|
||||
const buildinMasks = BUILTIN_MASKS.map(
|
||||
(m) =>
|
||||
({
|
||||
...m,
|
||||
modelConfig: {
|
||||
...config.modelConfig,
|
||||
...m.modelConfig,
|
||||
},
|
||||
} as Mask),
|
||||
);
|
||||
return userMasks.concat(buildinMasks);
|
||||
},
|
||||
search(text) {
|
||||
return Object.values(get().masks);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Mask,
|
||||
version: 3.1,
|
||||
|
||||
migrate(state, version) {
|
||||
const newState = JSON.parse(JSON.stringify(state)) as MaskState;
|
||||
|
||||
// migrate mask id to nanoid
|
||||
if (version < 3) {
|
||||
Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
|
||||
}
|
||||
|
||||
if (version < 3.1) {
|
||||
const updatedMasks: Record<string, Mask> = {};
|
||||
Object.values(newState.masks).forEach((m) => {
|
||||
updatedMasks[m.id] = m;
|
||||
});
|
||||
newState.masks = updatedMasks;
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
return masks[id];
|
||||
},
|
||||
),
|
||||
updateMask(id: string, updater: (mask: Mask) => void) {
|
||||
const masks = get().masks;
|
||||
const mask = masks[id];
|
||||
if (!mask) return;
|
||||
const updateMask = { ...mask };
|
||||
updater(updateMask);
|
||||
masks[id] = updateMask;
|
||||
set(() => ({ masks }));
|
||||
get().markUpdate();
|
||||
},
|
||||
delete(id: string) {
|
||||
const masks = get().masks;
|
||||
delete masks[id];
|
||||
set(() => ({ masks }));
|
||||
get().markUpdate();
|
||||
},
|
||||
|
||||
get(id?: string) {
|
||||
return get().masks[id ?? 1145141919810];
|
||||
},
|
||||
getAll() {
|
||||
const userMasks = Object.values(get().masks).sort(
|
||||
(a, b) => b.createdAt - a.createdAt,
|
||||
);
|
||||
const config = useAppConfig.getState();
|
||||
if (config.hideBuiltinMasks) return userMasks;
|
||||
const buildinMasks = BUILTIN_MASKS.map(
|
||||
(m) =>
|
||||
({
|
||||
...m,
|
||||
modelConfig: {
|
||||
...config.modelConfig,
|
||||
...m.modelConfig,
|
||||
},
|
||||
}) as Mask,
|
||||
);
|
||||
return userMasks.concat(buildinMasks);
|
||||
},
|
||||
search(text: string) {
|
||||
return Object.values(get().masks);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Mask,
|
||||
version: 3.1,
|
||||
|
||||
migrate(state, version) {
|
||||
const newState = JSON.parse(JSON.stringify(state)) as MaskState;
|
||||
|
||||
// migrate mask id to nanoid
|
||||
if (version < 3) {
|
||||
Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
|
||||
}
|
||||
|
||||
if (version < 3.1) {
|
||||
const updatedMasks: Record<string, Mask> = {};
|
||||
Object.values(newState.masks).forEach((m) => {
|
||||
updatedMasks[m.id] = m;
|
||||
});
|
||||
newState.masks = updatedMasks;
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import Fuse from "fuse.js";
|
||||
import { getLang } from "../locales";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export interface Prompt {
|
||||
id: string;
|
||||
@@ -13,19 +12,6 @@ export interface Prompt {
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface PromptStore {
|
||||
counter: number;
|
||||
prompts: Record<string, Prompt>;
|
||||
|
||||
add: (prompt: Prompt) => string;
|
||||
get: (id: string) => Prompt | undefined;
|
||||
remove: (id: string) => void;
|
||||
search: (text: string) => Prompt[];
|
||||
update: (id: string, updater: (prompt: Prompt) => void) => void;
|
||||
|
||||
getUserPrompts: () => Prompt[];
|
||||
}
|
||||
|
||||
export const SearchService = {
|
||||
ready: false,
|
||||
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
|
||||
@@ -62,130 +48,136 @@ export const SearchService = {
|
||||
},
|
||||
};
|
||||
|
||||
export const usePromptStore = create<PromptStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
counter: 0,
|
||||
latestId: 0,
|
||||
prompts: {},
|
||||
export const usePromptStore = createPersistStore(
|
||||
{
|
||||
counter: 0,
|
||||
prompts: {} as Record<string, Prompt>,
|
||||
},
|
||||
|
||||
add(prompt) {
|
||||
const prompts = get().prompts;
|
||||
prompt.id = nanoid();
|
||||
prompt.isUser = true;
|
||||
prompt.createdAt = Date.now();
|
||||
prompts[prompt.id] = prompt;
|
||||
(set, get) => ({
|
||||
add(prompt: Prompt) {
|
||||
const prompts = get().prompts;
|
||||
prompt.id = nanoid();
|
||||
prompt.isUser = true;
|
||||
prompt.createdAt = Date.now();
|
||||
prompts[prompt.id] = prompt;
|
||||
|
||||
set(() => ({
|
||||
latestId: prompt.id!,
|
||||
prompts: prompts,
|
||||
}));
|
||||
set(() => ({
|
||||
prompts: prompts,
|
||||
}));
|
||||
|
||||
return prompt.id!;
|
||||
},
|
||||
|
||||
get(id) {
|
||||
const targetPrompt = get().prompts[id];
|
||||
|
||||
if (!targetPrompt) {
|
||||
return SearchService.builtinPrompts.find((v) => v.id === id);
|
||||
}
|
||||
|
||||
return targetPrompt;
|
||||
},
|
||||
|
||||
remove(id) {
|
||||
const prompts = get().prompts;
|
||||
delete prompts[id];
|
||||
SearchService.remove(id);
|
||||
|
||||
set(() => ({
|
||||
prompts,
|
||||
counter: get().counter + 1,
|
||||
}));
|
||||
},
|
||||
|
||||
getUserPrompts() {
|
||||
const userPrompts = Object.values(get().prompts ?? {});
|
||||
userPrompts.sort((a, b) =>
|
||||
b.id && a.id ? b.createdAt - a.createdAt : 0,
|
||||
);
|
||||
return userPrompts;
|
||||
},
|
||||
|
||||
update(id, updater) {
|
||||
const prompt = get().prompts[id] ?? {
|
||||
title: "",
|
||||
content: "",
|
||||
id,
|
||||
};
|
||||
|
||||
SearchService.remove(id);
|
||||
updater(prompt);
|
||||
const prompts = get().prompts;
|
||||
prompts[id] = prompt;
|
||||
set(() => ({ prompts }));
|
||||
SearchService.add(prompt);
|
||||
},
|
||||
|
||||
search(text) {
|
||||
if (text.length === 0) {
|
||||
// return all rompts
|
||||
return get().getUserPrompts().concat(SearchService.builtinPrompts);
|
||||
}
|
||||
return SearchService.search(text) as Prompt[];
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Prompt,
|
||||
version: 3,
|
||||
|
||||
migrate(state, version) {
|
||||
const newState = JSON.parse(JSON.stringify(state)) as PromptStore;
|
||||
|
||||
if (version < 3) {
|
||||
Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
onRehydrateStorage(state) {
|
||||
const PROMPT_URL = "./prompts.json";
|
||||
|
||||
type PromptList = Array<[string, string]>;
|
||||
|
||||
fetch(PROMPT_URL)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
let fetchPrompts = [res.en, res.cn];
|
||||
if (getLang() === "cn") {
|
||||
fetchPrompts = fetchPrompts.reverse();
|
||||
}
|
||||
const builtinPrompts = fetchPrompts.map(
|
||||
(promptList: PromptList) => {
|
||||
return promptList.map(
|
||||
([title, content]) =>
|
||||
({
|
||||
id: nanoid(),
|
||||
title,
|
||||
content,
|
||||
createdAt: Date.now(),
|
||||
} as Prompt),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const userPrompts =
|
||||
usePromptStore.getState().getUserPrompts() ?? [];
|
||||
|
||||
const allPromptsForSearch = builtinPrompts
|
||||
.reduce((pre, cur) => pre.concat(cur), [])
|
||||
.filter((v) => !!v.title && !!v.content);
|
||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||
SearchService.init(allPromptsForSearch, userPrompts);
|
||||
});
|
||||
},
|
||||
return prompt.id!;
|
||||
},
|
||||
),
|
||||
|
||||
get(id: string) {
|
||||
const targetPrompt = get().prompts[id];
|
||||
|
||||
if (!targetPrompt) {
|
||||
return SearchService.builtinPrompts.find((v) => v.id === id);
|
||||
}
|
||||
|
||||
return targetPrompt;
|
||||
},
|
||||
|
||||
remove(id: string) {
|
||||
const prompts = get().prompts;
|
||||
delete prompts[id];
|
||||
|
||||
Object.entries(prompts).some(([key, prompt]) => {
|
||||
if (prompt.id === id) {
|
||||
delete prompts[key];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
SearchService.remove(id);
|
||||
|
||||
set(() => ({
|
||||
prompts,
|
||||
counter: get().counter + 1,
|
||||
}));
|
||||
},
|
||||
|
||||
getUserPrompts() {
|
||||
const userPrompts = Object.values(get().prompts ?? {});
|
||||
userPrompts.sort((a, b) =>
|
||||
b.id && a.id ? b.createdAt - a.createdAt : 0,
|
||||
);
|
||||
return userPrompts;
|
||||
},
|
||||
|
||||
updatePrompt(id: string, updater: (prompt: Prompt) => void) {
|
||||
const prompt = get().prompts[id] ?? {
|
||||
title: "",
|
||||
content: "",
|
||||
id,
|
||||
};
|
||||
|
||||
SearchService.remove(id);
|
||||
updater(prompt);
|
||||
const prompts = get().prompts;
|
||||
prompts[id] = prompt;
|
||||
set(() => ({ prompts }));
|
||||
SearchService.add(prompt);
|
||||
},
|
||||
|
||||
search(text: string) {
|
||||
if (text.length === 0) {
|
||||
// return all rompts
|
||||
return this.getUserPrompts().concat(SearchService.builtinPrompts);
|
||||
}
|
||||
return SearchService.search(text) as Prompt[];
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Prompt,
|
||||
version: 3,
|
||||
|
||||
migrate(state, version) {
|
||||
const newState = JSON.parse(JSON.stringify(state)) as {
|
||||
prompts: Record<string, Prompt>;
|
||||
};
|
||||
|
||||
if (version < 3) {
|
||||
Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
|
||||
onRehydrateStorage(state) {
|
||||
const PROMPT_URL = "./prompts.json";
|
||||
|
||||
type PromptList = Array<[string, string]>;
|
||||
|
||||
fetch(PROMPT_URL)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
let fetchPrompts = [res.en, res.cn];
|
||||
if (getLang() === "cn") {
|
||||
fetchPrompts = fetchPrompts.reverse();
|
||||
}
|
||||
const builtinPrompts = fetchPrompts.map((promptList: PromptList) => {
|
||||
return promptList.map(
|
||||
([title, content]) =>
|
||||
({
|
||||
id: nanoid(),
|
||||
title,
|
||||
content,
|
||||
createdAt: Date.now(),
|
||||
}) as Prompt,
|
||||
);
|
||||
});
|
||||
|
||||
const userPrompts = usePromptStore.getState().getUserPrompts() ?? [];
|
||||
|
||||
const allPromptsForSearch = builtinPrompts
|
||||
.reduce((pre, cur) => pre.concat(cur), [])
|
||||
.filter((v) => !!v.title && !!v.content);
|
||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||
SearchService.init(allPromptsForSearch, userPrompts);
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@@ -1,7 +1,19 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { Updater } from "../typing";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { StoreKey } from "../constant";
|
||||
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import {
|
||||
AppState,
|
||||
getLocalAppState,
|
||||
GetStoreState,
|
||||
mergeAppState,
|
||||
setLocalAppState,
|
||||
} from "../utils/sync";
|
||||
import { downloadAs, readFromFile } from "../utils";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||
import { corsPath } from "../utils/cors";
|
||||
|
||||
export interface WebDavConfig {
|
||||
server: string;
|
||||
@@ -9,79 +21,111 @@ export interface WebDavConfig {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SyncStore {
|
||||
webDavConfig: WebDavConfig;
|
||||
lastSyncTime: number;
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
|
||||
update: Updater<WebDavConfig>;
|
||||
check: () => Promise<boolean>;
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
|
||||
path: (path: string) => string;
|
||||
headers: () => { Authorization: string };
|
||||
}
|
||||
webdav: {
|
||||
endpoint: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
|
||||
const FILE = {
|
||||
root: "/chatgpt-next-web/",
|
||||
upstash: {
|
||||
endpoint: "",
|
||||
username: STORAGE_KEY,
|
||||
apiKey: "",
|
||||
},
|
||||
|
||||
lastSyncTime: 0,
|
||||
lastProvider: "",
|
||||
};
|
||||
|
||||
export const useSyncStore = create<SyncStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
webDavConfig: {
|
||||
server: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
|
||||
lastSyncTime: 0,
|
||||
|
||||
update(updater) {
|
||||
const config = { ...get().webDavConfig };
|
||||
updater(config);
|
||||
set({ webDavConfig: config });
|
||||
},
|
||||
|
||||
async check() {
|
||||
try {
|
||||
const res = await fetch(this.path(""), {
|
||||
method: "PROFIND",
|
||||
headers: this.headers(),
|
||||
});
|
||||
console.log(res);
|
||||
return res.status === 207;
|
||||
} catch (e) {
|
||||
console.error("[Sync] ", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
path(path: string) {
|
||||
let url = get().webDavConfig.server;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
},
|
||||
|
||||
headers() {
|
||||
const auth = btoa(
|
||||
[get().webDavConfig.username, get().webDavConfig.password].join(":"),
|
||||
);
|
||||
|
||||
return {
|
||||
Authorization: `Basic ${auth}`,
|
||||
};
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Sync,
|
||||
version: 1,
|
||||
export const useSyncStore = createPersistStore(
|
||||
DEFAULT_SYNC_STATE,
|
||||
(set, get) => ({
|
||||
coundSync() {
|
||||
const config = get()[get().provider];
|
||||
return Object.values(config).every((c) => c.toString().length > 0);
|
||||
},
|
||||
),
|
||||
|
||||
markSyncTime() {
|
||||
set({ lastSyncTime: Date.now(), lastProvider: get().provider });
|
||||
},
|
||||
|
||||
export() {
|
||||
const state = getLocalAppState();
|
||||
const datePart = isApp
|
||||
? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}`
|
||||
: new Date().toLocaleString();
|
||||
|
||||
const fileName = `Backup-${datePart}.json`;
|
||||
downloadAs(JSON.stringify(state), fileName);
|
||||
},
|
||||
|
||||
async import() {
|
||||
const rawContent = await readFromFile();
|
||||
|
||||
try {
|
||||
const remoteState = JSON.parse(rawContent) as AppState;
|
||||
const localState = getLocalAppState();
|
||||
mergeAppState(localState, remoteState);
|
||||
setLocalAppState(localState);
|
||||
location.reload();
|
||||
} catch (e) {
|
||||
console.error("[Import]", e);
|
||||
showToast(Locale.Settings.Sync.ImportFailed);
|
||||
}
|
||||
},
|
||||
|
||||
getClient() {
|
||||
const provider = get().provider;
|
||||
const client = createSyncClient(provider, get());
|
||||
return client;
|
||||
},
|
||||
|
||||
async sync() {
|
||||
const localState = getLocalAppState();
|
||||
const provider = get().provider;
|
||||
const config = get()[provider];
|
||||
const client = this.getClient();
|
||||
|
||||
try {
|
||||
const remoteState = JSON.parse(
|
||||
await client.get(config.username),
|
||||
) as AppState;
|
||||
mergeAppState(localState, remoteState);
|
||||
setLocalAppState(localState);
|
||||
} catch (e) {
|
||||
console.log("[Sync] failed to get remote state", e);
|
||||
}
|
||||
|
||||
await client.set(config.username, JSON.stringify(localState));
|
||||
|
||||
this.markSyncTime();
|
||||
},
|
||||
|
||||
async check() {
|
||||
const client = this.getClient();
|
||||
return await client.check();
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Sync,
|
||||
version: 1.1,
|
||||
|
||||
migrate(persistedState, version) {
|
||||
const newState = persistedState as typeof DEFAULT_SYNC_STATE;
|
||||
|
||||
if (version < 1.1) {
|
||||
newState.upstash.username = STORAGE_KEY;
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@@ -1,26 +1,19 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
|
||||
import { api } from "../client/api";
|
||||
import {
|
||||
FETCH_COMMIT_URL,
|
||||
FETCH_TAG_URL,
|
||||
ModelProvider,
|
||||
StoreKey,
|
||||
} from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
export interface UpdateStore {
|
||||
versionType: "date" | "tag";
|
||||
lastUpdate: number;
|
||||
version: string;
|
||||
remoteVersion: string;
|
||||
|
||||
used?: number;
|
||||
subscription?: number;
|
||||
lastUpdateUsage: number;
|
||||
|
||||
getLatestVersion: (force?: boolean) => Promise<void>;
|
||||
updateUsage: (force?: boolean) => Promise<void>;
|
||||
|
||||
formatVersion: (version: string) => string;
|
||||
}
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import ChatGptIcon from "../icons/chatgpt.png";
|
||||
import Locale from "../locales";
|
||||
import { use } from "react";
|
||||
import { useAppConfig } from ".";
|
||||
import { ClientApi } from "../client/api";
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
function formatVersionDate(t: string) {
|
||||
const d = new Date(+t);
|
||||
@@ -35,7 +28,9 @@ function formatVersionDate(t: string) {
|
||||
].join("");
|
||||
}
|
||||
|
||||
async function getVersion(type: "date" | "tag") {
|
||||
type VersionType = "date" | "tag";
|
||||
|
||||
async function getVersion(type: VersionType) {
|
||||
if (type === "date") {
|
||||
const data = (await (await fetch(FETCH_COMMIT_URL)).json()) as {
|
||||
commit: {
|
||||
@@ -55,75 +50,115 @@ async function getVersion(type: "date" | "tag") {
|
||||
}
|
||||
}
|
||||
|
||||
export const useUpdateStore = create<UpdateStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
versionType: "tag",
|
||||
lastUpdate: 0,
|
||||
version: "unknown",
|
||||
remoteVersion: "",
|
||||
export const useUpdateStore = createPersistStore(
|
||||
{
|
||||
versionType: "tag" as VersionType,
|
||||
lastUpdate: 0,
|
||||
version: "unknown",
|
||||
remoteVersion: "",
|
||||
used: 0,
|
||||
subscription: 0,
|
||||
|
||||
lastUpdateUsage: 0,
|
||||
|
||||
formatVersion(version: string) {
|
||||
if (get().versionType === "date") {
|
||||
version = formatVersionDate(version);
|
||||
}
|
||||
return version;
|
||||
},
|
||||
|
||||
async getLatestVersion(force = false) {
|
||||
const versionType = get().versionType;
|
||||
let version =
|
||||
versionType === "date"
|
||||
? getClientConfig()?.commitDate
|
||||
: getClientConfig()?.version;
|
||||
|
||||
set(() => ({ version }));
|
||||
|
||||
const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE;
|
||||
if (!force && !shouldCheck) return;
|
||||
|
||||
set(() => ({
|
||||
lastUpdate: Date.now(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const remoteId = await getVersion(versionType);
|
||||
set(() => ({
|
||||
remoteVersion: remoteId,
|
||||
}));
|
||||
console.log("[Got Upstream] ", remoteId);
|
||||
} catch (error) {
|
||||
console.error("[Fetch Upstream Commit Id]", error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateUsage(force = false) {
|
||||
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
|
||||
if (!overOneMinute && !force) return;
|
||||
|
||||
set(() => ({
|
||||
lastUpdateUsage: Date.now(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const usage = await api.llm.usage();
|
||||
|
||||
if (usage) {
|
||||
set(() => ({
|
||||
used: usage.used,
|
||||
subscription: usage.total,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error((e as Error).message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Update,
|
||||
version: 1,
|
||||
lastUpdateUsage: 0,
|
||||
},
|
||||
(set, get) => ({
|
||||
formatVersion(version: string) {
|
||||
if (get().versionType === "date") {
|
||||
version = formatVersionDate(version);
|
||||
}
|
||||
return version;
|
||||
},
|
||||
),
|
||||
|
||||
async getLatestVersion(force = false) {
|
||||
const versionType = get().versionType;
|
||||
let version =
|
||||
versionType === "date"
|
||||
? getClientConfig()?.commitDate
|
||||
: getClientConfig()?.version;
|
||||
|
||||
set(() => ({ version }));
|
||||
|
||||
const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE;
|
||||
if (!force && !shouldCheck) return;
|
||||
|
||||
set(() => ({
|
||||
lastUpdate: Date.now(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const remoteId = await getVersion(versionType);
|
||||
set(() => ({
|
||||
remoteVersion: remoteId,
|
||||
}));
|
||||
if (window.__TAURI__?.notification && isApp) {
|
||||
// Check if notification permission is granted
|
||||
await window.__TAURI__?.notification
|
||||
.isPermissionGranted()
|
||||
.then((granted) => {
|
||||
if (!granted) {
|
||||
return;
|
||||
} else {
|
||||
// Request permission to show notifications
|
||||
window.__TAURI__?.notification
|
||||
.requestPermission()
|
||||
.then((permission) => {
|
||||
if (permission === "granted") {
|
||||
if (version === remoteId) {
|
||||
// Show a notification using Tauri
|
||||
window.__TAURI__?.notification.sendNotification({
|
||||
title: "NextChat",
|
||||
body: `${Locale.Settings.Update.IsLatest}`,
|
||||
icon: `${ChatGptIcon.src}`,
|
||||
sound: "Default",
|
||||
});
|
||||
} else {
|
||||
const updateMessage =
|
||||
Locale.Settings.Update.FoundUpdate(`${remoteId}`);
|
||||
// Show a notification for the new version using Tauri
|
||||
window.__TAURI__?.notification.sendNotification({
|
||||
title: "NextChat",
|
||||
body: updateMessage,
|
||||
icon: `${ChatGptIcon.src}`,
|
||||
sound: "Default",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log("[Got Upstream] ", remoteId);
|
||||
} catch (error) {
|
||||
console.error("[Fetch Upstream Commit Id]", error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateUsage(force = false) {
|
||||
// only support openai for now
|
||||
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
|
||||
if (!overOneMinute && !force) return;
|
||||
|
||||
set(() => ({
|
||||
lastUpdateUsage: Date.now(),
|
||||
}));
|
||||
|
||||
try {
|
||||
const api = new ClientApi(ModelProvider.GPT);
|
||||
const usage = await api.llm.usage();
|
||||
|
||||
if (usage) {
|
||||
set(() => ({
|
||||
used: usage.used,
|
||||
subscription: usage.total,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error((e as Error).message);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Update,
|
||||
version: 1,
|
||||
},
|
||||
);
|
||||
|
@@ -105,6 +105,7 @@ body {
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
touch-action: pan-x pan-y;
|
||||
overflow: hidden;
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
background-color: var(--second);
|
||||
@@ -348,7 +349,7 @@ pre {
|
||||
justify-content: center;
|
||||
border: var(--border-in-light);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: 10px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.one-line {
|
||||
@@ -356,3 +357,7 @@ pre {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.copyable {
|
||||
user-select: text;
|
||||
}
|
||||
|
60
app/utils.ts
60
app/utils.ts
@@ -3,7 +3,10 @@ import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
|
||||
export function trimTopic(topic: string) {
|
||||
return topic.replace(/[,。!?”“"、,.!?]*$/, "");
|
||||
// Fix an issue where double quotes still show in the Indonesian language
|
||||
// This will remove the specified punctuation from the end of the string
|
||||
// and also trim quotes from both the start and end if they exist.
|
||||
return topic.replace(/^["“”]+|["“”]+$/g, "").replace(/[,。!?”“"、,.!?]*$/, "");
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string) {
|
||||
@@ -31,12 +34,41 @@ export async function copyToClipboard(text: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadAs(text: string, filename: string) {
|
||||
const element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
|
||||
);
|
||||
export async function downloadAs(text: string, filename: string) {
|
||||
if (window.__TAURI__) {
|
||||
const result = await window.__TAURI__.dialog.save({
|
||||
defaultPath: `${filename}`,
|
||||
filters: [
|
||||
{
|
||||
name: `${filename.split('.').pop()} files`,
|
||||
extensions: [`${filename.split('.').pop()}`],
|
||||
},
|
||||
{
|
||||
name: "All Files",
|
||||
extensions: ["*"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (result !== null) {
|
||||
try {
|
||||
await window.__TAURI__.fs.writeBinaryFile(
|
||||
result,
|
||||
new Uint8Array([...text].map((c) => c.charCodeAt(0)))
|
||||
);
|
||||
showToast(Locale.Download.Success);
|
||||
} catch (error) {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
} else {
|
||||
showToast(Locale.Download.Failed);
|
||||
}
|
||||
} else {
|
||||
const element = document.createElement("a");
|
||||
element.setAttribute(
|
||||
"href",
|
||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
|
||||
);
|
||||
element.setAttribute("download", filename);
|
||||
|
||||
element.style.display = "none";
|
||||
@@ -46,7 +78,7 @@ export function downloadAs(text: string, filename: string) {
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
}
|
||||
export function readFromFile() {
|
||||
return new Promise<string>((res, rej) => {
|
||||
const fileInput = document.createElement("input");
|
||||
@@ -173,3 +205,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
||||
export function getCSSVar(varName: string) {
|
||||
return getComputedStyle(document.body).getPropertyValue(varName).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects Macintosh
|
||||
*/
|
||||
export function isMacOS(): boolean {
|
||||
if (typeof window !== "undefined") {
|
||||
let userAgent = window.navigator.userAgent.toLocaleLowerCase();
|
||||
const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent)
|
||||
return !!macintosh
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
12
app/utils/clone.ts
Normal file
12
app/utils/clone.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function deepClone<T>(obj: T) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export function ensure<T extends object>(
|
||||
obj: T,
|
||||
keys: Array<[keyof T][number]>,
|
||||
) {
|
||||
return keys.every(
|
||||
(k) => obj[k] !== undefined && obj[k] !== null && obj[k] !== "",
|
||||
);
|
||||
}
|
33
app/utils/cloud/index.ts
Normal file
33
app/utils/cloud/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createWebDavClient } from "./webdav";
|
||||
import { createUpstashClient } from "./upstash";
|
||||
|
||||
export enum ProviderType {
|
||||
WebDAV = "webdav",
|
||||
UpStash = "upstash",
|
||||
}
|
||||
|
||||
export const SyncClients = {
|
||||
[ProviderType.UpStash]: createUpstashClient,
|
||||
[ProviderType.WebDAV]: createWebDavClient,
|
||||
} as const;
|
||||
|
||||
type SyncClientConfig = {
|
||||
[K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends (
|
||||
_: infer C,
|
||||
) => any
|
||||
? C
|
||||
: never;
|
||||
};
|
||||
|
||||
export type SyncClient = {
|
||||
get: (key: string) => Promise<string>;
|
||||
set: (key: string, value: string) => Promise<void>;
|
||||
check: () => Promise<boolean>;
|
||||
};
|
||||
|
||||
export function createSyncClient<T extends ProviderType>(
|
||||
provider: T,
|
||||
config: SyncClientConfig[T],
|
||||
): SyncClient {
|
||||
return SyncClients[provider](config as any) as any;
|
||||
}
|
101
app/utils/cloud/upstash.ts
Normal file
101
app/utils/cloud/upstash.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { STORAGE_KEY } from "@/app/constant";
|
||||
import { SyncStore } from "@/app/store/sync";
|
||||
import { corsFetch } from "../cors";
|
||||
import { chunks } from "../format";
|
||||
|
||||
export type UpstashConfig = SyncStore["upstash"];
|
||||
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
||||
|
||||
export function createUpstashClient(store: SyncStore) {
|
||||
const config = store.upstash;
|
||||
const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username;
|
||||
const chunkCountKey = `${storeKey}-chunk-count`;
|
||||
const chunkIndexKey = (i: number) => `${storeKey}-chunk-${i}`;
|
||||
|
||||
const proxyUrl =
|
||||
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
|
||||
|
||||
return {
|
||||
async check() {
|
||||
try {
|
||||
const res = await corsFetch(this.path(`get/${storeKey}`), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
console.log("[Upstash] check", res.status, res.statusText);
|
||||
return [200].includes(res.status);
|
||||
} catch (e) {
|
||||
console.error("[Upstash] failed to check", e);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async redisGet(key: string) {
|
||||
const res = await corsFetch(this.path(`get/${key}`), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[Upstash] get key = ", key, res.status, res.statusText);
|
||||
const resJson = (await res.json()) as { result: string };
|
||||
|
||||
return resJson.result;
|
||||
},
|
||||
|
||||
async redisSet(key: string, value: string) {
|
||||
const res = await corsFetch(this.path(`set/${key}`), {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: value,
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[Upstash] set key = ", key, res.status, res.statusText);
|
||||
},
|
||||
|
||||
async get() {
|
||||
const chunkCount = Number(await this.redisGet(chunkCountKey));
|
||||
if (!Number.isInteger(chunkCount)) return;
|
||||
|
||||
const chunks = await Promise.all(
|
||||
new Array(chunkCount)
|
||||
.fill(0)
|
||||
.map((_, i) => this.redisGet(chunkIndexKey(i))),
|
||||
);
|
||||
console.log("[Upstash] get full chunks", chunks);
|
||||
return chunks.join("");
|
||||
},
|
||||
|
||||
async set(_: string, value: string) {
|
||||
// upstash limit the max request size which is 1Mb for “Free” and “Pay as you go”
|
||||
// so we need to split the data to chunks
|
||||
let index = 0;
|
||||
for await (const chunk of chunks(value)) {
|
||||
await this.redisSet(chunkIndexKey(index), chunk);
|
||||
index += 1;
|
||||
}
|
||||
await this.redisSet(chunkCountKey, index.toString());
|
||||
},
|
||||
|
||||
headers() {
|
||||
return {
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
let url = config.endpoint;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
},
|
||||
};
|
||||
}
|
76
app/utils/cloud/webdav.ts
Normal file
76
app/utils/cloud/webdav.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { STORAGE_KEY } from "@/app/constant";
|
||||
import { SyncStore } from "@/app/store/sync";
|
||||
import { corsFetch } from "../cors";
|
||||
|
||||
export type WebDAVConfig = SyncStore["webdav"];
|
||||
export type WebDavClient = ReturnType<typeof createWebDavClient>;
|
||||
|
||||
export function createWebDavClient(store: SyncStore) {
|
||||
const folder = STORAGE_KEY;
|
||||
const fileName = `${folder}/backup.json`;
|
||||
const config = store.webdav;
|
||||
const proxyUrl =
|
||||
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
|
||||
|
||||
return {
|
||||
async check() {
|
||||
try {
|
||||
const res = await corsFetch(this.path(folder), {
|
||||
method: "MKCOL",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
console.log("[WebDav] check", res.status, res.statusText);
|
||||
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
|
||||
} catch (e) {
|
||||
console.error("[WebDav] failed to check", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
async get(key: string) {
|
||||
const res = await corsFetch(this.path(fileName), {
|
||||
method: "GET",
|
||||
headers: this.headers(),
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] get key = ", key, res.status, res.statusText);
|
||||
|
||||
return await res.text();
|
||||
},
|
||||
|
||||
async set(key: string, value: string) {
|
||||
const res = await corsFetch(this.path(fileName), {
|
||||
method: "PUT",
|
||||
headers: this.headers(),
|
||||
body: value,
|
||||
proxyUrl,
|
||||
});
|
||||
|
||||
console.log("[WebDav] set key = ", key, res.status, res.statusText);
|
||||
},
|
||||
|
||||
headers() {
|
||||
const auth = btoa(config.username + ":" + config.password);
|
||||
|
||||
return {
|
||||
authorization: `Basic ${auth}`,
|
||||
};
|
||||
},
|
||||
path(path: string) {
|
||||
let url = config.endpoint;
|
||||
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/";
|
||||
}
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
|
||||
return url + path;
|
||||
},
|
||||
};
|
||||
}
|
50
app/utils/cors.ts
Normal file
50
app/utils/cors.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ApiPath, DEFAULT_CORS_HOST } from "../constant";
|
||||
|
||||
export function corsPath(path: string) {
|
||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : "";
|
||||
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
|
||||
export function corsFetch(
|
||||
url: string,
|
||||
options: RequestInit & {
|
||||
proxyUrl?: string;
|
||||
},
|
||||
) {
|
||||
if (!url.startsWith("http")) {
|
||||
throw Error("[CORS Fetch] url must starts with http/https");
|
||||
}
|
||||
|
||||
let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors);
|
||||
if (!proxyUrl.endsWith("/")) {
|
||||
proxyUrl += "/";
|
||||
}
|
||||
|
||||
url = url.replace("://", "/");
|
||||
|
||||
const corsOptions = {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: options.method
|
||||
? {
|
||||
...options.headers,
|
||||
method: options.method,
|
||||
}
|
||||
: options.headers,
|
||||
};
|
||||
|
||||
const corsUrl = proxyUrl + url;
|
||||
console.info("[CORS] target = ", corsUrl);
|
||||
|
||||
return fetch(corsUrl, corsOptions);
|
||||
}
|
@@ -11,3 +11,18 @@ export function prettyObject(msg: any) {
|
||||
}
|
||||
return ["```json", msg, "```"].join("\n");
|
||||
}
|
||||
|
||||
export function* chunks(s: string, maxBytes = 1000 * 1000) {
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let buf = new TextEncoder().encode(s);
|
||||
while (buf.length) {
|
||||
let i = buf.lastIndexOf(32, maxBytes + 1);
|
||||
// If no space found, try forward search
|
||||
if (i < 0) i = buf.indexOf(32, maxBytes);
|
||||
// If there's no space at all, take all
|
||||
if (i < 0) i = buf.length;
|
||||
// This is a safe cut-off point; never half-way a multi-byte
|
||||
yield decoder.decode(buf.slice(0, i));
|
||||
buf = buf.slice(i + 1); // Skip space (if any)
|
||||
}
|
||||
}
|
||||
|
16
app/utils/hooks.ts
Normal file
16
app/utils/hooks.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useMemo } from "react";
|
||||
import { useAccessStore, useAppConfig } from "../store";
|
||||
import { collectModels } from "./model";
|
||||
|
||||
export function useAllModels() {
|
||||
const accessStore = useAccessStore();
|
||||
const configStore = useAppConfig();
|
||||
const models = useMemo(() => {
|
||||
return collectModels(
|
||||
configStore.models,
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
);
|
||||
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
||||
|
||||
return models;
|
||||
}
|
@@ -1,9 +1,13 @@
|
||||
export function merge(target: any, source: any) {
|
||||
Object.keys(source).forEach(function (key) {
|
||||
if (source[key] && typeof source[key] === "object") {
|
||||
if (
|
||||
source.hasOwnProperty(key) && // Check if the property is not inherited
|
||||
source[key] &&
|
||||
typeof source[key] === "object" || key === "__proto__" || key === "constructor"
|
||||
) {
|
||||
merge((target[key] = target[key] || {}), source[key]);
|
||||
return;
|
||||
}
|
||||
target[key] = source[key];
|
||||
});
|
||||
}
|
||||
}
|
61
app/utils/model.ts
Normal file
61
app/utils/model.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { LLMModel } from "../client/api";
|
||||
|
||||
export function collectModelTable(
|
||||
models: readonly LLMModel[],
|
||||
customModels: string,
|
||||
) {
|
||||
const modelTable: Record<
|
||||
string,
|
||||
{
|
||||
available: boolean;
|
||||
name: string;
|
||||
displayName: string;
|
||||
provider?: LLMModel["provider"]; // Marked as optional
|
||||
}
|
||||
> = {};
|
||||
|
||||
// default models
|
||||
models.forEach((m) => {
|
||||
modelTable[m.name] = {
|
||||
...m,
|
||||
displayName: m.name, // 'provider' is copied over if it exists
|
||||
};
|
||||
});
|
||||
|
||||
// server custom models
|
||||
customModels
|
||||
.split(",")
|
||||
.filter((v) => !!v && v.length > 0)
|
||||
.forEach((m) => {
|
||||
const available = !m.startsWith("-");
|
||||
const nameConfig =
|
||||
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
|
||||
const [name, displayName] = nameConfig.split("=");
|
||||
|
||||
// enable or disable all models
|
||||
if (name === "all") {
|
||||
Object.values(modelTable).forEach((model) => (model.available = available));
|
||||
} else {
|
||||
modelTable[name] = {
|
||||
name,
|
||||
displayName: displayName || name,
|
||||
available,
|
||||
provider: modelTable[name]?.provider, // Use optional chaining
|
||||
};
|
||||
}
|
||||
});
|
||||
return modelTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate full model table.
|
||||
*/
|
||||
export function collectModels(
|
||||
models: readonly LLMModel[],
|
||||
customModels: string,
|
||||
) {
|
||||
const modelTable = collectModelTable(models, customModels);
|
||||
const allModels = Object.values(modelTable);
|
||||
|
||||
return allModels;
|
||||
}
|
64
app/utils/store.ts
Normal file
64
app/utils/store.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { create } from "zustand";
|
||||
import { combine, persist } from "zustand/middleware";
|
||||
import { Updater } from "../typing";
|
||||
import { deepClone } from "./clone";
|
||||
|
||||
type SecondParam<T> = T extends (
|
||||
_f: infer _F,
|
||||
_s: infer S,
|
||||
...args: infer _U
|
||||
) => any
|
||||
? S
|
||||
: never;
|
||||
|
||||
type MakeUpdater<T> = {
|
||||
lastUpdateTime: number;
|
||||
|
||||
markUpdate: () => void;
|
||||
update: Updater<T>;
|
||||
};
|
||||
|
||||
type SetStoreState<T> = (
|
||||
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
|
||||
replace?: boolean | undefined,
|
||||
) => void;
|
||||
|
||||
export function createPersistStore<T extends object, M>(
|
||||
state: T,
|
||||
methods: (
|
||||
set: SetStoreState<T & MakeUpdater<T>>,
|
||||
get: () => T & MakeUpdater<T>,
|
||||
) => M,
|
||||
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
|
||||
) {
|
||||
return create(
|
||||
persist(
|
||||
combine(
|
||||
{
|
||||
...state,
|
||||
lastUpdateTime: 0,
|
||||
},
|
||||
(set, get) => {
|
||||
return {
|
||||
...methods(set, get as any),
|
||||
|
||||
markUpdate() {
|
||||
set({ lastUpdateTime: Date.now() } as Partial<
|
||||
T & M & MakeUpdater<T>
|
||||
>);
|
||||
},
|
||||
update(updater) {
|
||||
const state = deepClone(get());
|
||||
updater(state);
|
||||
set({
|
||||
...state,
|
||||
lastUpdateTime: Date.now(),
|
||||
});
|
||||
},
|
||||
} as M & MakeUpdater<T>;
|
||||
},
|
||||
),
|
||||
persistOptions as any,
|
||||
),
|
||||
);
|
||||
}
|
165
app/utils/sync.ts
Normal file
165
app/utils/sync.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
ChatSession,
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
} from "../store";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { usePromptStore } from "../store/prompt";
|
||||
import { StoreKey } from "../constant";
|
||||
import { merge } from "./merge";
|
||||
|
||||
type NonFunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
||||
}[keyof T];
|
||||
type NonFunctionFields<T> = Pick<T, NonFunctionKeys<T>>;
|
||||
|
||||
export function getNonFunctionFileds<T extends object>(obj: T) {
|
||||
const ret: any = {};
|
||||
|
||||
Object.entries(obj).map(([k, v]) => {
|
||||
if (typeof v !== "function") {
|
||||
ret[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
return ret as NonFunctionFields<T>;
|
||||
}
|
||||
|
||||
export type GetStoreState<T> = T extends { getState: () => infer U }
|
||||
? NonFunctionFields<U>
|
||||
: never;
|
||||
|
||||
const LocalStateSetters = {
|
||||
[StoreKey.Chat]: useChatStore.setState,
|
||||
[StoreKey.Access]: useAccessStore.setState,
|
||||
[StoreKey.Config]: useAppConfig.setState,
|
||||
[StoreKey.Mask]: useMaskStore.setState,
|
||||
[StoreKey.Prompt]: usePromptStore.setState,
|
||||
} as const;
|
||||
|
||||
const LocalStateGetters = {
|
||||
[StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()),
|
||||
[StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()),
|
||||
[StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()),
|
||||
[StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()),
|
||||
[StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()),
|
||||
} as const;
|
||||
|
||||
export type AppState = {
|
||||
[k in keyof typeof LocalStateGetters]: ReturnType<
|
||||
(typeof LocalStateGetters)[k]
|
||||
>;
|
||||
};
|
||||
|
||||
type Merger<T extends keyof AppState, U = AppState[T]> = (
|
||||
localState: U,
|
||||
remoteState: U,
|
||||
) => U;
|
||||
|
||||
type StateMerger = {
|
||||
[K in keyof AppState]: Merger<K>;
|
||||
};
|
||||
|
||||
// we merge remote state to local state
|
||||
const MergeStates: StateMerger = {
|
||||
[StoreKey.Chat]: (localState, remoteState) => {
|
||||
// merge sessions
|
||||
const localSessions: Record<string, ChatSession> = {};
|
||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||
|
||||
remoteState.sessions.forEach((remoteSession) => {
|
||||
// skip empty chats
|
||||
if (remoteSession.messages.length === 0) return;
|
||||
|
||||
const localSession = localSessions[remoteSession.id];
|
||||
if (!localSession) {
|
||||
// if remote session is new, just merge it
|
||||
localState.sessions.push(remoteSession);
|
||||
} else {
|
||||
// if both have the same session id, merge the messages
|
||||
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||
remoteSession.messages.forEach((m) => {
|
||||
if (!localMessageIds.has(m.id)) {
|
||||
localSession.messages.push(m);
|
||||
}
|
||||
});
|
||||
|
||||
// sort local messages with date field in asc order
|
||||
localSession.messages.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// sort local sessions with date field in desc order
|
||||
localState.sessions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||
);
|
||||
|
||||
return localState;
|
||||
},
|
||||
[StoreKey.Prompt]: (localState, remoteState) => {
|
||||
localState.prompts = {
|
||||
...remoteState.prompts,
|
||||
...localState.prompts,
|
||||
};
|
||||
return localState;
|
||||
},
|
||||
[StoreKey.Mask]: (localState, remoteState) => {
|
||||
localState.masks = {
|
||||
...remoteState.masks,
|
||||
...localState.masks,
|
||||
};
|
||||
return localState;
|
||||
},
|
||||
[StoreKey.Config]: mergeWithUpdate<AppState[StoreKey.Config]>,
|
||||
[StoreKey.Access]: mergeWithUpdate<AppState[StoreKey.Access]>,
|
||||
};
|
||||
|
||||
export function getLocalAppState() {
|
||||
const appState = Object.fromEntries(
|
||||
Object.entries(LocalStateGetters).map(([key, getter]) => {
|
||||
return [key, getter()];
|
||||
}),
|
||||
) as AppState;
|
||||
|
||||
return appState;
|
||||
}
|
||||
|
||||
export function setLocalAppState(appState: AppState) {
|
||||
Object.entries(LocalStateSetters).forEach(([key, setter]) => {
|
||||
setter(appState[key as keyof AppState]);
|
||||
});
|
||||
}
|
||||
|
||||
export function mergeAppState(localState: AppState, remoteState: AppState) {
|
||||
Object.keys(localState).forEach(<T extends keyof AppState>(k: string) => {
|
||||
const key = k as T;
|
||||
const localStoreState = localState[key];
|
||||
const remoteStoreState = remoteState[key];
|
||||
MergeStates[key](localStoreState, remoteStoreState);
|
||||
});
|
||||
|
||||
return localState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge state with `lastUpdateTime`, older state will be override
|
||||
*/
|
||||
export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
||||
localState: T,
|
||||
remoteState: T,
|
||||
) {
|
||||
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
|
||||
|
||||
if (localUpdateTime < remoteUpdateTime) {
|
||||
merge(remoteState, localState);
|
||||
return { ...remoteState };
|
||||
} else {
|
||||
merge(localState, remoteState);
|
||||
return { ...localState };
|
||||
}
|
||||
}
|
@@ -1,30 +1,38 @@
|
||||
version: '3.9'
|
||||
version: "3.9"
|
||||
services:
|
||||
chatgpt-next-web:
|
||||
profiles: ["no-proxy"]
|
||||
chatgpt-next-web:
|
||||
profiles: [ "no-proxy" ]
|
||||
container_name: chatgpt-next-web
|
||||
image: yidadaa/chatgpt-next-web
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- OPENAI_API_KEY=$OPENAI_API_KEY
|
||||
- GOOGLE_API_KEY=$GOOGLE_API_KEY
|
||||
- CODE=$CODE
|
||||
- BASE_URL=$BASE_URL
|
||||
- OPENAI_ORG_ID=$OPENAI_ORG_ID
|
||||
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
|
||||
- DISABLE_GPT4=$DISABLE_GPT4
|
||||
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
|
||||
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
|
||||
- OPENAI_SB=$OPENAI_SB
|
||||
|
||||
chatgpt-next-web-proxy:
|
||||
profiles: ["proxy"]
|
||||
chatgpt-next-web-proxy:
|
||||
profiles: [ "proxy" ]
|
||||
container_name: chatgpt-next-web-proxy
|
||||
image: yidadaa/chatgpt-next-web
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- OPENAI_API_KEY=$OPENAI_API_KEY
|
||||
- GOOGLE_API_KEY=$GOOGLE_API_KEY
|
||||
- CODE=$CODE
|
||||
- PROXY_URL=$PROXY_URL
|
||||
- BASE_URL=$BASE_URL
|
||||
- OPENAI_ORG_ID=$OPENAI_ORG_ID
|
||||
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY
|
||||
- DISABLE_GPT4=$DISABLE_GPT4
|
||||
- ENABLE_BALANCE_QUERY=$ENABLE_BALANCE_QUERY
|
||||
- DISABLE_FAST_LINK=$DISABLE_FAST_LINK
|
||||
- OPENAI_SB=$OPENAI_SB
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# Cloudflare Pages 部署指南
|
||||
|
||||
## 如何新建项目
|
||||
|
||||
在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。
|
||||
|
||||
1. 点击 "Create a project"。
|
||||
@@ -12,7 +13,7 @@
|
||||
7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。
|
||||
8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令:
|
||||
```
|
||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
||||
npx @cloudflare/next-on-pages@1.5.0
|
||||
```
|
||||
9. 对于 "Build output directory",使用默认值并且不要修改。
|
||||
10. 不要修改 "Root Directory"。
|
||||
@@ -30,10 +31,12 @@
|
||||
- `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
|
||||
- `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
|
||||
- `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
|
||||
|
||||
- `ENABLE_BALANCE_QUERY=1 可选,启用余额查询功能`
|
||||
- `DISABLE_FAST_LINK=1 可选,禁用从链接解析预制设置`
|
||||
|
||||
12. 点击 "Save and Deploy"。
|
||||
13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。
|
||||
14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。
|
||||
15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。
|
||||
16. 前往 "Deployments",点击 "Retry deployment"。
|
||||
17. Enjoy.
|
||||
17. Enjoy.
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# Cloudflare Pages Deployment Guide
|
||||
|
||||
## How to create a new project
|
||||
|
||||
Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||
|
||||
1. Click "Create a project".
|
||||
@@ -11,12 +12,13 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||
6. For "Project name" and "Production branch", use the default values or change them as needed.
|
||||
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
|
||||
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
|
||||
```
|
||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
||||
```
|
||||
```
|
||||
npx @cloudflare/next-on-pages --experimental-minify
|
||||
```
|
||||
9. For "Build output directory", use the default value and do not modify it.
|
||||
10. Do not modify "Root Directory".
|
||||
11. For "Environment variables", click ">" and then "Add variable". Fill in the following information:
|
||||
|
||||
- `NODE_VERSION=20.1`
|
||||
- `NEXT_TELEMETRY_DISABLE=1`
|
||||
- `OPENAI_API_KEY=your_own_API_key`
|
||||
@@ -29,10 +31,13 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||
- `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI`
|
||||
- `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key`
|
||||
- `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4`
|
||||
|
||||
- `ENABLE_BALANCE_QUERY=1 Optional, allow users to query balance`
|
||||
- `DISABLE_FAST_LINK=1 Optional, disable parse settings from url`
|
||||
- `OPENAI_SB=1 Optional,use the third-party OpenAI-SB API`
|
||||
|
||||
12. Click "Save and Deploy".
|
||||
13. Click "Cancel deployment" because you need to fill in Compatibility flags.
|
||||
14. Go to "Build settings", "Functions", and find "Compatibility flags".
|
||||
15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag".
|
||||
16. Go to "Deployments" and click "Retry deployment".
|
||||
17. Enjoy.
|
||||
17. Enjoy.
|
||||
|
38
docs/cloudflare-pages-ja.md
Normal file
38
docs/cloudflare-pages-ja.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Cloudflare Pages 導入ガイド
|
||||
|
||||
## 新規プロジェクトの作成方法
|
||||
GitHub でこのプロジェクトをフォークし、dash.cloudflare.com にログインして Pages にアクセスします。
|
||||
|
||||
1. "Create a project" をクリックする。
|
||||
2. "Connect to Git" を選択する。
|
||||
3. Cloudflare Pages を GitHub アカウントに接続します。
|
||||
4. フォークしたプロジェクトを選択します。
|
||||
5. "Begin setup" をクリックする。
|
||||
6. "Project name" と "Production branch" はデフォルト値を使用するか、必要に応じて変更してください。
|
||||
7. "Build Settings" で、"Framework presets" オプションを選択し、"Next.js" を選択します。
|
||||
8. node:buffer のバグのため、デフォルトの "Build command" は使用しないでください。代わりに、以下のコマンドを使用してください:
|
||||
```
|
||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
||||
```
|
||||
9. "Build output directory" はデフォルト値を使用し、変更しない。
|
||||
10. "Root Directory" を変更しない。
|
||||
11. "Environment variables" は、">" をクリックし、"Add variable" をクリックします。そして以下の情報を入力します:
|
||||
- `NODE_VERSION=20.1`
|
||||
- `NEXT_TELEMETRY_DISABLE=1`
|
||||
- `OPENAI_API_KEY=your_own_API_key`
|
||||
- `YARN_VERSION=1.22.19`
|
||||
- `PHP_VERSION=7.4`
|
||||
|
||||
必要に応じて、以下の項目を入力してください:
|
||||
|
||||
- `CODE= Optional, access passwords, multiple passwords can be separated by commas`
|
||||
- `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI`
|
||||
- `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key`
|
||||
- `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4`
|
||||
|
||||
12. "Save and Deploy" をクリックする。
|
||||
13. 互換性フラグを記入する必要があるため、"Cancel deployment" をクリックする。
|
||||
14. "Build settings" の "Functions" から "Compatibility flags" を見つける。
|
||||
15. "Configure Production compatibility flag" と "Configure Preview compatibility flag" の両方に "nodejs_compat "を記入する。
|
||||
16. "Deployments" に移動し、"Retry deployment" をクリックします。
|
||||
17. お楽しみください。
|
39
docs/cloudflare-pages-ko.md
Normal file
39
docs/cloudflare-pages-ko.md
Normal file
@@ -0,0 +1,39 @@
|
||||
## Cloudflare 페이지 배포 가이드
|
||||
|
||||
## 새 프로젝트를 만드는 방법
|
||||
이 프로젝트를 Github에서 포크한 다음 dash.cloudflare.com에 로그인하고 페이지로 이동합니다.
|
||||
|
||||
1. "프로젝트 만들기"를 클릭합니다.
|
||||
2. "Git에 연결"을 선택합니다.
|
||||
3. Cloudflare 페이지를 GitHub 계정과 연결합니다.
|
||||
4. 포크한 프로젝트를 선택합니다.
|
||||
5. "설정 시작"을 클릭합니다.
|
||||
6. "프로젝트 이름" 및 "프로덕션 브랜치"의 기본값을 사용하거나 필요에 따라 변경합니다.
|
||||
7. "빌드 설정"에서 "프레임워크 프리셋" 옵션을 선택하고 "Next.js"를 선택합니다.
|
||||
8. node:buffer 버그로 인해 지금은 기본 "빌드 명령어"를 사용하지 마세요. 다음 명령을 사용하세요:
|
||||
``
|
||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental- minify
|
||||
```
|
||||
9. "빌드 출력 디렉토리"의 경우 기본값을 사용하고 수정하지 마십시오.
|
||||
10. "루트 디렉토리"는 수정하지 마십시오.
|
||||
11. "환경 변수"의 경우 ">"를 클릭한 다음 "변수 추가"를 클릭합니다. 다음에 따라 정보를 입력합니다:
|
||||
|
||||
- node_version=20.1`.
|
||||
- next_telemetry_disable=1`.
|
||||
- `OPENAI_API_KEY=자신의 API 키`
|
||||
- ``yarn_version=1.22.19``
|
||||
- ``php_version=7.4``.
|
||||
|
||||
실제 필요에 따라 다음 옵션을 선택적으로 입력합니다:
|
||||
|
||||
- `CODE= 선택적으로 액세스 비밀번호를 입력하며 쉼표를 사용하여 여러 비밀번호를 구분할 수 있습니다`.
|
||||
- `OPENAI_ORG_ID= 선택 사항, OpenAI에서 조직 ID 지정`
|
||||
- `HIDE_USER_API_KEY=1 선택 사항, 사용자가 API 키를 입력하지 못하도록 합니다.
|
||||
- `DISABLE_GPT4=1 옵션, 사용자가 GPT-4를 사용하지 못하도록 설정` 12.
|
||||
|
||||
12. "저장 후 배포"를 클릭합니다.
|
||||
13. 호환성 플래그를 입력해야 하므로 "배포 취소"를 클릭합니다.
|
||||
14. "빌드 설정", "기능"으로 이동하여 "호환성 플래그"를 찾습니다.
|
||||
"프로덕션 호환성 플래그 구성" 및 "프리뷰 호환성 플래그 구성"에서 "nodejs_compat"를 입력합니다.
|
||||
16. "배포"로 이동하여 "배포 다시 시도"를 클릭합니다.
|
||||
17. 즐기세요!
|
@@ -23,7 +23,7 @@ Docker 版本相当于稳定版,latest Docker 总是与 latest release version
|
||||
## 如何修改 Vercel 环境变量
|
||||
|
||||
- 进入 vercel 的控制台页面;
|
||||
- 选中你的 chatgpt next web 项目;
|
||||
- 选中你的 NextChat 项目;
|
||||
- 点击页面头部的 Settings 选项;
|
||||
- 找到侧边栏的 Environment Variables 选项;
|
||||
- 修改对应的值即可。
|
||||
@@ -101,7 +101,7 @@ keepalive_timeout 300; # 设定keep-alive超时时间为65秒
|
||||
|
||||
项目通过环境变量 CODE 设置了访问密码。第一次使用时,需要到设置中,输入访问码才可以使用。
|
||||
|
||||
## 使用时提示"You exceeded your current quota, ..."
|
||||
## 使用时提示 "You exceeded your current quota, ..."
|
||||
|
||||
API KEY 有问题。余额不足。
|
||||
|
||||
@@ -122,6 +122,9 @@ API KEY 有问题。余额不足。
|
||||
|
||||
注意,关闭此特性后,用户会在第一次访问网站时加载所有资源,如果用户网络状况较差,可能会引起较长时间的白屏,从而影响用户使用体验,所以自行考虑。
|
||||
|
||||
## 使用时遇到 "NotFoundError: Failed to execute 'removeChild' on 'Node': The node...."
|
||||
请关闭浏览器自身的自动翻译功能,并关闭所有自动翻译插件。
|
||||
|
||||
# 网络服务相关问题
|
||||
|
||||
## Cloudflare 是什么?
|
||||
|
191
docs/faq-ja.md
Normal file
191
docs/faq-ja.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# よくある質問
|
||||
|
||||
## 早く助けを求めるには?
|
||||
|
||||
1. ChatGPT / Bing / Baidu / Google などに尋ねてください。
|
||||
2. オンラインの友達に聞く。背景情報と問題の詳細な説明を提供してください。質の高い質問ほど、有益な回答を得られる可能性が高くなります。
|
||||
|
||||
# デプロイメントに関する質問
|
||||
|
||||
## なぜ Docker のデプロイバージョンは常に更新を要求するのか
|
||||
|
||||
Docker のバージョンは安定版と同等であり、最新の Docker は常に最新のリリースバージョンと一致しています。現在、私たちのリリース頻度は1~2日に1回なので、Dockerのバージョンは常に最新のコミットから1~2日遅れており、これは予想されることです。
|
||||
|
||||
## Vercel での展開方法
|
||||
|
||||
1. GitHub アカウントを登録し、このプロジェクトをフォークする。
|
||||
2. Vercel を登録し(携帯電話認証が必要、中国の番号でも可)、GitHub アカウントを接続する。
|
||||
3. Vercel で新規プロジェクトを作成し、GitHub でフォークしたプロジェクトを選択し、必要な環境変数を入力し、デプロイを開始する。デプロイ後、Vercel が提供するドメインからプロジェクトにアクセスできます。(中国本土ではプロキシが必要)
|
||||
|
||||
- 中国で直接アクセスする必要がある場合: DNS プロバイダーで、cname.vercel-dns.com を指すドメイン名の CNAME レコードを追加します。その後、Vercel でドメインアクセスを設定してください。
|
||||
|
||||
## Vercel 環境変数の変更方法
|
||||
|
||||
- Vercel のコンソールページに入ります;
|
||||
- chatgpt-next-web プロジェクトを選択してください;
|
||||
- ページ上部の設定オプションをクリックしてください;
|
||||
- サイドバーで環境変数オプションを見つけます;
|
||||
- 必要に応じて対応する値を変更してください。
|
||||
|
||||
## 環境変数 CODE とは何ですか?設定する必要がありますか?
|
||||
|
||||
カスタムアクセスパスワードです:
|
||||
|
||||
1. 設定しないで、環境変数を削除する。この時、誰でもあなたのプロジェクトにアクセスすることができます。
|
||||
2. プロジェクトをデプロイするときに、環境変数 CODE を設定する(カンマ区切りで複数のパスワードをサポート)。アクセスパスワードを設定した後、ユーザーはそれを使用するために設定ページでアクセスパスワードを入力する必要があります。[関連手順](https://github.com/Yidadaa/ChatGPT-Next-Web#access-password)
|
||||
|
||||
## なぜ私がデプロイしたバージョンにはストリーミングレスポンスがないのでしょうか?
|
||||
|
||||
> 関連する議論: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
|
||||
|
||||
nginx のリバースプロキシを使っている場合、設定ファイルに以下のコードを追加する必要があります:
|
||||
|
||||
```
|
||||
# キャッシュなし、ストリーミング出力をサポート
|
||||
proxy_cache off; # キャッシュをオフにする
|
||||
proxy_buffering off; # プロキシバッファリングをオフにする
|
||||
chunked_transfer_encoding on; # チャンク転送エンコーディングをオンにする
|
||||
tcp_nopush on; # TCP NOPUSH オプションをオンにし、Nagleアルゴリズムを無効にする
|
||||
tcp_nodelay on; # TCP NODELAY オプションをオンにし、遅延ACKアルゴリズムを無効にする
|
||||
keepalive_timeout 300; # keep-alive のタイムアウトを 65 秒に設定する
|
||||
```
|
||||
|
||||
netlify でデプロイしている場合、この問題はまだ解決待ちです。
|
||||
|
||||
## デプロイしましたが、アクセスできません。
|
||||
|
||||
以下の問題を確認し、トラブルシューティングを行ってください:
|
||||
|
||||
- サービスは開始されていますか?
|
||||
- ポートは正しくマッピングされていますか?
|
||||
- ファイアウォールのポートは開いていますか?
|
||||
- サーバーへのルートは問題ありませんか?
|
||||
- ドメイン名は正しく解決されていますか?
|
||||
|
||||
## "Error: Loading CSS chunk xxx failed..." と表示されることがあります。
|
||||
|
||||
Next.js では、最初のホワイトスクリーンの時間を短縮するために、デフォルトでチャンキングを有効にしています。技術的な詳細はこちらをご覧ください:
|
||||
|
||||
- https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
|
||||
- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4
|
||||
- https://github.com/vercel/next.js/issues/38507
|
||||
- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4
|
||||
|
||||
ただし、Next.js は古いブラウザとの互換性に制限があるため、このエラーが発生することがあります。
|
||||
|
||||
ビルド時にチャンキングを無効にすることができます。
|
||||
|
||||
Vercel プラットフォームの場合は、環境変数に `DISABLE_CHUNK=1` を追加して再デプロイします。
|
||||
セルフデプロイのプロジェクトでは、ビルド時に `DISABLE_CHUNK=1 yarn build` を使用することができます。
|
||||
Docker ユーザーの場合、ビルドはパッケージング時にすでに完了しているため、この機能を無効にすることは現在サポートされていません。
|
||||
|
||||
この機能を無効にすると、ユーザーの最初の訪問時にすべてのリソースがロードされることに注意してください。その結果、ユーザーのネットワーク接続が悪い場合、ホワイト・スクリーンの時間が長くなり、ユーザーエクスペリエンスに影響を与える可能性があります。この点を考慮の上、ご判断ください。
|
||||
|
||||
# 使用法に関する質問
|
||||
|
||||
## なぜいつも "An error occurred, please try again later" と表示されるのですか?
|
||||
|
||||
様々な原因が考えられますので、以下の項目を順番にチェックしてみてください:
|
||||
|
||||
- まず、コードのバージョンが最新版かどうかを確認し、最新版にアップデートしてから再試行してください;
|
||||
- api キーが正しく設定されているか確認してください。環境変数名は大文字とアンダースコアでなければなりません;
|
||||
- api キーが使用可能かどうか確認する;
|
||||
- 上記のステップを踏んでも問題が解決しない場合は、issue エリアに新しい issue を投稿し、vercel のランタイムログまたは docker のランタイムログを添付してください。
|
||||
|
||||
## ChatGPT の返信が文字化けするのはなぜですか?
|
||||
|
||||
設定画面-機種設定の中に `temperature` という項目があります。この値が 1 より大きい場合、返信が文字化けすることがあります。1 以内に調整してください。
|
||||
|
||||
## 設定ページでアクセスパスワードを入力してください」と表示される。
|
||||
|
||||
プロジェクトでは環境変数 CODE でアクセスパスワードを設定しています。初めて使うときは、設定ページでアクセスコードを入力する必要があります。
|
||||
|
||||
## 使用すると、"You exceeded your current quota, ..." と表示される。
|
||||
|
||||
API KEY に問題があります。残高不足です。
|
||||
|
||||
## プロキシとは何ですか?
|
||||
|
||||
OpenAI の IP 制限により、中国をはじめとする一部の国や地域では、OpenAI API に直接接続することができず、プロキシを経由する必要があります。プロキシサーバ(フォワードプロキシ)を利用するか、事前に設定された OpenAI API リバースプロキシを利用します。
|
||||
|
||||
- フォワードプロキシの例: VPN ラダー。docker デプロイの場合は、環境変数 HTTP_PROXY にプロキシアドレス (http://address:port) を設定します。
|
||||
- リバースプロキシの例: 他人のプロキシアドレスを使うか、Cloudflare を通じて無料で設定できる。プロジェクトの環境変数 BASE_URL にプロキシアドレスを設定してください。
|
||||
|
||||
## 中国のサーバーにデプロイできますか?
|
||||
|
||||
可能ですが、対処すべき問題があります:
|
||||
|
||||
- GitHub や OpenAI などのウェブサイトに接続するにはプロキシが必要です;
|
||||
- GitHub や OpenAI のようなウェブサイトに接続するにはプロキシが必要です;
|
||||
- 中国の政策により、海外のウェブサイト/ChatGPT 関連アプリケーションへのプロキシアクセスが制限されており、ブロックされる可能性があります。
|
||||
|
||||
# ネットワークサービス関連の質問
|
||||
|
||||
## クラウドフレアとは何ですか?
|
||||
|
||||
Cloudflare(CF)は、CDN、ドメイン管理、静的ページホスティング、エッジコンピューティング機能展開などを提供するネットワークサービスプロバイダーです。一般的な使用例: メインの購入やホスティング(解決、ダイナミックドメインなど)、サーバーへの CDN の適用(ブロックされないように IP を隠すことができる)、ウェブサイト(CF Pages)の展開。CF はほとんどのサービスを無料で提供しています。
|
||||
|
||||
## Vercel とは?
|
||||
|
||||
Vercel はグローバルなクラウドプラットフォームで、開発者がモダンなウェブアプリケーションをより迅速に構築、デプロイできるように設計されています。このプロジェクトや多くのウェブアプリケーションは、ワンクリックで Vercel 上に無料でデプロイできます。コードを理解する必要も、Linux を理解する必要も、サーバーを持つ必要も、お金を払う必要も、OpenAI API プロキシを設定する必要もありません。欠点は、中国の制限なしにアクセスするためにドメイン名をバインドする必要があることだ。
|
||||
|
||||
## ドメイン名の取得方法
|
||||
|
||||
1. Namesilo(アリペイ対応)や Cloudflare(海外プロバイダー)、Wanwang(中国国内プロバイダー)などのドメインプロバイダーに登録する。
|
||||
2. 無料ドメインプロバイダー: eu.org(セカンドレベルドメイン)など。
|
||||
3. 無料セカンドレベルドメインを友人に頼む。
|
||||
|
||||
## サーバーの取得方法
|
||||
|
||||
- 海外サーバープロバイダーの例 Amazon Web Services、Google Cloud、Vultr、Bandwagon、Hostdare など。
|
||||
海外サーバーの注意点 サーバー回線は中国でのアクセス速度に影響するため、CN2 GIA、CN2 回線を推奨。もしサーバーが中国でアクセスしにくい場合(深刻なパケットロスなど)、CDN(Cloudflare のようなプロバイダーのもの)を使ってみるとよいでしょう。
|
||||
- 国内のサーバープロバイダー アリババクラウド、テンセントなど
|
||||
国内サーバーの注意点 ドメイン名の解決にはファイリングが必要。国内サーバーの帯域幅は比較的高い。海外のウェブサイト(GitHub、OpenAI など)へのアクセスにはプロキシが必要。
|
||||
|
||||
# OpenAI 関連の質問
|
||||
|
||||
## OpenAI のアカウントを登録するには?
|
||||
|
||||
chat.openai.com にアクセスして登録してください。以下のものが必要です:
|
||||
|
||||
- 優れた VPN (OpenAI はサポートされている地域のネイティブ IP アドレスしか許可しません)
|
||||
- サポートされているメール (例: Gmail や会社/学校のメール。Outlook や QQ のメールは不可)
|
||||
- SMS 認証を受ける方法(SMS-activate ウェブサイトなど)
|
||||
|
||||
## OpenAI API を有効にするには?API 残高の確認方法は?
|
||||
|
||||
公式ウェブサイト(VPN が必要): https://platform.openai.com/account/usage
|
||||
VPN なしで残高を確認するためにプロキシを設定しているユーザーもいます。API キーの漏洩を避けるため、信頼できる情報源であることを確認してください。
|
||||
|
||||
## OpenAI の新規アカウントに API 残高がないのはなぜですか?
|
||||
|
||||
(4月6日更新) 新規登録アカウントは通常 24 時間以内に API 残高が表示されます。現在、新規アカウントには 5 ドルの残高が与えられています。
|
||||
|
||||
## OpenAI API へのチャージ方法を教えてください。
|
||||
|
||||
OpenAI では、指定された地域のクレジットカードのみご利用いただけます(中国のクレジットカードはご利用いただけません)。お住まいの地域のクレジットカードに対応していない場合は、以下の方法があります:
|
||||
|
||||
1. Depay バーチャルクレジットカード
|
||||
2. 海外のクレジットカードを申し込む
|
||||
3. オンラインでトップアップしてくれる人を探す
|
||||
|
||||
## GPT-4 API にアクセスするには?
|
||||
|
||||
(4月6日更新) GPT-4 API へのアクセスには別途申請が必要です。以下のアドレスにアクセスし、ウェイティングリストに参加するための情報を入力してください(OpenAI の組織 ID をご用意ください): https://openai.com/waitlist/gpt-4-api
|
||||
その後、メールの更新をお待ちください。
|
||||
|
||||
## Azure OpenAI インターフェースの使い方
|
||||
|
||||
次を参照: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
|
||||
|
||||
## トークンの消費が速いのはなぜですか?
|
||||
|
||||
> 関連する議論: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
|
||||
|
||||
- GPT-4 にアクセスし、GPT-4 の API を定期的に使用している場合、GPT-4 の価格は GPT-3.5 の約 15 倍であるため、請求額が急激に増加します;
|
||||
- GPT-3.5 を使用しており、頻繁に使用していないにもかかわらず、請求額が急速に増加している場合は、以下の手順で直ちにトラブルシューティングを行ってください:
|
||||
- OpenAI のウェブサイトで API キーの消費記録を確認してください。トークンが 1 時間ごとに消費され、毎回数万トークンが消費される場合は、キーが流出している可能性があります。すぐに削除して再生成してください。**適当なサイトで残高を確認しないでください。**
|
||||
- パスワードが 5 文字以下など短い場合、ブルートフォースによるコストは非常に低くなります。誰かが大量のパスワードの組み合わせを試したかどうかを確認するために、docker のログを検索することを推奨する。キーワード:アクセスコードの取得
|
||||
- これら 2 つの方法を実行することで、トークンが急速に消費された原因を突き止めることができます:
|
||||
- OpenAI の消費記録に異常があるが、Docker ログに問題がない場合、API キーが流出したことを意味します;
|
||||
- Docker ログにアクセスコード取得のブルートフォース試行回数が多い場合は、パスワードがクラックされています。
|
230
docs/faq-ko.md
Normal file
230
docs/faq-ko.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# 자주 묻는 질문
|
||||
|
||||
## 어떻게 빠르게 도움을 받을 수 있나요?
|
||||
|
||||
1. ChatGPT / Bing / Baidu / Google 등에 질문합니다.
|
||||
2. 인터넷 사용자에게 질문합니다. 문제의 배경 정보와 자세한 문제 설명을 제공하세요. 질 좋은 질문은 유용한 답변을 쉽게 받을 수 있습니다.
|
||||
|
||||
# 배포 관련 질문
|
||||
|
||||
각종 배포 방법에 대한 자세한 튜토리얼 참조: [링크](https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b)
|
||||
|
||||
## 왜 Docker 배포 버전이 계속 업데이트 알림을 주나요?
|
||||
|
||||
Docker 버전은 사실상 안정된 버전과 같습니다. latest Docker는 항상 latest release version과 일치합니다. 현재 우리의 발행 빈도는 하루 또는 이틀에 한 번이므로 Docker 버전은 항상 최신 커밋보다 하루나 이틀 뒤처집니다. 이것은 예상된 것입니다.
|
||||
|
||||
## Vercel에서 어떻게 배포하나요?
|
||||
|
||||
1. Github 계정을 등록하고, 이 프로젝트를 포크합니다.
|
||||
2. Vercel을 등록합니다(휴대폰 인증 필요, 중국 번호 사용 가능), Github 계정을 연결합니다.
|
||||
3. Vercel에서 새 프로젝트를 생성하고, Github에서 포크한 프로젝트를 선택합니다. 환경 변수를 필요에 따라 입력한 후 배포를 시작합니다. 배포 후에는 VPN이 있는 환경에서 Vercel이 제공하는 도메인으로 프로젝트에 접근할 수 있습니다.
|
||||
4. 중국에서 방화벽 없이 접근하려면: 도메인 관리 사이트에서 도메인의 CNAME 레코드를 추가하고, cname.vercel-dns.com을 가리키게 합니다. 그런 다음 Vercel에서 도메인 접근을 설정합니다.
|
||||
|
||||
## Vercel 환경 변수를 어떻게 수정하나요?
|
||||
|
||||
- Vercel의 제어판 페이지로 이동합니다.
|
||||
- NextChat 프로젝트를 선택합니다.
|
||||
- 페이지 상단의 Settings 옵션을 클릭합니다.
|
||||
- 사이드바의 Environment Variables 옵션을 찾습니다.
|
||||
- 해당 값을 수정합니다.
|
||||
|
||||
## 환경 변수 CODE는 무엇이며, 반드시 설정해야 하나요?
|
||||
|
||||
이것은 당신이 사용자 정의한 접근 비밀번호입니다. 다음 중 하나를 선택할 수 있습니다:
|
||||
|
||||
1. 설정하지 않습니다. 해당 환경 변수를 삭제합니다. 주의: 이 경우 누구나 프로젝트에 접근할 수 있습니다.
|
||||
2. 프로젝트를 배포할 때 환경 변수 CODE를 설정합니다(여러 비밀번호는 쉼표로 구분). 접근 비밀번호를 설정하면 사용자는 설정 페이지에서 접근 비밀번호를 입력해야만 사용할 수 있습니다. [관련 설명 참조](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81)
|
||||
|
||||
## 왜 내 배포 버전에 스트리밍 응답이 없나요?
|
||||
|
||||
> 관련 토론: [#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
|
||||
|
||||
nginx 리버스 프록시를 사용하는 경우, 설정 파일에 다음 코드를 추가해야 합니다:
|
||||
|
||||
```nginx
|
||||
# 캐시하지 않고, 스트리밍 출력 지원
|
||||
proxy_cache off; # 캐시 비활성화
|
||||
proxy_buffering off; # 프록시 버퍼링 비활성화
|
||||
chunked_transfer_encoding on; # 청크 전송 인코딩 활성화
|
||||
tcp_nopush on; # TCP NOPUSH 옵션 활성화, Nagle 알고리즘 금지
|
||||
tcp_nodelay on; # TCP NODELAY 옵션 활성화, 지연 ACK 알고리즘 금지
|
||||
keepalive_timeout 300; # keep-alive 타임아웃을 65초로 설정
|
||||
```
|
||||
|
||||
netlify에서 배포하는 경우, 이 문제는 아직 해결되지 않았습니다. 기다려 주십시오.
|
||||
|
||||
## 배포했지만 액세스할 수 없는 경우.
|
||||
|
||||
다음의 사항들을 확인해보세요:
|
||||
|
||||
- 서비스가 배포 중인가요?
|
||||
- 포트가 올바르게 매핑되었나요?
|
||||
- 방화벽에서 포트가 열렸나요?
|
||||
- 서버 경로가 유효한가요?
|
||||
- 도메인 이름이 올바른가요?
|
||||
|
||||
## 프록시란 무엇이며 어떻게 사용하나요?
|
||||
|
||||
중국 및 일부 국가에서는 OpenAI의 IP 제한으로 인해 OpenAI API에 직접 연결할 수 없으며 프록시를 거쳐야 합니다. 프록시 서버(정방향 프록시)를 사용하거나 OpenAI API에 대해 설정된 역방향 프록시를 사용할 수 있습니다.
|
||||
|
||||
- 정방향 프록시 예: 사이언티픽 인터넷 래더. 도커 배포의 경우 환경 변수 HTTP_PROXY를 프록시 주소(예: 10.10.10.10:8002)로 설정합니다.
|
||||
- 역방향 프록시 예: 다른 사람이 구축한 프록시 주소를 사용하거나 Cloudflare를 통해 무료로 설정할 수 있습니다. 프로젝트 환경 변수 BASE_URL을 프록시 주소로 설정합니다.
|
||||
|
||||
## 국내 서버를 배포할 수 있나요?
|
||||
|
||||
예. 하지만 해결해야 할 문제가 있습니다:
|
||||
|
||||
- github 및 openAI와 같은 사이트에 연결하려면 프록시가 필요합니다;
|
||||
- 도메인 이름 확인을 설정하려면 국내 서버를 신청해야 합니다;
|
||||
- 국내 정책에 따라 프록시가 엑스트라넷/ChatGPT 관련 애플리케이션에 액세스하지 못하도록 제한되어 차단될 수 있습니다.
|
||||
|
||||
## 도커 배포 후 네트워크 오류가 발생하는 이유는 무엇인가요?
|
||||
|
||||
https://github.com/Yidadaa/ChatGPT-Next-Web/issues/1569 에서 토론을 참조하세요.
|
||||
|
||||
## 사용 관련 문제
|
||||
|
||||
## "문제가 발생했습니다, 나중에 다시 시도하세요"라는 메시지가 계속 뜨는 이유는 무엇인가요?
|
||||
|
||||
여러 가지 이유가 있을 수 있으니 순서대로 확인해 주세요:
|
||||
|
||||
- 코드 버전이 최신 버전인지 확인하고, 최신 버전으로 업데이트한 후 다시 시도해 주세요;
|
||||
- API 키가 올바르게 설정되었는지 확인해주세요. 환경 변수 이름은 모두 대문자이며 밑줄이 있어야 합니다;
|
||||
- API 키가 사용 가능한지 확인해 주세요;
|
||||
- 위 단계를 수행한 후에도 문제를 확인할 수 없는 경우, 이슈 영역에 신규 이슈를 제출하고 버셀의 런타임 로그 또는 도커 런타임 로그를 첨부해 주시기 바랍니다.
|
||||
|
||||
## ChatGPT 응답이 왜곡되는 이유는 무엇인가요?
|
||||
|
||||
설정 - 모델 설정 섹션에 '온도'에 대한 값이 있는데, 이 값이 1보다 크면 응답이 왜곡될 수 있으니 1 이내로 다시 설정해 주세요.
|
||||
|
||||
## "권한이 없는 상태입니다, 설정 페이지에서 액세스 비밀번호를 입력하세요"?
|
||||
|
||||
프로젝트에서 환경 변수 CODE에 접근 비밀번호를 설정했습니다. 처음 사용할 때는 설정 페이지에서 액세스 코드를 입력해야 합니다.
|
||||
|
||||
## 사용 시 "현재 할당량을 초과했습니다, ..."라는 메시지가 표시됩니다.
|
||||
|
||||
API 키에 문제가 있습니다. 잔액이 부족합니다.
|
||||
|
||||
## "오류: CSS 청크 xxx를 로드하지 못했습니다..."와 함께 사용.
|
||||
|
||||
첫 번째 화이트 스크린 시간을 줄이기 위해 청크 컴파일이 기본적으로 활성화되어 있으며, 기술 원칙은 아래를 참조하세요:
|
||||
|
||||
- https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
|
||||
- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4
|
||||
- https://github.com/vercel/next.js/issues/38507
|
||||
- https://stackoverflow.com/questions/55993890/how-can-i-disable-chunkcode-splitting-with-webpack4
|
||||
|
||||
그러나 NextJS는 호환성이 좋지 않아 구형 브라우저에서 이 오류가 발생할 수 있으므로 컴파일 시 청크 컴파일을 비활성화할 수 있습니다.
|
||||
|
||||
버셀 플랫폼의 경우 환경 변수에 `DISABLE_CHUNK=1`을 추가하고 다시 배포합니다;
|
||||
자체 컴파일 및 배포한 프로젝트의 경우, 빌드 시 `DISABLE_CHUNK=1 yarn build`를 사용하여 빌드합니다;
|
||||
Docker 사용자의 경우, Docker가 프로젝트를 패키징할 때 이미 빌드하기 때문에 이 기능을 해제하는 것은 지원되지 않습니다.
|
||||
|
||||
이 기능을 끄면 사용자가 웹사이트를 처음 방문할 때 모든 리소스를 로드하므로 인터넷 연결 상태가 좋지 않은 경우 흰색 화면이 길게 표시되어 사용자 경험에 영향을 줄 수 있으므로 사용자가 직접 고려하시기 바랍니다.
|
||||
|
||||
"## NotFoundError: '노드': 노드....에서 'removeChild'를 실행하지 못했습니다." 오류가 발생했습니다.
|
||||
브라우저의 자체 자동 번역 기능을 비활성화하고 모든 자동 번역 플러그인을 닫아주세요.
|
||||
|
||||
## 웹 서비스 관련 문제
|
||||
|
||||
## 클라우드플레어란 무엇인가요?
|
||||
|
||||
Cloudflare(CF)는 CDN, 도메인 관리, 정적 페이지 호스팅, 엣지 컴퓨팅 기능 배포 등을 제공하는 웹 서비스 제공업체입니다. 일반적인 용도: 도메인 구매 및/또는 호스팅(리졸브, 동적 도메인 등), 서버에 CDN 설치(벽에서 IP를 숨기는 기능), 웹사이트 배포(CF 페이지). CF는 이러한 서비스 대부분을 무료로 제공합니다.
|
||||
|
||||
## Vercel이란 무엇인가요?
|
||||
|
||||
Vercel은 개발자가 최신 웹 애플리케이션을 더 빠르게 빌드하고 배포할 수 있도록 설계된 글로벌 클라우드 플랫폼입니다. 이 프로젝트와 많은 웹 애플리케이션을 클릭 한 번으로 Vercel에 무료로 배포할 수 있습니다. 코드, 리눅스, 서버, 수수료가 필요 없고 OpenAI API 프록시를 설정할 필요도 없습니다. 단점은 중국에서 장벽 없이 액세스하려면 도메인 이름을 바인딩해야 한다는 것입니다.
|
||||
|
||||
## 도메인 네임은 어떻게 얻나요?
|
||||
|
||||
1) 도메인 네임 공급업체로 이동하여 해외에서는 Namesilo(알리페이 지원), 클라우드플레어 등, 중국에서는 월드와이드웹과 같은 도메인 네임을 등록합니다. 2) 무료 도메인 네임 공급업체: 예: eBay;
|
||||
2. 무료 도메인 네임 제공업체: eu.org(두 번째 레벨 도메인 네임) 등..;
|
||||
3. 친구에게 무료 2단계 도메인 네임을 요청합니다.
|
||||
|
||||
## 서버를 얻는 방법
|
||||
|
||||
- 외국 서버 제공업체의 예: 아마존 클라우드, 구글 클라우드, 벌터, 밴드왜건, 호스트데어 등;
|
||||
해외 서버 문제: 서버 라인은 해당 국가의 액세스 속도에 영향을 미치므로 CN2 GIA 및 CN2 라인 서버를 권장합니다. 국내 서버의 접속에 문제가 있는 경우(심각한 패킷 손실 등) CDN(Cloudflare 및 기타 제공 업체)을 설정해 볼 수 있습니다.
|
||||
- 국내 서버 제공업체: 알리윈, 텐센트 등;
|
||||
국내 서버 문제: 도메인 이름 확인을 신청해야 하며, 국내 서버 대역폭이 더 비싸고, 해외 사이트(Github, openAI 등)에 액세스하려면 프록시가 필요합니다.
|
||||
|
||||
## 서버는 언제 신청해야 하나요?
|
||||
|
||||
중국 본토에서 운영되는 웹사이트는 규제 요건에 따라 신고해야 합니다. 실제로 서버가 중국에 있고 도메인 네임 레졸루션이 있는 경우 서버 제공업체가 규제 신고 요건을 시행하며, 그렇지 않으면 서비스가 종료됩니다. 일반적인 규칙은 다음과 같습니다:
|
||||
|서버 위치|도메인 네임 공급자|파일링 필요 여부|
|
||||
|---|---|---|
|
||||
|국내|국내|예
|
||||
|국내|외국|예
|
||||
|외국|외국인|아니요
|
||||
|외국|국내|일반적으로 아니요|
|
||||
|
||||
서버 공급자를 전환한 후 파일링을 전환해야 합니다.
|
||||
|
||||
## OpenAI 관련 질문
|
||||
|
||||
## OpenAI 계정은 어떻게 가입하나요?
|
||||
|
||||
chat.openai.com으로 이동하여 등록하세요. 다음이 필요합니다:
|
||||
|
||||
- 유효한 래더(OpenAI는 지역별 기본 IP 주소를 지원합니다)
|
||||
- 지원되는 이메일 주소(예: Outlook이나 qq가 아닌 Gmail 또는 회사/학교 이메일)
|
||||
- SMS 인증을 받을 수 있는 방법(예: SMS 활성화 웹사이트)
|
||||
|
||||
## OpenAI API는 어떻게 열 수 있나요? API 잔액은 어떻게 확인하나요?
|
||||
|
||||
공식 웹사이트 주소(래더 필요): https://platform.openai.com/account/usage
|
||||
일부 사용자는 래더 없이 잔액 조회 에이전트를 구축한 경우가 있으니, 해당 사용자에게 요청해 주시기 바랍니다. API 키 유출을 방지하기 위해 신뢰할 수 있는 소스인지 확인하시기 바랍니다.
|
||||
|
||||
## 새로 등록한 OpenAI 계정에 API 잔액이 없는 이유는 무엇인가요?
|
||||
|
||||
(4월 6일 업데이트) 새로 등록된 계정은 일반적으로 24시간 후에 API 잔액이 표시됩니다. 현재 새로 등록된 계정에는 $5의 잔액이 표시됩니다.
|
||||
|
||||
## OpenAI API를 충전하려면 어떻게 해야 하나요?
|
||||
|
||||
OpenAI는 특정 지역의 신용카드만 사용할 수 있습니다(중국 신용카드는 사용할 수 없음). 충전 방법의 몇 가지 예는 다음과 같습니다:
|
||||
|
||||
1. 가상 신용카드로 결제하기
|
||||
2. 해외 신용카드 신청
|
||||
3. 온라인에서 신용카드를 충전할 사람 찾기
|
||||
|
||||
## GPT-4 API 액세스는 어떻게 사용하나요?
|
||||
|
||||
- GPT-4 API 액세스는 별도의 신청이 필요합니다. 다음 주소로 이동하여 정보를 입력하여 신청 대기열 대기자 명단에 들어가세요(OpenAI 조직 ID를 준비하세요): https://openai.com/waitlist/gpt-4-api.
|
||||
그런 다음 이메일 메시지를 기다립니다.
|
||||
- ChatGPT Plus를 사용하도록 설정했다고 해서 GPT-4 권한이 있는 것은 아니며, 서로 관련이 없습니다.
|
||||
|
||||
## Azure OpenAI 인터페이스 사용 방법
|
||||
|
||||
참조: [#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
|
||||
|
||||
## 내 토큰이 왜 이렇게 빨리 소모되나요?
|
||||
|
||||
> 관련 토론: [#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
|
||||
|
||||
- GPT 4에 액세스 권한이 있고 매일 GPT 4 API를 사용하는 경우, GPT 4 가격이 GPT 3.5의 약 15배이기 때문에 청구 금액이 급격히 증가합니다;
|
||||
- GPT 3.5를 자주 사용하지 않는데도 요금이 급격하게 증가하는 경우 아래 단계를 따라 확인하시기 바랍니다:
|
||||
- 오픈아이 공식 웹사이트로 이동하여 API 키 소비 기록을 확인하고, 매 시간마다 토큰이 소비되고 매번 수만 개의 토큰이 소비된다면 키가 유출된 것이므로 즉시 삭제하고 재생성하시기 바랍니다. 즉시 키를 삭제하고 다시 생성하시기 바랍니다. 지저분한 웹사이트에서 잔액을 확인하지 마세요. **
|
||||
- 비밀번호 설정이 5자리 이내의 문자와 같이 매우 짧으면 블라스팅 비용이 매우 낮습니다. 도커의 로그 기록을 검색하여 누군가 많은 수의 비밀번호 조합을 시도했는지 확인하는 것이 좋습니다. 키워드: 액세스 코드를 얻었습니다.
|
||||
- 이 두 가지 방법을 사용하면 토큰이 소비되는 이유를 빠르게 찾을 수 있습니다:
|
||||
- 오픈아이 소비 기록은 비정상적이지만 도커 로그는 정상이라면 API 키가 유출되고 있다는 뜻입니다;
|
||||
- 도커 로그에서 액세스 코드 버스트 레코드가 많이 발견되면 비밀번호가 버스트된 것입니다.
|
||||
|
||||
|
||||
## API의 가격은 어떻게 청구되나요?
|
||||
|
||||
OpenAI의 청구 지침은 https://openai.com/pricing#language-models 에서 확인할 수 있습니다.
|
||||
OpenAI는 토큰 수에 따라 요금을 청구하며, 일반적으로 1000토큰은 영어 단어 750개 또는 중국어 문자 500개를 나타냅니다. 입력(프롬프트)과 출력(완료)은 별도로 청구됩니다.
|
||||
|
||||
|모델|사용자 입력(프롬프트) 청구 |모델 출력(완료) 청구 |인터랙션당 최대 토큰 수 |
|
||||
|----|----|----|----|
|
||||
|GPT-3.5-TURBO|$0.0015 / 1천 토큰|$0.002 / 1천 토큰|4096|
|
||||
|GPT-3.5-TURBO-16K|$0.003 / 1천 토큰|$0.004 / 1천 토큰|16384| |GPT-4|$0.004 / 1천 토큰|16384
|
||||
|GPT-3.5-TURBO-16K|$0.003 / 1천 토큰|$0.004 / 1천 토큰|16384| |GPT-4|$0.03 / 1천 토큰|$0.06 / 1천 토큰|8192
|
||||
|GPT-4-32K|$0.06 / 1천 토큰|$0.12 / 1천 토큰|32768|
|
||||
|
||||
## gpt-3.5-터보와 gpt3.5-터보-0301(또는 gpt3.5-터보-mmdd) 모델의 차이점은 무엇인가요?
|
||||
|
||||
공식 문서 설명: https://platform.openai.com/docs/models/gpt-3-5
|
||||
|
||||
- GPT-3.5-TURBO는 최신 모델이며 지속적으로 업데이트될 예정입니다.
|
||||
- gpt-3.5-turbo-0301은 3월 1일에 고정된 모델의 스냅샷으로, 변경되지 않으며 3개월 후에 새로운 스냅샷으로 대체될 예정입니다.
|
BIN
docs/images/head-cover.png
Normal file
BIN
docs/images/head-cover.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/images/upstash-1.png
Normal file
BIN
docs/images/upstash-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user