Compare commits

...

133 Commits

Author SHA1 Message Date
Yifei Zhang
b7b16aa33b Merge pull request #882 from Yidadaa/bugfix-0417
feat: user prompts
2023-04-18 01:40:38 +08:00
Yidadaa
789a779775 feat: user prompts 2023-04-18 01:34:12 +08:00
Yidadaa
fdc8278b90 feat: check usage throttle 2023-04-17 23:12:27 +08:00
Yidadaa
525a2ff9a7 fix: #866 remove unused retry messages 2023-04-17 22:51:14 +08:00
Yifei Zhang
78c9e66c1f Merge pull request #870 from jaw52/fix/windows_error
fix: error in windows
2023-04-17 16:38:55 +08:00
jaw
3038dfdb27 fix: error in windows 2023-04-17 15:37:03 +08:00
Yifei Zhang
7cd9f644ee Merge pull request #861 from ClarenceYk/for_test
Update app/requests.ts
2023-04-17 13:00:06 +08:00
Ma Yuke
64e78329ec Update app/requests.ts
fix: displaying the number of subscription beyond two decimal places.
2023-04-17 11:53:09 +08:00
Yifei Zhang
e49acda806 Merge pull request #859 from Yidadaa/Yidadaa-patch-1
fix: #853 fetch duplex errors
2023-04-17 11:41:47 +08:00
Yifei Zhang
b79845fcaa fixup 2023-04-17 11:36:32 +08:00
Yifei Zhang
76ef5ef9a9 fixup 2023-04-17 11:34:33 +08:00
Yifei Zhang
cc053b148d fix: #853 fetch duplex errors 2023-04-17 11:27:31 +08:00
Yifei Zhang
2979df1d82 Update README.md 2023-04-17 11:17:05 +08:00
Yifei Zhang
c305ba3c92 Merge pull request #839 from Yidadaa/bugfix-0416
feat: close #813 log user ip time
2023-04-16 18:57:59 +08:00
Yidadaa
12d4081311 feat: close #539 add delete message button 2023-04-16 18:55:29 +08:00
Yidadaa
124938ecc9 fix: #832 update nextjs version to 13.3.0 2023-04-16 18:17:46 +08:00
Yidadaa
ea3e8a7459 fix: #829 filter empty prompt 2023-04-16 18:11:09 +08:00
Yidadaa
dc3883ed1a feat: close #118 add stop all button 2023-04-16 18:07:43 +08:00
Yidadaa
bd69c8f5dd feat: close #813 log user ip time 2023-04-16 17:20:33 +08:00
Yifei Zhang
b751861bfb Merge pull request #838 from tscherrie/add-german-translations
some final language quality improvements
2023-04-16 16:57:14 +08:00
tscherrie
d035523f30 Merge branch 'Yidadaa:main' into add-german-translations 2023-04-16 10:44:22 +03:00
tscherrie tscherru
f9906aba7f found typo 2023-04-16 07:43:38 +00:00
tscherrie tscherru
410695d823 some final language quality improvements 2023-04-16 07:11:36 +00:00
Yifei Zhang
cd198ee1fa Merge pull request #836 from tscherrie/add-german-translations
Add german language translation de.ts
2023-04-16 14:51:07 +08:00
tscherrie tscherru
2c35c26749 fixed typo 2023-04-16 05:32:55 +00:00
tscherrie tscherru
f042d07ee7 added de to index and other files 2023-04-16 05:07:54 +00:00
tscherrie tscherru
4ce5f89c66 fixed german language translations 2023-04-16 04:16:04 +00:00
tscherrie tscherru
e0fa0d1936 Add german language translation de.tss 2023-04-16 04:00:31 +00:00
Yifei Zhang
96d65f7d39 Merge pull request #830 from ilario92/main
fix IT translation
2023-04-16 00:39:59 +08:00
ilarioscandurra
98fd08d9e5 fix IT translation 2023-04-15 16:33:04 +02:00
Yifei Zhang
e0f6c801d5 Merge pull request #812 from Yidadaa/bugfix-0415
refactor: remove protocol env vars
2023-04-15 02:52:53 +08:00
Yidadaa
ad274b7cad fixup 2023-04-15 02:51:15 +08:00
Yidadaa
0a79df3670 refactor: remove protocol env vars 2023-04-15 02:50:04 +08:00
Yifei Zhang
f5a05da792 Merge pull request #811 from Yidadaa/bugfix-0415
fix: #804 disable auto scroll in textarea
2023-04-15 02:38:30 +08:00
Yidadaa
47a2911ee2 feat: edit session title button 2023-04-15 02:37:24 +08:00
Yidadaa
fb14785cad fix: #804 disable auto scroll in textarea 2023-04-15 02:28:30 +08:00
Yifei Zhang
b0f7aeece5 Merge pull request #807 from jtung4/pr
feat: close #796 improve tw summarize title prompt
2023-04-15 02:03:45 +08:00
jtung4
73216513dd Update tw.ts 2023-04-14 19:49:48 +08:00
Yifei Zhang
563eb96282 Update README.md 2023-04-13 22:47:40 +08:00
Yifei Zhang
480ffa0ab3 Merge pull request #770 from JessySnow/dev/fix_docker_config
fix docker proxy config
2023-04-13 21:57:48 +08:00
JessySnow
4baadefa1d fix docker proxy config 2023-04-13 18:36:50 +08:00
Yifei Zhang
9fef4cab73 Merge pull request #755 from peanut996/patch-2
Fix wrong spelling
2023-04-13 14:45:47 +08:00
Peanuts
7fc43b4745 Fix wrong spelling 2023-04-13 11:07:42 +08:00
Yifei Zhang
86fb36163c Update README_CN.md 2023-04-13 11:05:36 +08:00
Yifei Zhang
5ad8a3e214 Update README.md 2023-04-13 11:04:48 +08:00
Yifei Zhang
f6f07abf1d Update markdown.tsx 2023-04-13 02:16:21 +08:00
Yifei Zhang
233e7bf406 Merge pull request #746 from Yidadaa/bugfix-0412
feat: close #680 lazy load markdown dom
2023-04-13 02:11:05 +08:00
Yifei Zhang
3f34731adf Merge pull request #747 from yorunning/ci-prompt
ci(sync): add an error prompt
2023-04-13 02:09:05 +08:00
Yidadaa
8363cdd9fa feat: close #680 lazy rendering markdown 2023-04-13 02:07:24 +08:00
Yorun
e12ea69b6a ci(sync): add an error prompt 2023-04-12 15:31:40 +00:00
Yidadaa
d790b0b372 feat: close #680 lazy load markdown dom 2023-04-12 23:27:28 +08:00
Yidadaa
9146b98285 fix: *.scss *.svg types 2023-04-12 23:04:45 +08:00
Yifei Zhang
2fb67f52a1 Merge pull request #742 from Yidadaa/revert-715-bugfix-scrollthumb
Revert "fix: autoscroll conflict"
2023-04-12 19:48:59 +08:00
Yifei Zhang
c1bb53c4e3 Revert "fix: autoscroll conflict" 2023-04-12 19:48:12 +08:00
Yifei Zhang
2b921093e1 Update README.md 2023-04-12 17:03:42 +08:00
Yifei Zhang
7c9248a764 Update README.md 2023-04-12 14:58:11 +08:00
Yifei Zhang
5fc0ae1377 Update README.md 2023-04-12 12:13:52 +08:00
Yifei Zhang
3ee99cae75 docs: add vercel instructions 2023-04-12 12:11:22 +08:00
Yifei Zhang
a15bd3ce33 Merge pull request #715 from betgo/bugfix-scrollthumb
fix: autoscroll conflict
2023-04-12 11:05:53 +08:00
Yifei Zhang
88fff82e3a Merge pull request #721 from helloworld-xdy/main
fix:修复正常响应结果后额外追加‘出错了,请稍后再试’bug
2023-04-11 22:56:10 +08:00
Yifei Zhang
3f2a8cc0bb Merge pull request #723 from latorc/patch-1
docs: Update faq-cn.md
2023-04-11 22:53:17 +08:00
latorc
65aca5c38a Update faq-cn.md 2023-04-11 22:17:36 +08:00
helloworld_xy
d8ef4a1031 fix:修复正常响应结果后额外追加‘出错了,请稍后再试’bug 2023-04-11 19:03:09 +08:00
Yifei Zhang
8d2abe36a9 Update README.md 2023-04-11 16:04:36 +08:00
Yifei Zhang
5a56bc3465 Merge pull request #688 from aooyoo/main
add Japanese
2023-04-11 15:37:23 +08:00
Yifei Zhang
b4cea45bfe Merge pull request #708 from peanut996/patch-1
[refactor] rename ChatResponse.
2023-04-11 15:36:50 +08:00
betgo
4269775665 fix: autoscroll conflict 2023-04-11 14:27:34 +08:00
peanut996
a1c709bb58 [refactor] rename ChatResponse. 2023-04-11 12:06:12 +08:00
Yifei Zhang
9f92968b96 Update issue templates 2023-04-11 11:21:05 +08:00
Yifei Zhang
f43d936ef0 Update faq-cn.md 2023-04-11 11:15:24 +08:00
Yifei Zhang
0f739f442e Merge pull request #697 from Yidadaa/bugfix-0410
fix: runtime config and proxy fix
2023-04-11 02:59:51 +08:00
Yidadaa
6841846613 fixup 2023-04-11 02:56:48 +08:00
Yidadaa
d6e6dd09f0 feat: dynamic config 2023-04-11 02:54:31 +08:00
Yidadaa
9b61cb1335 refactor: build/runtime/client configs 2023-04-11 01:21:34 +08:00
Yidadaa
7aee53ea05 fix: #507 break cjk chars in stream mode 2023-04-10 23:13:20 +08:00
Yidadaa
8df8ee8936 fix: #676 docker override old proxy files 2023-04-10 22:46:58 +08:00
aooyoo
042e989ebb fix spelling errors 2023-04-10 14:12:31 +00:00
aooyoo
7670b8b738 add new language, Japanese 2023-04-10 14:01:54 +00:00
Yifei Zhang
ec985f6a1d Merge pull request #679 from muhammetdemirel/main
Added new language, Turkish.
2023-04-10 18:03:39 +08:00
Yifei Zhang
2ec99bbb70 Update docker.yml 2023-04-10 17:39:21 +08:00
Muhammet Demirel
d7edcadec7 Added new language, Turkish. 2023-04-10 12:04:30 +03:00
Yifei Zhang
150735b001 Update access.ts 2023-04-10 10:57:16 +08:00
Yidadaa
3c1e81897a fixup: wont show fullscreen icon on mobile 2023-04-10 01:13:33 +08:00
Yifei Zhang
601e72b56c Merge pull request #659 from Yidadaa/bugfix-0409
fix: many UI bugs and resizable side bar
2023-04-10 01:04:38 +08:00
Yidadaa
09fd743e2e feat: wider app body 2023-04-10 00:56:44 +08:00
Yidadaa
6ae61c5357 fix: #522 resizable side bar 2023-04-10 00:54:17 +08:00
Yidadaa
eae5a8a2e6 feat: #577 maximum / minimium icon 2023-04-10 00:00:36 +08:00
Yidadaa
0e05733bbb fix: #589 improve unauthorized tips 2023-04-09 23:51:12 +08:00
Yidadaa
4a492264a1 fix: #641 delete wrong chat list 2023-04-09 23:41:16 +08:00
Yidadaa
174c745279 fix: #648 password input style 2023-04-09 23:35:45 +08:00
Yifei Zhang
3cfec63a95 Merge pull request #654 from Yidadaa/docker
feat: #120 docker proxy
2023-04-09 20:46:37 +08:00
yidadaa
0c92d49f89 chore: update readme 2023-04-09 20:43:34 +08:00
yidadaa
1bb7b4a653 feat: add proxy for docker 2023-04-09 20:35:42 +08:00
Yifei Zhang
72aa2bcad8 Update cn.ts 2023-04-09 13:31:33 +08:00
Yidadaa
c8be5e4267 feat: add docker proxy 2023-04-09 00:56:56 +08:00
Yifei Zhang
40b8b225f9 Create vercel.json 2023-04-08 22:27:51 +08:00
Yifei Zhang
2666241df7 Merge pull request #618 from ilario92/main
Update: Confirmation on reset chats and settings
2023-04-08 16:58:20 +08:00
GH Action - Upstream Sync
037d4638ea Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-04-08 07:07:44 +00:00
Yifei Zhang
c4f1376faf Merge pull request #621 from ClarenceYk/main
Update ui-lib.module.scss
2023-04-08 14:43:05 +08:00
MaYuKe
7d2e850c42 Update ui-lib.module.scss
Fix: icon-button can not be completely masked by popover.
2023-04-08 12:19:14 +08:00
GH Action - Upstream Sync
70b6507299 Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-04-07 21:07:51 +00:00
Ilario Scandurra
00ba47c4de Update sync.yml 2023-04-07 22:19:21 +02:00
Ilario Scandurra
5b9fd1b101 Update sync.yml 2023-04-07 22:19:06 +02:00
Yifei Zhang
913305190a Update README_CN.md 2023-04-08 03:30:11 +08:00
Yifei Zhang
3c6f2962cc Merge pull request #617 from Yidadaa/bugfix-0408
fix: auto grow textarea
2023-04-08 03:13:14 +08:00
Yidadaa
45c8de42b9 fix: #528 wont send max_tokens 2023-04-08 03:03:07 +08:00
Yidadaa
637cda5b4c fix: #613 show all prompts when input / 2023-04-08 02:53:35 +08:00
ilarioscandurra
d935de3e57 Update: Confirmation on reset chats and settings 2023-04-07 20:50:06 +02:00
Yidadaa
13035ecb0d fix: auto grow textarea 2023-04-08 02:36:02 +08:00
Yifei Zhang
d6b2cf8bcb Merge pull request #318 from leedom92/textarea-height-optimize
feat: textarea with adaptive height
2023-04-08 01:01:52 +08:00
Yifei Zhang
9afed21efd Merge pull request #595 from yorunning/fix
fix: hide toast when confirmation box is cancelled
2023-04-08 00:59:09 +08:00
Yorun
71d9fbc367 fix: hide toast on cancel session deletion on mobile 2023-04-07 14:51:08 +08:00
Yifei Zhang
f83859113e Update README.md 2023-04-07 13:58:56 +08:00
leedom
00d45e7cc4 fix: solve navigator undefined && merge from main 2023-04-07 13:47:34 +08:00
leedom
406ed8a02a Merge branch 'main' into textarea-height-optimize 2023-04-07 13:33:37 +08:00
Yifei Zhang
e793b0c49d Merge pull request #587 from zhongmeizhi/main
feat: 添加对热键的支持 Escape to close settings, Up Arrow to get last input
2023-04-07 13:09:09 +08:00
leedom
de740ec57f feat: add calcTextareaHeight.ts 2023-04-07 12:42:47 +08:00
leedom
e5b4cb28fe fix: useEffect hooks 2023-04-07 12:39:27 +08:00
leedom
cb210d82e1 update: resizeTextarea 2023-04-07 12:33:09 +08:00
Mokou
58b956f7cc feat: Hot keys: Escape to close settings, Up Arrow to get last input 2023-04-07 12:26:22 +08:00
leedom
620b98fe6a Merge branch 'main' into textarea-height-optimize 2023-04-07 12:20:53 +08:00
leedom
3656c8458f feat: textarea with adaptive height 2023-04-07 12:17:37 +08:00
leedom
a8a8becf96 merge 2023-04-07 07:29:17 +08:00
leedom
b8a605f07d merge 2023-04-07 07:18:53 +08:00
leedom
3825b3c2c2 Merge branch 'textarea-height-optimize' of github.com:leedom92/ChatGPT-Next-Web into textarea-height-optimize 2023-04-02 11:16:30 +08:00
Leedom
4369b26e22 Update calcTextareaHeight.js 2023-04-02 09:47:55 +08:00
leedom
6f7c2916ef Merge remote-tracking branch 'upstream/main' into textarea-height-optimize 2023-04-02 03:20:06 +08:00
Leedom
b419e7d918 Update home.tsx 2023-04-02 02:56:45 +08:00
Leedom
ad8e09d188 Update home.tsx 2023-04-02 02:54:46 +08:00
leedom
bce020fc8e feat: add calcTextareaHeight.js from element-ui 2023-04-02 02:42:00 +08:00
leedom
a811637176 refactor: use the method of element-ui 2023-04-02 02:40:00 +08:00
Leedom
9ba59351c5 Update home.tsx 2023-04-01 17:48:05 +08:00
leedom
2a79d35667 feat: add optimize textarea height when inputing 2023-04-01 17:34:38 +08:00
73 changed files with 2244 additions and 524 deletions

View File

@@ -7,6 +7,11 @@ assignees: ''
---
**反馈须知**
> 请在下方中括号内输入 x 来表示你已经知晓相关内容。
- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
**描述问题**
请在此描述你遇到了什么问题。

View File

@@ -43,7 +43,7 @@ jobs:
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -5,7 +5,7 @@ permissions:
on:
schedule:
- cron: "0 */6 * * *" # every 6 hours
- cron: "0 * * * *" # every hour
workflow_dispatch:
jobs:
@@ -31,3 +31,10 @@ jobs:
# Set test_mode true to run tests instead of the true action!!
test_mode: false
- name: Sync check
if: failure()
run: |
echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
exit 1

View File

@@ -8,6 +8,7 @@ WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn config set registry 'https://registry.npm.taobao.org'
RUN yarn install
FROM base AS builder
@@ -16,7 +17,6 @@ RUN apk update && apk add --no-cache git
ENV OPENAI_API_KEY=""
ENV CODE=""
ARG DOCKER=true
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
@@ -27,6 +27,9 @@ RUN yarn build
FROM base AS runner
WORKDIR /app
RUN apk add proxychains-ng
ENV PROXY_URL=""
ENV OPENAI_API_KEY=""
ENV CODE=""
@@ -37,4 +40,20 @@ COPY --from=builder /app/.next/server ./.next/server
EXPOSE 3000
CMD ["node","server.js"]
CMD if [ -n "$PROXY_URL" ]; then \
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 "[ProxyList]" >> $conf; \
echo "$protocol $host $port" >> $conf; \
cat /etc/proxychains.conf; \
proxychains -f $conf node server.js; \
else \
node server.js; \
fi

View File

@@ -3,11 +3,15 @@
<h1 align="center">ChatGPT Next Web</h1>
English / [简体中文](./README_CN.md)
One-Click to deploy well-designed ChatGPT web UI on Vercel.
一键免费部署你的私人 ChatGPT 网页应用。
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join 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)
[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231789746-41f34d05-6ef9-43f3-a1d1-ff109d4c3c14.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
[![Deploy with Vercel](https://vercel.com/button)](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)
@@ -20,8 +24,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
## Features
- **Deploy for free with one-click** on Vercel in under 1 minute
- Privacy first, all data stored locally in the browser
- Responsive design, dark mode and PWA
- Fast first screen loading speed (~100kb)
- Fast first screen loading speed (~100kb), support streaming response
- 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
- One-click export all chat history with full Markdown support
@@ -44,13 +49,15 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- 在 1 分钟内使用 Vercel **免费一键部署**
- 精心设计的 UI响应式设计支持深色模式支持 PWA
- 极快的首屏加载速度(~100kb
- 极快的首屏加载速度(~100kb,支持流式响应
- 隐私安全,所有数据保存在用户浏览器本地
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 一键导出聊天记录,完整的 Markdown 支持
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划
## 开发计划
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
- [ ] 允许用户自行编辑内置 Prompt 列表
- [ ] 使用 tauri 打包桌面应用
@@ -58,11 +65,12 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能
- 界面文字自定义
- 用户登录、账号管理、消息云同步
## Get Started
> [简体中文 > 如何开始使用](./README_CN.md#开始使用)
1. Get [OpenAI API Key](https://platform.openai.com/account/api-keys);
@@ -71,27 +79,39 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
3. Enjoy :)
## FAQ
[简体中文 > 常见问题](./docs/faq-cn.md) | [English > FAQ](./docs/faq.en.md)
[简体中文 > 常见问题](./docs/faq-cn.md)
[English > FAQ](./docs/faq-en.md)
## Keep Updated
> [简体中文 > 如何保持代码更新](./README_CN.md#保持更新)
If you have deployed your own project with just one click following the steps above, you may encounter the issue of "Updates Available" constantly showing up. This is because Vercel will create a new project for you by default instead of forking this project, resulting in the inability to detect updates correctly.
We recommend that you follow the steps below to re-deploy:
- Delete the original repo;
- Fork this project;
- Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy;
- Please manually add an environment variable named `OPENAI_API_KEY` and enter your API key as the value during the redeploy process.
- Delete the original repository;
- Use the fork button in the upper right corner of the page to fork this project;
- Choose and deploy in Vercel again, [please see the detailed tutorial](./docs/vercel-cn.md).
This project will be continuously updated, and after forking the project, the upstream code will be automatically synchronized every day without additional operations.
### Enable Automatic Updates
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
![Automatic Updates](./docs/images/enable-actions.jpg)
![Enable Automatic Updates](./docs/images/enable-actions-sync.jpg)
### Manually Updating Code
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
You can star or watch this project or follow author to get release notifictions in time.
## Access Password
> [简体中文 > 如何增加访问密码](./README_CN.md#配置页面访问密码)
This project provides limited access control. Please add an environment variable named `CODE` on the vercel environment variables page. The value should be passwords separated by comma like this:
@@ -103,6 +123,7 @@ code1,code2,code3
After adding or modifying this environment variable, please redeploy the project for the changes to take effect.
## Environment Variables
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
@@ -115,19 +136,14 @@ Access passsword, separated by comma.
### `BASE_URL` (optional)
> Default: `api.openai.com`
> Default: `https://api.openai.com`
> Examples: `http://your-openai-proxy.com`
Override openai api request base url.
### `PROTOCOL` (optional)
> Default: `https`
> Values: `http` | `https`
Override openai api request protocol.
## Development
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -149,13 +165,28 @@ yarn dev
```
## Deployment
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
### Docker (Recommended)
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="your-password" \
yidadaa/chatgpt-next-web
```
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" \
yidadaa/chatgpt-next-web
```
### Shell
@@ -163,20 +194,36 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
## Screenshots
![Settings](./docs/images/settings.png)
![More](./docs/images/more.png)
## Donation
[Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
## Special Thanks
### Sponsor
> 仅列出捐赠金额 >= 100RMB 的用户。
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon)
[@relativequantum](https://github.com/relativequantum)
[@desenmeng](https://github.com/desenmeng)
[@webees](https://github.com/webees)
[@chazzhou](https://github.com/chazzhou)
[@hauy](https://github.com/hauy)
[@Corwin006](https://github.com/Corwin006)
[@yankunsong](https://github.com/yankunsong)
[@ypwhs](https://github.com/ypwhs)
[@fxxxchao](https://github.com/fxxxchao)
### Contributor

View File

@@ -11,7 +11,7 @@
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
![主界面](./static/cover.png)
![主界面](./docs/images/cover.png)
</div>
@@ -29,7 +29,7 @@
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. 点击右侧按钮开始部署:
[![Deploy with Vercel](https://vercel.com/button)](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
[![Deploy with Vercel](https://vercel.com/button)](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
3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
@@ -38,12 +38,19 @@
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
推荐你按照下列步骤重新部署:
- 删除掉原先的 repo
- fork 本项目;
- 前往 vercel 控制台,删除掉原先的 project然后新建 project选择你刚刚 fork 出来的项目重新进行部署即可;
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
- 删除掉原先的仓库
- 使用页面右上角的 fork 按钮,fork 本项目;
- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
本项目会持续更新,当你 Fork 项目之后,默认会每天自动同步上游代码,无需额外操作。
### 打开自动更新
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows并启用 Upstream Sync Action启用之后即可开启每小时定时自动更新
![自动更新](./docs/images/enable-actions.jpg)
![启用自动更新](./docs/images/enable-actions-sync.jpg)
### 手动更新代码
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
@@ -53,6 +60,8 @@
> 配置密码后,用户需要在设置页手动填写访问码才可以正常聊天,否则会通过消息提示未授权状态。
> **警告**:请务必将密码的位数设置得足够长,最好 7 位以上,否则[会被爆破](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
本项目提供有限的权限控制功能,请在 Vercel 项目控制面板的环境变量页增加名为 `CODE` 的环境变量,值为用英文逗号分隔的自定义密码:
```
@@ -62,6 +71,7 @@ code1,code2,code3
增加或修改该环境变量后,请**重新部署**项目使改动生效。
## 环境变量
> 本项目大多数配置项都通过环境变量来设置。
### `OPENAI_API_KEY` (必填项)
@@ -76,17 +86,13 @@ OpanAI 密钥,你在 openai 账户页面申请的 api key。
### `BASE_URL` (可选)
> Default: `api.openai.com`
> Default: `https://api.openai.com`
> Examples: `http://your-openai-proxy.com`
OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填写此选项。
### `PROTOCOL` (可选)
> Default: `https`
> Values: `http` | `https`
OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此选项设置为 http。
> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
## 开发
@@ -108,37 +114,51 @@ OPENAI_API_KEY=<your api key here>
2. 执行 `yarn install && yarn dev` 即可。
## 部署
### 容器部署 (推荐)
> 注意docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
yidadaa/chatgpt-next-web
```
你也可以指定 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
```
### 本地部署
在控制台运行下方命令:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
## 鸣谢
### 捐赠者
> 仅列出了部分大额打赏,小额打赏(< 100RMB人数太多在此不再列出敬请谅解。
[@mushan0x0](https://github.com/mushan0x0)
[@ClarenceDan](https://github.com/ClarenceDan)
[@zhangjia](https://github.com/zhangjia)
[@hoochanlon](https://github.com/hoochanlon)
### 捐赠者
> 见英文版。
### 贡献者
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## 开源协议
> 反对 996从我开始。
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

View File

@@ -1,17 +0,0 @@
import md5 from "spark-md5";
export function getAccessCodes(): Set<string> {
const code = process.env.CODE;
try {
const codes = (code?.split(",") ?? [])
.filter((v) => !!v)
.map((v) => md5.hash(v.trim()));
return new Set(codes);
} catch (e) {
return new Set();
}
}
export const ACCESS_CODES = getAccessCodes();
export const IS_IN_DOCKER = process.env.DOCKER;

View File

@@ -40,7 +40,7 @@ async function createStream(req: NextRequest) {
const parser = createParser(onParse);
for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk));
parser.feed(decoder.decode(chunk, { stream: true }));
}
},
});

View File

@@ -9,9 +9,16 @@ export async function requestOpenai(req: NextRequest) {
const apiKey = req.headers.get("token");
const openaiPath = req.headers.get("path");
console.log("[Proxy] ", openaiPath);
let baseUrl = BASE_URL;
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
if (!baseUrl.startsWith("http")) {
baseUrl = `${PROTOCOL}://${baseUrl}`;
}
console.log("[Proxy] ", openaiPath);
console.log("[Base Url]", baseUrl);
return fetch(`${baseUrl}/${openaiPath}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,

21
app/api/config/route.ts Normal file
View File

@@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../../config/server";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig.needCode,
};
declare global {
type DangerConfig = typeof DANGER_CONFIG;
}
export async function POST(req: NextRequest) {
return NextResponse.json({
needCode: serverConfig.needCode,
});
}

View File

@@ -29,3 +29,7 @@ export async function POST(req: NextRequest) {
export async function GET(req: NextRequest) {
return makeRequest(req);
}
export const config = {
runtime: "edge",
};

View File

@@ -4,4 +4,4 @@ import type {
} from "openai";
export type ChatRequest = CreateChatCompletionRequest;
export type ChatReponse = CreateChatCompletionResponse;
export type ChatResponse = CreateChatCompletionResponse;

View File

@@ -49,4 +49,7 @@
.icon-button-text {
margin-left: 5px;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -96,7 +96,7 @@ export function ChatList() {
index={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
onDelete={chatStore.deleteSession}
onDelete={() => chatStore.deleteSession(i)}
/>
))}
{provided.placeholder}

View File

@@ -1,5 +1,29 @@
@import "../styles/animation.scss";
.chat-input-actions {
display: flex;
flex-wrap: wrap;
.chat-input-action {
display: inline-flex;
border-radius: 20px;
font-size: 12px;
background-color: var(--white);
color: var(--black);
border: var(--border-in-light);
padding: 4px 10px;
animation: slide-in ease 0.3s;
box-shadow: var(--card-shadow);
transition: all ease 0.3s;
margin-bottom: 10px;
align-items: center;
&:not(:last-child) {
margin-right: 5px;
}
}
}
.prompt-toast {
position: absolute;
bottom: -50px;

View File

@@ -1,9 +1,10 @@
import { useDebouncedCallback } from "use-debounce";
import { useDebounce, useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg";
import ExportIcon from "../icons/export.svg";
import RenameIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
@@ -11,6 +12,14 @@ import LoadingIcon from "../icons/three-dots.svg";
import BotIcon from "../icons/bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg";
import AutoIcon from "../icons/auto.svg";
import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg";
import {
Message,
@@ -19,6 +28,8 @@ import {
BOT_HELLO,
ROLES,
createMessage,
useAccessStore,
Theme,
} from "../store";
import {
@@ -27,6 +38,7 @@ import {
getEmojiUrl,
isMobileScreen,
selectOrCopy,
autoGrowTextArea,
} from "../utils";
import dynamic from "next/dynamic";
@@ -39,7 +51,7 @@ import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
import { Input, Modal, showModal, showToast } from "./ui-lib";
import { Input, Modal, showModal } from "./ui-lib";
const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown),
@@ -56,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) {
const config = useChatStore((state) => state.config);
if (props.role !== "user") {
return <BotIcon className={styles["user-avtar"]} />;
return (
<div className="no-dark">
<BotIcon className={styles["user-avtar"]} />
</div>
);
}
return (
@@ -312,22 +328,90 @@ function useScrollToBottom() {
// for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
const scrollToBottom = () => {
const dom = scrollRef.current;
if (dom) {
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
}
};
// auto scroll
useLayoutEffect(() => {
const dom = scrollRef.current;
if (dom && autoScroll) {
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
}
autoScroll && scrollToBottom();
});
return {
scrollRef,
autoScroll,
setAutoScroll,
scrollToBottom,
};
}
export function ChatActions(props: {
showPromptModal: () => void;
scrollToBottom: () => void;
hitBottom: boolean;
}) {
const chatStore = useChatStore();
// switch themes
const theme = chatStore.config.theme;
function nextTheme() {
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
const themeIndex = themes.indexOf(theme);
const nextIndex = (themeIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
chatStore.updateConfig((config) => (config.theme = nextTheme));
}
// stop all responses
const couldStop = ControllerPool.hasPending();
const stopAll = () => ControllerPool.stopAll();
return (
<div className={chatStyle["chat-input-actions"]}>
{couldStop && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={stopAll}
>
<StopIcon />
</div>
)}
{!props.hitBottom && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.scrollToBottom}
>
<BottomIcon />
</div>
)}
{props.hitBottom && (
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptModal}
>
<BrainIcon />
</div>
)}
<div
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={nextTheme}
>
{theme === Theme.Auto ? (
<AutoIcon />
) : theme === Theme.Light ? (
<LightIcon />
) : theme === Theme.Dark ? (
<DarkIcon />
) : null}
</div>
</div>
);
}
export function Chat(props: {
showSideBar?: () => void;
sideBarShowing?: boolean;
@@ -343,9 +427,10 @@ export function Chat(props: {
const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState("");
const [beforeInput, setBeforeInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const { scrollRef, setAutoScroll } = useScrollToBottom();
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(false);
const onChatBodyScroll = (e: HTMLElement) => {
@@ -370,20 +455,30 @@ export function Chat(props: {
inputRef.current?.focus();
};
const scrollInput = () => {
const dom = inputRef.current;
if (!dom) return;
const paddingBottomNum: number = parseInt(
window.getComputedStyle(dom).paddingBottom,
10,
);
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
};
// auto grow input
const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback(
() => {
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
const inputRows = Math.min(
5,
Math.max(2 + Number(!isMobileScreen()), rows),
);
setInputRows(inputRows);
},
100,
{
leading: true,
trailing: true,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(measure, [userInput]);
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => {
scrollInput();
setUserInput(text);
const n = text.trim().length;
@@ -394,9 +489,6 @@ export function Chat(props: {
// check if need to trigger auto completion
if (text.startsWith("/")) {
let searchText = text.slice(1);
if (searchText.length === 0) {
searchText = " ";
}
onSearch(searchText);
}
}
@@ -407,6 +499,7 @@ export function Chat(props: {
if (userInput.length <= 0) return;
setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
setBeforeInput(userInput);
setUserInput("");
setPromptHints([]);
if (!isMobileScreen()) inputRef.current?.focus();
@@ -420,6 +513,12 @@ export function Chat(props: {
// check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// if ArrowUp and no userInput
if (e.key === "ArrowUp" && userInput.length <= 0) {
setUserInput(beforeInput);
e.preventDefault();
return;
}
if (shouldSubmit(e)) {
onUserSubmit();
e.preventDefault();
@@ -437,32 +536,61 @@ export function Chat(props: {
}
};
const onResend = (botIndex: number) => {
const findLastUesrIndex = (messageId: number) => {
// find last user input message and resend
for (let i = botIndex; i >= 0; i -= 1) {
if (messages[i].role === "user") {
setIsLoading(true);
chatStore
.onUserInput(messages[i].content)
.then(() => setIsLoading(false));
chatStore.updateCurrentSession((session) =>
session.messages.splice(i, 2),
);
inputRef.current?.focus();
return;
let lastUserMessageIndex: number | null = null;
for (let i = 0; i < session.messages.length; i += 1) {
const message = session.messages[i];
if (message.id === messageId) {
break;
}
if (message.role === "user") {
lastUserMessageIndex = i;
}
}
return lastUserMessageIndex;
};
const deleteMessage = (userIndex: number) => {
chatStore.updateCurrentSession((session) =>
session.messages.splice(userIndex, 2),
);
};
const onDelete = (botMessageId: number) => {
const userIndex = findLastUesrIndex(botMessageId);
if (userIndex === null) return;
deleteMessage(userIndex);
};
const onResend = (botMessageId: number) => {
// find last user input message and resend
const userIndex = findLastUesrIndex(botMessageId);
if (userIndex === null) return;
setIsLoading(true);
const content = session.messages[userIndex].content;
deleteMessage(userIndex);
chatStore.onUserInput(content).then(() => setIsLoading(false));
inputRef.current?.focus();
};
const config = useChatStore((state) => state.config);
const context: RenderMessage[] = session.context.slice();
const accessStore = useAccessStore();
if (
context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content
) {
context.push(BOT_HELLO);
const copiedHello = Object.assign({}, BOT_HELLO);
if (!accessStore.isAuthorized()) {
copiedHello.content = Locale.Error.Unauthorized;
}
context.push(copiedHello);
}
// preview messages
@@ -497,6 +625,13 @@ export function Chat(props: {
const [showPromptModal, setShowPromptModal] = useState(false);
const renameSession = () => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
}
};
// Auto focus
useEffect(() => {
if (props.sideBarShowing && isMobileScreen()) return;
@@ -510,14 +645,7 @@ export function Chat(props: {
<div className={styles["window-header-title"]}>
<div
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClickCapture={() => {
const newTopic = prompt(Locale.Chat.Rename, session.topic);
if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession(
(session) => (session.topic = newTopic!),
);
}
}}
onClickCapture={renameSession}
>
{session.topic}
</div>
@@ -536,12 +664,9 @@ export function Chat(props: {
</div>
<div className={styles["window-action-button"]}>
<IconButton
icon={<BrainIcon />}
icon={<RenameIcon />}
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
setShowPromptModal(true);
}}
onClick={renameSession}
/>
</div>
<div className={styles["window-action-button"]}>
@@ -557,6 +682,19 @@ export function Chat(props: {
}}
/>
</div>
{!isMobileScreen() && (
<div className={styles["window-action-button"]}>
<IconButton
icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {
chatStore.updateConfig(
(config) => (config.tightBorder = !config.tightBorder),
);
}}
/>
</div>
)}
</div>
<PromptToast
@@ -607,12 +745,20 @@ export function Chat(props: {
{Locale.Chat.Actions.Stop}
</div>
) : (
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(i)}
>
{Locale.Chat.Actions.Retry}
</div>
<>
<div
className={styles["chat-message-top-action"]}
onClick={() => onDelete(message.id ?? i)}
>
{Locale.Chat.Actions.Delete}
</div>
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(message.id ?? i)}
>
{Locale.Chat.Actions.Retry}
</div>
</>
)}
<div
@@ -623,22 +769,20 @@ export function Chat(props: {
</div>
</div>
)}
{(message.preview || message.content.length === 0) &&
!isUser ? (
<LoadingIcon />
) : (
<div
className="markdown-body"
style={{ fontSize: `${fontSize}px` }}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen()) return;
setUserInput(message.content);
}}
>
<Markdown content={message.content} />
</div>
)}
<Markdown
content={message.content}
loading={
(message.preview || message.content.length === 0) &&
!isUser
}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen()) return;
setUserInput(message.content);
}}
fontSize={fontSize}
parentRef={scrollRef}
/>
</div>
{!isUser && !message.preview && (
<div className={styles["chat-message-actions"]}>
@@ -655,12 +799,17 @@ export function Chat(props: {
<div className={styles["chat-input-panel"]}>
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
<ChatActions
showPromptModal={() => setShowPromptModal(true)}
scrollToBottom={scrollToBottom}
hitBottom={hitBottom}
/>
<div className={styles["chat-input-panel-inner"]}>
<textarea
ref={inputRef}
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
rows={2}
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}
@@ -670,6 +819,7 @@ export function Chat(props: {
setTimeout(() => setPromptHints([]), 500);
}}
autoFocus={!props?.sideBarShowing}
rows={inputRows}
/>
<IconButton
icon={<SendWhiteIcon />}

View File

@@ -10,7 +10,7 @@
background-color: var(--white);
min-width: 600px;
min-height: 480px;
max-width: 900px;
max-width: 1200px;
display: flex;
overflow: hidden;
@@ -36,6 +36,7 @@
max-height: var(--full-height);
border-radius: 0;
border: 0;
}
}
@@ -48,6 +49,27 @@
display: flex;
flex-direction: column;
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
position: relative;
transition: width ease 0.1s;
}
.sidebar-drag {
$width: 10px;
position: absolute;
top: 0;
right: 0;
height: 100%;
width: $width;
background-color: var(--black);
cursor: ew-resize;
opacity: 0;
transition: all ease 0.3s;
&:hover,
&:active {
opacity: 0.2;
}
}
.window-content {
@@ -177,10 +199,11 @@
margin-top: 8px;
}
.chat-item-count {
}
.chat-item-count,
.chat-item-date {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar-tail {
@@ -208,6 +231,7 @@
flex: 1;
overflow: auto;
padding: 20px;
padding-bottom: 40px;
position: relative;
}
@@ -332,11 +356,16 @@
}
.chat-input-panel {
position: relative;
width: 100%;
padding: 20px;
padding-top: 5px;
padding-top: 10px;
box-sizing: border-box;
flex-direction: column;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-top: var(--border-in-light);
box-shadow: var(--card-shadow);
}
@mixin single-line {
@@ -406,17 +435,11 @@
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 14px 50px;
padding: 10px 90px 10px 14px;
resize: none;
outline: none;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
}
.chat-input:focus {
border: 1px solid var(--primary);
}
@@ -427,11 +450,22 @@
position: absolute;
right: 30px;
bottom: 30px;
bottom: 32px;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
.chat-input-send {
bottom: 30px;
}
}
.export-content {
white-space: break-spaces;
padding: 10px !important;
}
.loading-content {

View File

@@ -2,7 +2,7 @@
require("../polyfill");
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { IconButton } from "./button";
import styles from "./home.module.scss";
@@ -17,7 +17,7 @@ import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import { useChatStore } from "../store";
import { isMobileScreen } from "../utils";
import { getCSSVar, isMobileScreen } from "../utils";
import Locale from "../locales";
import { Chat } from "./chat";
@@ -66,15 +66,60 @@ function useSwitchTheme() {
metaDescriptionDark?.setAttribute("content", "#151515");
metaDescriptionLight?.setAttribute("content", "#fafafa");
} else {
const themeColor = getComputedStyle(document.body)
.getPropertyValue("--theme-color")
.trim();
const themeColor = getCSSVar("--themeColor");
metaDescriptionDark?.setAttribute("content", themeColor);
metaDescriptionLight?.setAttribute("content", themeColor);
}
}, [config.theme]);
}
function useDragSideBar() {
const limit = (x: number) => Math.min(500, Math.max(220, x));
const chatStore = useChatStore();
const startX = useRef(0);
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 100) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = chatStore.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);
};
useEffect(() => {
if (isMobileScreen()) {
return;
}
document.documentElement.style.setProperty(
"--sidebar-width",
`${limit(chatStore.config.sidebarWidth ?? 300)}px`,
);
}, [chatStore.config.sidebarWidth]);
return {
onDragMouseDown,
};
}
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
@@ -101,6 +146,9 @@ function _Home() {
const [openSettings, setOpenSettings] = useState(false);
const config = useChatStore((state) => state.config);
// drag side bar
const { onDragMouseDown } = useDragSideBar();
useSwitchTheme();
if (loading) {
@@ -174,6 +222,11 @@ function _Home() {
/>
</div>
</div>
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
></div>
</div>
<div className={styles["window-content"]}>

View File

@@ -8,6 +8,8 @@ import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { copyToClipboard } from "../utils";
import LoadingIcon from "../icons/three-dots.svg";
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
@@ -27,49 +29,78 @@ export function PreCode(props: { children: any }) {
);
}
const useLazyLoad = (ref: RefObject<Element>): boolean => {
const [isIntersecting, setIntersecting] = useState<boolean>(false);
export function Markdown(
props: {
content: string;
loading?: boolean;
fontSize?: number;
parentRef: RefObject<HTMLDivElement>;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
const parent = props.parentRef.current;
const md = mdRef.current;
const rendered = useRef(true); // disable lazy loading for bad ux
const [counter, setCounter] = useState(0);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIntersecting(true);
observer.disconnect();
// to triggr rerender
setCounter(counter + 1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.loading]);
const inView =
rendered.current ||
(() => {
if (parent && md) {
const parentBounds = parent.getBoundingClientRect();
const mdBounds = md.getBoundingClientRect();
const isInRange = (x: number) =>
x <= parentBounds.bottom && x >= parentBounds.top;
const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
if (inView) {
rendered.current = true;
}
return inView;
}
});
})();
if (ref.current) {
observer.observe(ref.current);
}
const shouldLoading = props.loading || !inView;
return () => {
observer.disconnect();
};
}, [ref]);
return isIntersecting;
};
export function Markdown(props: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
}}
linkTarget={'_blank'}
<div
className="markdown-body"
style={{ fontSize: `${props.fontSize ?? 14}px` }}
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
>
{props.content}
</ReactMarkdown>
{shouldLoading ? (
<LoadingIcon />
) : (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
rehypePlugins={[
RehypeKatex,
[
RehypeHighlight,
{
detect: false,
ignoreMissing: true,
},
],
]}
components={{
pre: PreCode,
}}
linkTarget={"_blank"}
>
{props.content}
</ReactMarkdown>
)}
</div>
);
}

View File

@@ -19,11 +19,76 @@
cursor: pointer;
}
.password-input {
.password-input-container {
max-width: 50%;
display: flex;
justify-content: flex-end;
.password-eye {
margin-right: 4px;
}
.password-input {
min-width: 80%;
}
}
.user-prompt-modal {
min-height: 40vh;
.user-prompt-search {
width: 100%;
max-width: 100%;
margin-bottom: 10px;
background-color: var(--gray);
}
.user-prompt-list {
padding: 10px 0;
.user-prompt-item {
margin-bottom: 10px;
widows: 100%;
.user-prompt-header {
display: flex;
widows: 100%;
margin-bottom: 5px;
.user-prompt-title {
flex-grow: 1;
max-width: 100%;
margin-right: 5px;
padding: 5px;
font-size: 12px;
text-align: left;
}
.user-prompt-buttons {
display: flex;
align-items: center;
.user-prompt-button {
height: 100%;
&:not(:last-child) {
margin-right: 5px;
}
}
}
}
.user-prompt-content {
width: 100%;
box-sizing: border-box;
padding: 5px;
margin-right: 10px;
font-size: 12px;
flex-grow: 1;
}
}
}
.user-prompt-actions {
}
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, HTMLProps } from "react";
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
@@ -6,12 +6,13 @@ import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import { List, ListItem, Popover, showToast } from "./ui-lib";
import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
import { IconButton } from "./button";
import {
@@ -26,14 +27,114 @@ import {
import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { getCurrentVersion, getEmojiUrl } from "../utils";
import { copyToClipboard, getEmojiUrl } from "../utils";
import Link from "next/link";
import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt";
import { requestUsage } from "../requests";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore();
const userPrompts = promptStore.getUserPrompts();
const builtinPrompts = SearchService.builtinPrompts;
const allPrompts = userPrompts.concat(builtinPrompts);
const [searchInput, setSearchInput] = useState("");
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
useEffect(() => {
if (searchInput.length > 0) {
const searchResult = SearchService.search(searchInput);
setSearchPrompts(searchResult);
} else {
setSearchPrompts([]);
}
}, [searchInput]);
return (
<div className="modal-mask">
<Modal
title={Locale.Settings.Prompt.Modal.Title}
onClose={() => props.onClose?.()}
actions={[
<IconButton
key="add"
onClick={() => promptStore.add({ title: "", content: "" })}
icon={<ClearIcon />}
bordered
text={Locale.Settings.Prompt.Modal.Add}
/>,
]}
>
<div className={styles["user-prompt-modal"]}>
<input
type="text"
className={styles["user-prompt-search"]}
placeholder={Locale.Settings.Prompt.Modal.Search}
value={searchInput}
onInput={(e) => setSearchInput(e.currentTarget.value)}
></input>
<div className={styles["user-prompt-list"]}>
{prompts.map((v, _) => (
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
<div className={styles["user-prompt-header"]}>
<input
type="text"
className={styles["user-prompt-title"]}
value={v.title}
readOnly={!v.isUser}
onChange={(e) => {
if (v.isUser) {
promptStore.updateUserPrompts(
v.id!,
(prompt) => (prompt.title = e.currentTarget.value),
);
}
}}
></input>
<div className={styles["user-prompt-buttons"]}>
{v.isUser && (
<IconButton
icon={<ClearIcon />}
bordered
className={styles["user-prompt-button"]}
onClick={() => promptStore.remove(v.id!)}
/>
)}
<IconButton
icon={<CopyIcon />}
bordered
className={styles["user-prompt-button"]}
onClick={() => copyToClipboard(v.content)}
/>
</div>
</div>
<Input
rows={2}
value={v.content}
className={styles["user-prompt-content"]}
readOnly={!v.isUser}
onChange={(e) => {
if (v.isUser) {
promptStore.updateUserPrompts(
v.id!,
(prompt) => (prompt.content = e.currentTarget.value),
);
}
}}
/>
</div>
))}
</div>
</div>
</Modal>
</div>
);
}
function SettingItem(props: {
title: string;
subTitle?: string;
@@ -60,13 +161,17 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
}
return (
<div className={styles["password-input"]}>
<div className={styles["password-input-container"]}>
<IconButton
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
onClick={changeVisibility}
className={styles["password-eye"]}
/>
<input {...props} type={visible ? "text" : "password"} />
<input
{...props}
type={visible ? "text" : "password"}
className={styles["password-input"]}
/>
</div>
);
}
@@ -84,29 +189,27 @@ export function Settings(props: { closeSettings: () => void }) {
const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false);
const currentId = getCurrentVersion();
const remoteId = updateStore.remoteId;
const hasNewVersion = currentId !== remoteId;
const currentVersion = updateStore.version;
const remoteId = updateStore.remoteVersion;
const hasNewVersion = currentVersion !== remoteId;
function checkUpdate(force = false) {
setCheckingUpdate(true);
updateStore.getLatestCommitId(force).then(() => {
updateStore.getLatestVersion(force).then(() => {
setCheckingUpdate(false);
});
}
const [usage, setUsage] = useState<{
used?: number;
subscription?: number;
}>();
const usage = {
used: updateStore.used,
subscription: updateStore.subscription,
};
const [loadingUsage, setLoadingUsage] = useState(false);
function checkUsage() {
setLoadingUsage(true);
requestUsage()
.then((res) => setUsage(res))
.finally(() => {
setLoadingUsage(false);
});
updateStore.updateUsage().finally(() => {
setLoadingUsage(false);
});
}
const accessStore = useAccessStore();
@@ -118,16 +221,30 @@ export function Settings(props: { closeSettings: () => void }) {
const promptStore = usePromptStore();
const builtinCount = SearchService.count.builtin;
const customCount = promptStore.prompts.size ?? 0;
const showUsage = !!accessStore.token || !!accessStore.accessCode;
const customCount = promptStore.getUserPrompts().length ?? 0;
const [shouldShowPromptModal, setShowPromptModal] = useState(false);
const showUsage = accessStore.isAuthorized();
useEffect(() => {
// checks per minutes
checkUpdate();
showUsage && checkUsage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const keydownEvent = (e: KeyboardEvent) => {
if (e.key === "Escape") {
props.closeSettings();
}
};
document.addEventListener("keydown", keydownEvent);
return () => {
document.removeEventListener("keydown", keydownEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ErrorBoundary>
<div className={styles["window-header"]}>
@@ -143,7 +260,14 @@ export function Settings(props: { closeSettings: () => void }) {
<div className={styles["window-action-button"]}>
<IconButton
icon={<ClearIcon />}
onClick={clearSessions}
onClick={() => {
const confirmed = window.confirm(
`${Locale.Settings.Actions.ConfirmClearAll.Confirm}`,
);
if (confirmed) {
clearSessions();
}
}}
bordered
title={Locale.Settings.Actions.ClearAll}
/>
@@ -151,7 +275,14 @@ export function Settings(props: { closeSettings: () => void }) {
<div className={styles["window-action-button"]}>
<IconButton
icon={<ResetIcon />}
onClick={resetConfig}
onClick={() => {
const confirmed = window.confirm(
`${Locale.Settings.Actions.ConfirmResetAll.Confirm}`,
);
if (confirmed) {
resetConfig();
}
}}
bordered
title={Locale.Settings.Actions.ResetAll}
/>
@@ -194,7 +325,7 @@ export function Settings(props: { closeSettings: () => void }) {
</SettingItem>
<SettingItem
title={Locale.Settings.Update.Version(currentId)}
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
subTitle={
checkingUpdate
? Locale.Settings.Update.IsChecking
@@ -315,37 +446,7 @@ export function Settings(props: { closeSettings: () => void }) {
></input>
</SettingItem>
</List>
<List>
<SettingItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => showToast(Locale.WIP)}
/>
</SettingItem>
</List>
<List>
{enabledAccessControl ? (
<SettingItem
@@ -442,6 +543,38 @@ export function Settings(props: { closeSettings: () => void }) {
</SettingItem>
</List>
<List>
<SettingItem
title={Locale.Settings.Prompt.Disable.Title}
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
updateConfig(
(config) =>
(config.disablePromptHint = e.currentTarget.checked),
)
}
></input>
</SettingItem>
<SettingItem
title={Locale.Settings.Prompt.List}
subTitle={Locale.Settings.Prompt.ListCount(
builtinCount,
customCount,
)}
>
<IconButton
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => setShowPromptModal(true)}
/>
</SettingItem>
</List>
<List>
<SettingItem title={Locale.Settings.Model}>
<select
@@ -523,6 +656,10 @@ export function Settings(props: { closeSettings: () => void }) {
></InputRange>
</SettingItem>
</List>
{shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} />
)}
</div>
</ErrorBoundary>
);

View File

@@ -9,6 +9,7 @@
.popover {
position: relative;
z-index: 2;
}
.popover-content {
@@ -52,7 +53,7 @@
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
width: 50vw;
width: 60vw;
animation: slide-in ease 0.3s;
--modal-padding: 20px;
@@ -126,6 +127,7 @@
width: 100vw;
display: flex;
justify-content: center;
pointer-events: none;
.toast-content {
max-width: 80vw;
@@ -140,6 +142,7 @@
margin-bottom: 20px;
display: flex;
align-items: center;
pointer-events: all;
.toast-action {
padding-left: 20px;

27
app/config/build.ts Normal file
View File

@@ -0,0 +1,27 @@
const COMMIT_ID: string = (() => {
try {
const childProcess = require("child_process");
return (
childProcess
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim()
);
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
}
})();
export const getBuildConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
commitId: COMMIT_ID,
};
};

42
app/config/server.ts Normal file
View File

@@ -0,0 +1,42 @@
import md5 from "spark-md5";
declare global {
namespace NodeJS {
interface ProcessEnv {
OPENAI_API_KEY?: string;
CODE?: string;
PROXY_URL?: string;
VERCEL?: string;
}
}
}
const ACCESS_CODES = (function getAccessCodes(): Set<string> {
const code = process.env.CODE;
try {
const codes = (code?.split(",") ?? [])
.filter((v) => !!v)
.map((v) => md5.hash(v.trim()));
return new Set(codes);
} catch (e) {
return new Set();
}
})();
export const getServerSideConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
apiKey: process.env.OPENAI_API_KEY,
code: process.env.CODE,
codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
};
};

View File

@@ -5,3 +5,4 @@ export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
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";

11
app/global.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
declare module "*.jpg";
declare module "*.png";
declare module "*.woff2";
declare module "*.woff";
declare module "*.ttf";
declare module "*.scss" {
const content: Record<string, string>;
export default content;
}
declare module "*.svg";

1
app/icons/auto.svg Normal file
View 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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="分组 1" style="stroke:#333333; stroke-width:1; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.333333333333333) rotate(0 2.333750009536743 2.6666666666666665)" d="M0 5.33667L0.73 3.66667 M4.6675 5.33667L3.9375 3.66667 M0.729167 3.67L2.32917 0L3.93917 3.67 M0.729167 3.66667L3.93917 3.66667 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.533316666666666 2.6666666666666665)" d="M13.07,5.33C12.45,2.29 9.76,0 6.53,0C3.31,0 0.62,2.29 0,5.33L2,4.67 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 9.333333333333332) rotate(0 6.533316666666666 2.6666666666666665)" d="M0,0C0.62,3.04 3.31,5.33 6.53,5.33C9.76,5.33 12.45,3.04 13.07,0L11.33,0.67 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
app/icons/bottom.svg Normal file
View 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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 4) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 8) rotate(0 4 2)" d="M8,0L4,4L0,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 736 B

1
app/icons/dark.svg Normal file
View 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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.333333333333485) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0L4.91,1.76L1.76,1.76L1.76,4.91L0,6.67L1.76,8.42L1.76,11.58L4.91,11.58L6.67,13.33L8.42,11.58L11.58,11.58L11.58,8.42L13.33,6.67L11.58,4.91L11.58,1.76L8.42,1.76L6.67,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.44771525016904) rotate(0 2.4732087011352872 2.442809041582063)" d="M4,0.55C2.17,-0.78 0,0.55 0,1.89C1.67,1.89 3.33,2.22 3.33,4.89C4.67,4.89 5.83,1.89 4,0.55Z " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
app/icons/light.svg Normal file
View 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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4.340166666666667 4.21550000000002) rotate(0 3.6666666666666665 3.666666666666666)" d="M0,3.67C0,5.69 1.64,7.33 3.67,7.33C5.69,7.33 7.33,5.69 7.33,3.67C7.33,1.64 5.69,0 3.67,0C1.64,0 0,1.64 0,3.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.166666666666666 12.1719333333333) rotate(0 0.4100499999999994 0.41240499999999997)" d="M0.82,0.82L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.0068366666666666 3.0654333333332033) rotate(0 0.3411483333333332 0.34309999999999974)" d="M0.68,0.69L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 1.2155666666667457) rotate(0 0 0.5)" d="M0,1L0,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(13.333266666666667 8.21550000000002) rotate(0 0.6666666666666666 0)" d="M1.33,0L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.5108 3.065499999999929) rotate(0 0.41123333333333295 0.41123333333333295)" d="M0,0.82L0.82,0 " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(5.673499999999999 5.5488333333332776) rotate(0 1.1666666666666665 2.333333333333333)" opacity="1" d="M2.33,0C1.04,0 0,1.04 0,2.33C0,3.62 1.04,4.67 2.33,4.67L2.33,0Z " /><path id="路径 8" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333966666666666 7.8821666666667625) rotate(0 0.6666666666666666 0)" d="M0,0L1.33,0 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.348133333333333 12.3125) rotate(0 0.3421333333333335 0.3421266666666665)" d="M0,0.68L0.68,0 " /><path id="路径 10" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 13.548763333333454) rotate(0 0 0.6666666666666666)" d="M0,1.33L0,0 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

41
app/icons/max.svg Normal file
View File

@@ -0,0 +1,41 @@
<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">
<defs>
<rect id="path_0" x="0" y="0" width="16" height="16" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<path id="路径 1"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2) rotate(0 1.6666666666666665 1.6499166666666665)"
d="M0,0L3.33,3.3 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
d="M0,3.3L3.33,0 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
d="M3.3,3.3L0,0 " />
<path id="路径 4"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
d="M3.3,0L0,3.3 " />
<path id="路径 5"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(11 2) rotate(0 1.5 1.5)" d="M0,0L3,0L3,3 " />
<path id="路径 6"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(11 11) rotate(0 1.5 1.5)" d="M3,0L3,3L0,3 " />
<path id="路径 7"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 11) rotate(0 1.5 1.5)" d="M3,3L0,3L0,0 " />
<path id="路径 8"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2) rotate(0 1.5 1.5)" d="M0,3L0,0L3,0 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

45
app/icons/min.svg Normal file
View File

@@ -0,0 +1,45 @@
<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">
<defs>
<rect id="path_0" x="0" y="0" width="16" height="16" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<path id="路径 1"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 2) rotate(0 1.6666666666666665 1.6499166666666665)"
d="M0,0L3.33,3.3 " />
<path id="路径 2"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 10.666666666666666) rotate(0 1.6666666666666665 1.6499166666666671)"
d="M0,3.3L3.33,0 " />
<path id="路径 3"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.700199999999999 10.666666666666666) rotate(0 1.6499166666666671 1.6499166666666671)"
d="M3.3,3.3L0,0 " />
<path id="路径 4"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2) rotate(0 1.6499166666666671 1.6499166666666665)"
d="M3.3,0L0,3.3 " />
<path id="路径 5"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 2.333333333333333) rotate(0 1.5 1.5)"
d="M0,0L0,3L3,3 " />
<path id="路径 6"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2.333333333333333 2.333333333333333) rotate(0 1.5 1.5)"
d="M3,0L3,3L0,3 " />
<path id="路径 7"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2.333333333333333 10.666666666666666) rotate(0 1.5 1.5)"
d="M3,3L3,0L0,0 " />
<path id="路径 8"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(10.666666666666666 10.666666666666666) rotate(0 1.4832500000000004 1.5)"
d="M0,3L0,0L2.97,0 " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

1
app/icons/pause.svg Normal file
View 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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M13.33,6.67C13.33,2.98 10.35,0 6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 6) rotate(0 0 2)" d="M0,0L0,4 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666666666666666 6) rotate(0 0 2)" d="M0,0L0,4 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
app/icons/rename.svg Normal file
View 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"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.774903333333333 1.3006199999999999) rotate(0 6.599664999999999 6.599656666666666)" d="M2.83,13.2L13.2,2.83L10.37,0L0,10.37L0,13.2L2.83,13.2Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.317366666666667 4.129066666666667) rotate(0 1.4142166666666658 1.4142166666666665)" d="M0,0L2.83,2.83 " /></g></g></svg>

After

Width:  |  Height:  |  Size: 911 B

17
app/icons/share.svg Normal file
View File

@@ -0,0 +1,17 @@
<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">
<defs>
<rect id="path_0" x="0" y="0" width="16" height="16" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="white">
<use xlink:href="#path_0"></use>
</mask>
<g mask="url(#bg-mask-0)">
<path id="路径 1"
style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
transform="translate(2 1.3333333333333333) rotate(0 6.333333333333333 6.5)"
d="M6.67,3.67C1.67,3.67 0,7.33 0,13C0,13 2,8 6.67,8L6.67,11.67L12.67,6L6.67,0L6.67,3.67Z " />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 740 B

View File

@@ -2,19 +2,9 @@
import "./styles/globals.scss";
import "./styles/markdown.scss";
import "./styles/highlight.scss";
import process from "child_process";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
import { getBuildConfig } from "./config/build";
let COMMIT_ID: string | undefined;
try {
COMMIT_ID = process
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch (e) {
console.error("No git or not from git repo.");
}
const buildConfig = getBuildConfig();
export const metadata = {
title: "ChatGPT Next Web",
@@ -26,21 +16,6 @@ export const metadata = {
themeColor: "#fafafa",
};
function Meta() {
const metas = {
version: COMMIT_ID ?? "unknown",
access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
};
return (
<>
{Object.entries(metas).map(([k, v]) => (
<meta name={k} content={v} key={k} />
))}
</>
);
}
export default function RootLayout({
children,
}: {
@@ -58,7 +33,7 @@ export default function RootLayout({
content="#151515"
media="(prefers-color-scheme: dark)"
/>
<Meta />
<meta name="version" content={buildConfig.commitId} />
<link rel="manifest" href="/site.webmanifest"></link>
<link rel="preconnect" href="https://fonts.googleapis.com"></link>
<link rel="preconnect" href="https://fonts.gstatic.com"></link>

View File

@@ -3,7 +3,7 @@ import { SubmitKey } from "../store/app";
const cn = {
WIP: "该功能仍在开发中……",
Error: {
Unauthorized: "现在是未授权状态,请在设置页输入访问密码。",
Unauthorized: "现在是未授权状态,请点击左下角设置按钮输入访问密码。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
@@ -17,15 +17,16 @@ const cn = {
Copy: "复制",
Stop: "停止",
Retry: "重试",
Delete: "删除",
},
Rename: "重命名对话",
Typing: "正在输入…",
Input: (submitKey: string) => {
var inputHints = `输入消息,${submitKey} 发送`;
var inputHints = `${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行";
}
return inputHints;
return inputHints + "/ 触发补全";
},
Send: "发送",
},
@@ -57,6 +58,12 @@ const cn = {
ClearAll: "清除所有数据",
ResetAll: "重置所有选项",
Close: "关闭",
ConfirmResetAll: {
Confirm: "确认清除所有配置?",
},
ConfirmClearAll: {
Confirm: "确认清除所有聊天记录?",
},
},
Lang: {
Name: "Language",
@@ -66,6 +73,9 @@ const cn = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "头像",
@@ -84,7 +94,7 @@ const cn = {
},
SendKey: "发送键",
Theme: "主题",
TightBorder: "紧凑边框",
TightBorder: "无边框模式",
SendPreviewBubble: "发送预览气泡",
Prompt: {
Disable: {
@@ -95,6 +105,11 @@ const cn = {
ListCount: (builtin: number, custom: number) =>
`内置 ${builtin} 条,用户定义 ${custom}`,
Edit: "编辑",
Modal: {
Title: "提示词列表",
Add: "增加一条",
Search: "搜尋提示詞",
},
},
HistoryCount: {
Title: "附带历史消息数",
@@ -120,13 +135,13 @@ const cn = {
},
AccessCode: {
Title: "访问密码",
SubTitle: "现在是未授权访问状态",
SubTitle: "已开启加密访问",
Placeholder: "请输入访问密码",
},
Model: "模型 (model)",
Temperature: {
Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机",
SubTitle: "值越大,回复越随机,大于 1 的值可能会导致乱码",
},
MaxTokens: {
Title: "单次回复限制 (max_tokens)",

189
app/locales/de.ts Normal file
View File

@@ -0,0 +1,189 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const de: LocaleType = {
WIP: "In Bearbeitung...",
Error: {
Unauthorized:
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} Nachrichten`,
},
Chat: {
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
Actions: {
ChatList: "Zur Chat-Liste gehen",
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
Export: "Alle Nachrichten als Markdown exportieren",
Copy: "Kopieren",
Stop: "Stop",
Retry: "Wiederholen",
Delete: "Delete",
},
Rename: "Chat umbenennen",
Typing: "Tippen...",
Input: (submitKey: string) => {
var inputHints = `${submitKey} um zu Senden`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
}
return inputHints + ", / zum Durchsuchen von Prompts";
},
Send: "Senden",
},
Export: {
Title: "Alle Nachrichten",
Copy: "Alles kopieren",
Download: "Herunterladen",
MessageFromYou: "Deine Nachricht",
MessageFromChatGPT: "Nachricht von ChatGPT",
},
Memory: {
Title: "Gedächtnis-Prompt",
EmptyContent: "Noch nichts.",
Send: "Gedächtnis senden",
Copy: "Gedächtnis kopieren",
Reset: "Sitzung zurücksetzen",
ResetConfirm:
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
},
Home: {
NewChat: "Neuer Chat",
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
DeleteToast: "Chat gelöscht",
Revert: "Zurücksetzen",
},
Settings: {
Title: "Einstellungen",
SubTitle: "Alle Einstellungen",
Actions: {
ClearAll: "Alle Daten löschen",
ResetAll: "Alle Einstellungen zurücksetzen",
Close: "Schließen",
ConfirmResetAll: {
Confirm: "Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
},
ConfirmClearAll: {
Confirm: "Möchten Sie wirklich alle Chats zurücksetzen?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Schriftgröße",
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Neueste Version",
CheckUpdate: "Update prüfen",
IsChecking: "Update wird geprüft...",
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
GoToUpdate: "Aktualisieren",
},
SendKey: "Senden-Taste",
Theme: "Erscheinungsbild",
TightBorder: "Enger Rahmen",
SendPreviewBubble: "Vorschau-Bubble senden",
Prompt: {
Disable: {
Title: "Autovervollständigung deaktivieren",
SubTitle: "Autovervollständigung mit / starten",
},
List: "Prompt-Liste",
ListCount: (builtin: number, custom: number) =>
`${builtin} integriert, ${custom} benutzerdefiniert`,
Edit: "Bearbeiten",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Anzahl der angehängten Nachrichten",
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
},
CompressThreshold: {
Title: "Schwellenwert für Verlaufskomprimierung",
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) {
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
},
IsChecking: "Wird überprüft...",
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
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
},
MaxTokens: {
Title: "Max Tokens", //Maximale Token
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
},
PresencePenlty: {
Title: "Presence Penalty", //Anwesenheitsstrafe
SubTitle:
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
},
},
Store: {
DefaultTopic: "Neues Gespräch",
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
Error:
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
Prompt: {
History: (content: string) =>
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
content,
Topic:
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
Summarize:
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
},
ConfirmClearAll:
"Bestätigen Sie, um alle Chat- und Einstellungsdaten zu löschen?",
},
Copy: {
Success: "In die Zwischenablage kopiert",
Failed:
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
},
Context: {
Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
Edit: "Kontext- und Gedächtnis-Prompts",
Add: "Hinzufügen",
},
};
export default de;

View File

@@ -19,15 +19,16 @@ const en: LocaleType = {
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
Delete: "Delete",
},
Rename: "Rename Chat",
Typing: "Typing…",
Input: (submitKey: string) => {
var inputHints = `Type something and press ${submitKey} to send`;
var inputHints = `${submitKey} to send`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", press Shift + Enter to newline";
inputHints += ", Shift + Enter to wrap";
}
return inputHints;
return inputHints + ", / to search prompts";
},
Send: "Send",
},
@@ -60,6 +61,12 @@ const en: LocaleType = {
ClearAll: "Clear All Data",
ResetAll: "Reset All Settings",
Close: "Close",
ConfirmResetAll: {
Confirm: "Are you sure you want to reset all configurations?",
},
ConfirmClearAll: {
Confirm: "Are you sure you want to reset all chat?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
@@ -69,6 +76,9 @@ const en: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -97,6 +107,11 @@ const en: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Edit",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Attached Messages Count",
@@ -118,7 +133,7 @@ const en: LocaleType = {
return `Used this month $${used}, subscription $${total}`;
},
IsChecking: "Checking...",
Check: "Check Again",
Check: "Check",
NoAccess: "Enter API Key to check balance",
},
AccessCode: {

View File

@@ -19,6 +19,7 @@ const es: LocaleType = {
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
Delete: "Delete",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
@@ -60,6 +61,12 @@ const es: LocaleType = {
ClearAll: "Borrar todos los datos",
ResetAll: "Restablecer todas las configuraciones",
Close: "Cerrar",
ConfirmResetAll: {
Confirm: "Are you sure you want to reset all configurations?",
},
ConfirmClearAll: {
Confirm: "Are you sure you want to reset all chat?",
},
},
Lang: {
Name: "Language",
@@ -69,6 +76,9 @@ const es: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -97,6 +107,11 @@ const es: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} incorporado, ${custom} definido por el usuario`,
Edit: "Editar",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Cantidad de mensajes adjuntos",

View File

@@ -3,10 +3,22 @@ import EN from "./en";
import TW from "./tw";
import ES from "./es";
import IT from "./it";
import TR from "./tr";
import JP from "./jp";
import DE from "./de";
export type { LocaleType } from "./cn";
export const AllLangs = ["en", "cn", "tw", "es", "it"] as const;
export const AllLangs = [
"en",
"cn",
"tw",
"es",
"it",
"tr",
"jp",
"de",
] as const;
type Lang = (typeof AllLangs)[number];
const LANG_KEY = "lang";
@@ -42,17 +54,13 @@ export function getLang(): Lang {
const lang = getLanguage();
if (lang.includes("zh") || lang.includes("cn")) {
return "cn";
} else if (lang.includes("tw")) {
return "tw";
} else if (lang.includes("es")) {
return "es";
} else if (lang.includes("it")) {
return "it";
} else {
return "en";
for (const option of AllLangs) {
if (lang.includes(option)) {
return option;
}
}
return "en";
}
export function changeLang(lang: Lang) {
@@ -60,4 +68,13 @@ export function changeLang(lang: Lang) {
location.reload();
}
export default { en: EN, cn: CN, tw: TW, es: ES, it: IT }[getLang()];
export default {
en: EN,
cn: CN,
tw: TW,
es: ES,
it: IT,
tr: TR,
jp: JP,
de: DE,
}[getLang()] as typeof CN;

View File

@@ -19,6 +19,7 @@ const it: LocaleType = {
Copy: "Copia",
Stop: "Stop",
Retry: "Riprova",
Delete: "Delete",
},
Rename: "Rinomina Chat",
Typing: "Typing…",
@@ -45,12 +46,12 @@ const it: LocaleType = {
Send: "Send Memory",
Reset: "Reset Session",
ResetConfirm:
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
"Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
},
Home: {
NewChat: "Nuova Chat",
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
DeleteToast: "Chat Deleted",
DeleteToast: "Chat Cancellata",
Revert: "Revert",
},
Settings: {
@@ -60,6 +61,12 @@ const it: LocaleType = {
ClearAll: "Cancella tutti i dati",
ResetAll: "Resetta tutte le impostazioni",
Close: "Chiudi",
ConfirmResetAll: {
Confirm: "Sei sicuro vuoi cancellare tutte le impostazioni?",
},
ConfirmClearAll: {
Confirm: "Sei sicuro vuoi cancellare tutte le chat?",
},
},
Lang: {
Name: "Lingue",
@@ -69,6 +76,9 @@ const it: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
@@ -85,9 +95,9 @@ const it: LocaleType = {
GoToUpdate: "Aggiorna",
},
SendKey: "Tasto invia",
Theme: "tema",
TightBorder: "Bordi stretti",
SendPreviewBubble: "Invia l'anteprima della bolla",
Theme: "Tema",
TightBorder: "Schermo intero",
SendPreviewBubble: "Anteprima di digitazione",
Prompt: {
Disable: {
Title: "Disabilita l'auto completamento",
@@ -97,6 +107,11 @@ const it: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "Modifica",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Conteggio dei messaggi allegati",
@@ -108,7 +123,7 @@ const it: LocaleType = {
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
},
Token: {
Title: "Chiave API",
Title: "API Key",
SubTitle:
"Utilizzare la chiave per ignorare il limite del codice di accesso",
Placeholder: "OpenAI API Key",
@@ -116,7 +131,7 @@ const it: LocaleType = {
Usage: {
Title: "Bilancio Account",
SubTitle(used: any, total: any) {
return `Usato in questo mese $${used}, subscription $${total}`;
return `Attualmente usato in questo mese $${used}, soglia massima $${total}`;
},
IsChecking: "Controllando...",
Check: "Controlla ancora",

187
app/locales/jp.ts Normal file
View File

@@ -0,0 +1,187 @@
import { SubmitKey } from "../store/app";
const jp = {
WIP: "この機能は開発中です……",
Error: {
Unauthorized:
"現在は未承認状態です。左下の設定ボタンをクリックし、アクセスパスワードを入力してください。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 通のチャット`,
},
Chat: {
SubTitle: (count: number) => `ChatGPTとの ${count} 通のチャット`,
Actions: {
ChatList: "メッセージリストを表示",
CompressedHistory: "圧縮された履歴プロンプトを表示",
Export: "チャット履歴をエクスポート",
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
Delete: "Delete",
},
Rename: "チャットの名前を変更",
Typing: "入力中…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} で送信`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter で改行";
}
return inputHints + "/ で自動補完をトリガー";
},
Send: "送信",
},
Export: {
Title: "チャット履歴をMarkdown形式でエクスポート",
Copy: "すべてコピー",
Download: "ファイルをダウンロード",
MessageFromYou: "あなたからのメッセージ",
MessageFromChatGPT: "ChatGPTからのメッセージ",
},
Memory: {
Title: "履歴メモリ",
EmptyContent: "まだ記憶されていません",
Send: "メモリを送信",
Copy: "メモリをコピー",
Reset: "チャットをリセット",
ResetConfirm:
"リセット後、現在のチャット履歴と過去のメモリがクリアされます。リセットしてもよろしいですか?",
},
Home: {
NewChat: "新しいチャット",
DeleteChat: "選択したチャットを削除してもよろしいですか?",
DeleteToast: "チャットが削除されました",
Revert: "元に戻す",
},
Settings: {
Title: "設定",
SubTitle: "設定オプション",
Actions: {
ClearAll: "すべてのデータをクリア",
ResetAll: "すべてのオプションをリセット",
Close: "閉じる",
ConfirmResetAll: {
Confirm: "すべての設定をリセットしてもよろしいですか?",
},
ConfirmClearAll: {
Confirm: "すべてのチャットをリセットしてもよろしいですか?",
},
},
Lang: {
Name: "Language",
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "アバター",
FontSize: {
Title: "フォントサイズ",
SubTitle: "チャット内容のフォントサイズ",
},
Update: {
Version: (x: string) => `現在のバージョン:${x}`,
IsLatest: "最新バージョンです",
CheckUpdate: "アップデートを確認",
IsChecking: "アップデートを確認しています...",
FoundUpdate: (x: string) => `新しいバージョンが見つかりました:${x}`,
GoToUpdate: "更新する",
},
SendKey: "送信キー",
Theme: "テーマ",
TightBorder: "ボーダーレスモード",
SendPreviewBubble: "プレビューバブルの送信",
Prompt: {
Disable: {
Title: "プロンプトの自動補完を無効にする",
SubTitle:
"入力フィールドの先頭に / を入力すると、自動補完がトリガーされます。",
},
List: "カスタムプロンプトリスト",
ListCount: (builtin: number, custom: number) =>
`組み込み ${builtin} 件、ユーザー定義 ${custom}`,
Edit: "編集",
Modal: {
Title: "提示词列表",
Add: "增加一条",
Search: "搜尋提示詞",
},
},
HistoryCount: {
Title: "履歴メッセージ数を添付",
SubTitle: "リクエストごとに添付する履歴メッセージ数",
},
CompressThreshold: {
Title: "履歴メッセージの長さ圧縮しきい値",
SubTitle:
"圧縮されていない履歴メッセージがこの値を超えた場合、圧縮が行われます。",
},
Token: {
Title: "APIキー",
SubTitle: "自分のキーを使用してパスワードアクセス制限を迂回する",
Placeholder: "OpenAI APIキー",
},
Usage: {
Title: "残高照会",
SubTitle(used: any, total: any) {
return `今月は $${used} を使用しました。総額は $${total} です。`;
},
IsChecking: "確認中...",
Check: "再確認",
NoAccess: "APIキーまたはアクセスパスワードを入力して残高を表示",
},
AccessCode: {
Title: "アクセスパスワード",
SubTitle: "暗号化アクセスが有効になっています",
Placeholder: "アクセスパスワードを入力してください",
},
Model: "モデル (model)",
Temperature: {
Title: "ランダム性 (temperature)",
SubTitle:
"値が大きいほど、回答がランダムになります。1以上の値には文字化けが含まれる可能性があります。",
},
MaxTokens: {
Title: "シングルレスポンス制限 (max_tokens)",
SubTitle: "1回のインタラクションで使用される最大トークン数",
},
PresencePenlty: {
Title: "トピックの新鮮度 (presence_penalty)",
SubTitle: "値が大きいほど、新しいトピックへの展開が可能になります。",
},
},
Store: {
DefaultTopic: "新しいチャット",
BotHello: "何かお手伝いできることはありますか",
Error: "エラーが発生しました。しばらくしてからやり直してください。",
Prompt: {
History: (content: string) =>
"これは、AI とユーザの過去のチャットを要約した前提となるストーリーです:" +
content,
Topic:
"45文字でこの文章の簡潔な主題を返してください。説明、句読点、感嘆詞、余分なテキストは無しで。もし主題がない場合は、「おしゃべり」を返してください",
Summarize:
"あなたとユーザの会話を簡潔にまとめて、後続のコンテキストプロンプトとして使ってください。200字以内に抑えてください。",
},
ConfirmClearAll:
"すべてのチャット、設定データをクリアしてもよろしいですか?",
},
Copy: {
Success: "クリップボードに書き込みました",
Failed: "コピーに失敗しました。クリップボード許可を与えてください。",
},
Context: {
Toast: (x: any) => `前置コンテキストが ${x} 件設定されました`,
Edit: "前置コンテキストと履歴メモリ",
Add: "新規追加",
},
};
export default jp;

188
app/locales/tr.ts Normal file
View File

@@ -0,0 +1,188 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const tr: LocaleType = {
WIP: "Çalışma devam ediyor...",
Error: {
Unauthorized:
"Yetkisiz erişim, lütfen erişim kodunu ayarlar sayfasından giriniz.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} mesaj`,
},
Chat: {
SubTitle: (count: number) => `ChatGPT tarafından ${count} mesaj`,
Actions: {
ChatList: "Sohbet Listesine Git",
CompressedHistory: "Sıkıştırılmış Geçmiş Bellek Komutu",
Export: "Tüm Mesajları Markdown Olarak Dışa Aktar",
Copy: "Kopyala",
Stop: "Durdur",
Retry: "Tekrar Dene",
Delete: "Delete",
},
Rename: "Sohbeti Yeniden Adlandır",
Typing: "Yazıyor…",
Input: (submitKey: string) => {
var inputHints = `Göndermek için ${submitKey}`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", kaydırmak için Shift + Enter";
}
return inputHints + ", komutları aramak için / (eğik çizgi)";
},
Send: "Gönder",
},
Export: {
Title: "Tüm Mesajlar",
Copy: "Tümünü Kopyala",
Download: "İndir",
MessageFromYou: "Sizin Mesajınız",
MessageFromChatGPT: "ChatGPT'nin Mesajı",
},
Memory: {
Title: "Bellek Komutları",
EmptyContent: "Henüz değil.",
Send: "Belleği Gönder",
Copy: "Belleği Kopyala",
Reset: "Oturumu Sıfırla",
ResetConfirm:
"Sıfırlama, geçerli görüşme geçmişini ve geçmiş belleği siler. Sıfırlamak istediğinizden emin misiniz?",
},
Home: {
NewChat: "Yeni Sohbet",
DeleteChat: "Seçili sohbeti silmeyi onaylıyor musunuz?",
DeleteToast: "Sohbet Silindi",
Revert: "Geri Al",
},
Settings: {
Title: "Ayarlar",
SubTitle: "Tüm Ayarlar",
Actions: {
ClearAll: "Tüm Verileri Temizle",
ResetAll: "Tüm Ayarları Sıfırla",
Close: "Kapat",
ConfirmResetAll: {
Confirm: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?",
},
ConfirmClearAll: {
Confirm: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Yazı Boyutu",
SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
},
Update: {
Version: (x: string) => `Sürüm: ${x}`,
IsLatest: "En son sürüm",
CheckUpdate: "Güncellemeyi Kontrol Et",
IsChecking: "Güncelleme kontrol ediliyor...",
FoundUpdate: (x: string) => `Yeni sürüm bulundu: ${x}`,
GoToUpdate: "Güncelle",
},
SendKey: "Gönder Tuşu",
Theme: "Tema",
TightBorder: "Tam Ekran",
SendPreviewBubble: "Mesaj Önizleme Balonu",
Prompt: {
Disable: {
Title: "Otomatik tamamlamayı devre dışı bırak",
SubTitle: "Otomatik tamamlamayı kullanmak için / (eğik çizgi) girin",
},
List: "Komut Listesi",
ListCount: (builtin: number, custom: number) =>
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
Edit: "Düzenle",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
},
},
HistoryCount: {
Title: "Ekli Mesaj Sayısı",
SubTitle: "İstek başına ekli gönderilen mesaj sayısı",
},
CompressThreshold: {
Title: "Geçmiş Sıkıştırma Eşiği",
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) {
return `Bu ay kullanılan $${used}, abonelik $${total}`;
},
IsChecking: "Kontrol ediliyor...",
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",
SubTitle:
"Daha büyük bir değer girildiğinde gerçeklik oranı düşer ve daha rastgele çıktılar üretir",
},
MaxTokens: {
Title: "Maksimum Belirteç",
SubTitle:
"Girdi belirteçlerinin ve oluşturulan belirteçlerin maksimum uzunluğu",
},
PresencePenlty: {
Title: "Varlık Cezası",
SubTitle:
"Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır",
},
},
Store: {
DefaultTopic: "Yeni Konuşma",
BotHello: "Merhaba! Size bugün nasıl yardımcı olabilirim?",
Error: "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyiniz.",
Prompt: {
History: (content: string) =>
"Bu, yapay zeka ile kullanıcı arasındaki sohbet geçmişinin bir özetidir: " +
content,
Topic:
"Lütfen herhangi bir giriş, noktalama işareti, tırnak işareti, nokta, sembol veya ek metin olmadan konuşmamızı özetleyen dört ila beş kelimelik bir başlık oluşturun. Çevreleyen tırnak işaretlerini kaldırın.",
Summarize:
"Gelecekteki bağlam için bir bilgi istemi olarak kullanmak üzere tartışmamızı en fazla 200 kelimeyle özetleyin.",
},
ConfirmClearAll:
"Tüm sohbet ve ayar verilerini temizlemeyi onaylıyor musunuz?",
},
Copy: {
Success: "Panoya kopyalandı",
Failed: "Kopyalama başarısız oldu, lütfen panoya erişim izni verin",
},
Context: {
Toast: (x: any) => `${x} bağlamsal bellek komutu`,
Edit: "Bağlamsal ve Bellek Komutları",
Add: "Yeni Ekle",
},
};
export default tr;

View File

@@ -18,6 +18,7 @@ const tw: LocaleType = {
Copy: "複製",
Stop: "停止",
Retry: "重試",
Delete: "刪除",
},
Rename: "重命名對話",
Typing: "正在輸入…",
@@ -58,6 +59,12 @@ const tw: LocaleType = {
ClearAll: "清除所有數據",
ResetAll: "重置所有設定",
Close: "關閉",
ConfirmResetAll: {
Confirm: "Are you sure you want to reset all configurations?",
},
ConfirmClearAll: {
Confirm: "Are you sure you want to reset all chat?",
},
},
Lang: {
Name: "Language",
@@ -67,6 +74,9 @@ const tw: LocaleType = {
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
jp: "日本語",
de: "Deutsch",
},
},
Avatar: "大頭貼",
@@ -95,6 +105,11 @@ const tw: LocaleType = {
ListCount: (builtin: number, custom: number) =>
`內置 ${builtin} 條,用戶定義 ${custom}`,
Edit: "編輯",
Modal: {
Title: "提示詞列表",
Add: "增加一條",
Search: "搜索提示词",
},
},
HistoryCount: {
Title: "附帶歷史訊息數",
@@ -144,9 +159,10 @@ const tw: LocaleType = {
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
Topic:
"Summarise the conversation in a short and concise eye-catching title that instantly conveys the main topic. Use as few words as possible. Use the language used in the enquiry, e.g. use English for English enquiry, use zh-hant for traditional chinese enquiry. Don't use quotation marks at the beginning and the end.",
Summarize:
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt且字數控制在 200 字以內",
"Summarise the conversation in at most 250 tokens for continuing the conversation in future. Use the language used in the conversation, e.g. use English for English conversation, use zh-hant for traditional chinese conversation.",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},

View File

@@ -2,11 +2,15 @@ import { Analytics } from "@vercel/analytics/react";
import { Home } from "./components/home";
export default function App() {
import { getServerSideConfig } from "./config/server";
const serverConfig = getServerSideConfig();
export default async function App() {
return (
<>
<Home />
<Analytics />
{serverConfig?.isVercel && <Analytics />}
</>
);
}

View File

@@ -1,15 +1,15 @@
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
import type { ChatRequest, ChatResponse } from "./api/openai/typing";
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
import { showToast } from "./components/ui-lib";
const TIME_OUT_MS = 30000;
const TIME_OUT_MS = 60000;
const makeRequestParam = (
messages: Message[],
options?: {
filterBot?: boolean;
stream?: boolean;
}
},
): ChatRequest => {
let sendMessages = messages.map((v) => ({
role: v.role,
@@ -20,7 +20,11 @@ const makeRequestParam = (
sendMessages = sendMessages.filter((m) => m.role !== "assistant");
}
const modelConfig = useChatStore.getState().config.modelConfig;
const modelConfig = { ...useChatStore.getState().config.modelConfig };
// @yidadaa: wont send max_tokens, because it is nonsense for Muggles
// @ts-expect-error
delete modelConfig.max_tokens;
return {
messages: sendMessages,
@@ -63,7 +67,7 @@ export async function requestChat(messages: Message[]) {
const res = await requestOpenaiClient("v1/chat/completions")(req);
try {
const response = (await res.json()) as ChatReponse;
const response = (await res.json()) as ChatResponse;
return response;
} catch (error) {
console.error("[Request Chat] ", error, res.body);
@@ -84,7 +88,7 @@ export async function requestUsage() {
const [used, subs] = await Promise.all([
requestOpenaiClient(
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
)(null, "GET"),
requestOpenaiClient("dashboard/billing/subscription")(null, "GET"),
]);
@@ -110,6 +114,10 @@ export async function requestUsage() {
response.total_usage = Math.round(response.total_usage) / 100;
}
if (total.hard_limit_usd) {
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
}
return {
used: response.total_usage,
subscription: total.hard_limit_usd,
@@ -124,7 +132,7 @@ export async function requestChatStream(
onMessage: (message: string, done: boolean) => void;
onError: (error: Error, statusCode?: number) => void;
onController?: (controller: AbortController) => void;
}
},
) {
const req = makeRequestParam(messages, {
stream: true,
@@ -163,14 +171,18 @@ export async function requestChatStream(
options?.onController?.(controller);
while (true) {
// handle time out, will stop if no response in 10 secs
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read();
clearTimeout(resTimeoutId);
const text = decoder.decode(content?.value);
if (!content || !content.value) {
break;
}
const text = decoder.decode(content.value, { stream: true });
responseText += text;
const done = !content || content.done;
const done = content.done;
options?.onMessage(responseText, false);
if (done) {
@@ -180,8 +192,8 @@ export async function requestChatStream(
finish();
} else if (res.status === 401) {
console.error("Anauthorized");
options?.onError(new Error("Anauthorized"), res.status);
console.error("Unauthorized");
options?.onError(new Error("Unauthorized"), res.status);
} else {
console.error("Stream Error", res.body);
options?.onError(new Error("Stream Error"), res.status);
@@ -213,7 +225,7 @@ export const ControllerPool = {
addController(
sessionIndex: number,
messageId: number,
controller: AbortController
controller: AbortController,
) {
const key = this.key(sessionIndex, messageId);
this.controllers[key] = controller;
@@ -226,6 +238,14 @@ export const ControllerPool = {
controller?.abort();
},
stopAll() {
Object.values(this.controllers).forEach((v) => v.abort());
},
hasPending() {
return Object.values(this.controllers).length > 0;
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
delete this.controllers[key];

View File

@@ -1,25 +1,33 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { queryMeta } from "../utils";
export interface AccessControlStore {
accessCode: string;
token: string;
needCode: boolean;
updateToken: (_: string) => void;
updateCode: (_: string) => void;
enabledAccessControl: () => boolean;
isAuthorized: () => boolean;
fetch: () => void;
}
export const ACCESS_KEY = "access-control";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
token: "",
accessCode: "",
needCode: true,
enabledAccessControl() {
return queryMeta("access") === "enabled";
get().fetch();
return get().needCode;
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
@@ -27,10 +35,35 @@ export const useAccessStore = create<AccessControlStore>()(
updateToken(token: string) {
set((state) => ({ token }));
},
isAuthorized() {
// has token or has code or disabled access control
return (
!!get().token || !!get().accessCode || !get().enabledAccessControl()
);
},
fetch() {
if (fetchState > 0) return;
fetchState = 1;
fetch("/api/config", {
method: "post",
body: null,
})
.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: ACCESS_KEY,
version: 1,
}
)
},
),
);

View File

@@ -53,6 +53,7 @@ export interface ChatConfig {
theme: Theme;
tightBorder: boolean;
sendPreviewBubble: boolean;
sidebarWidth: number;
disablePromptHint: boolean;
@@ -141,6 +142,7 @@ const DEFAULT_CONFIG: ChatConfig = {
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
sidebarWidth: 300,
disablePromptHint: false,
@@ -205,7 +207,7 @@ interface ChatStore {
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
deleteSession: () => void;
deleteSession: (index?: number) => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
@@ -326,24 +328,31 @@ export const useChatStore = create<ChatStore>()(
}));
},
deleteSession() {
deleteSession(i?: number) {
const deletedSession = get().currentSession();
const index = get().currentSessionIndex;
const index = i ?? get().currentSessionIndex;
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(
state.sessions.slice(index + Number(isLastSession)),
),
}));
},
},
5000,
);
}
showToast(Locale.Home.DeleteToast, {
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(state.sessions.slice(index + Number(isLastSession))),
}));
},
});
},
currentSession() {
@@ -377,6 +386,7 @@ export const useChatStore = create<ChatStore>()(
const botMessage: Message = createMessage({
role: "assistant",
streaming: true,
id: userMessage.id! + 1,
});
// get recent messages
@@ -412,7 +422,7 @@ export const useChatStore = create<ChatStore>()(
onError(error, statusCode) {
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
} else {
} else if (!error.message.includes("aborted")) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;

View File

@@ -1,63 +1,78 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
import { getLang } from "../locales";
export interface Prompt {
id?: number;
isUser?: boolean;
title: string;
content: string;
}
export interface PromptStore {
counter: number;
latestId: number;
prompts: Map<number, Prompt>;
prompts: Record<number, Prompt>;
add: (prompt: Prompt) => number;
remove: (id: number) => void;
search: (text: string) => Prompt[];
getUserPrompts: () => Prompt[];
updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
engine: new Fuse<Prompt>([], { keys: ["title"] }),
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
userEngine: new Fuse<Prompt>([], { keys: ["title"] }),
count: {
builtin: 0,
},
allPrompts: [] as Prompt[],
builtinPrompts: [] as Prompt[],
init(prompts: Prompt[]) {
init(builtinPrompts: Prompt[], userPrompts: Prompt[]) {
if (this.ready) {
return;
}
this.engine.setCollection(prompts);
this.allPrompts = userPrompts.concat(builtinPrompts);
this.builtinPrompts = builtinPrompts.slice();
this.builtinEngine.setCollection(builtinPrompts);
this.userEngine.setCollection(userPrompts);
this.ready = true;
},
remove(id: number) {
this.engine.remove((doc) => doc.id === id);
this.userEngine.remove((doc) => doc.id === id);
},
add(prompt: Prompt) {
this.engine.add(prompt);
this.userEngine.add(prompt);
},
search(text: string) {
const results = this.engine.search(text);
return results.map((v) => v.item);
const userResults = this.userEngine.search(text);
const builtinResults = this.builtinEngine.search(text);
return userResults.concat(builtinResults).map((v) => v.item);
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
counter: 0,
latestId: 0,
prompts: new Map(),
prompts: {},
add(prompt) {
const prompts = get().prompts;
prompt.id = get().latestId + 1;
prompts.set(prompt.id, prompt);
prompt.isUser = true;
prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
@@ -69,15 +84,41 @@ export const usePromptStore = create<PromptStore>()(
remove(id) {
const prompts = get().prompts;
prompts.delete(id);
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.id - a.id : 0));
return userPrompts;
},
updateUserPrompts(id: number, 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 SearchService.allPrompts.concat([...get().getUserPrompts()]);
}
return SearchService.search(text) as Prompt[];
},
}),
@@ -92,24 +133,31 @@ export const usePromptStore = create<PromptStore>()(
fetch(PROMPT_URL)
.then((res) => res.json())
.then((res) => {
const builtinPrompts = [res.en, res.cn]
.map((promptList: PromptList) => {
let fetchPrompts = [res.en, res.cn];
if (getLang() === "cn") {
fetchPrompts = fetchPrompts.reverse();
}
const builtinPrompts = fetchPrompts.map(
(promptList: PromptList) => {
return promptList.map(
([title, content]) =>
({
id: Math.random(),
title,
content,
} as Prompt),
);
})
.concat([...(state?.prompts?.values() ?? [])]);
const allPromptsForSearch = builtinPrompts.reduce(
(pre, cur) => pre.concat(cur),
[],
},
);
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);
SearchService.init(allPromptsForSearch, userPrompts);
});
},
},

View File

@@ -1,29 +1,58 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { getCurrentVersion } from "../utils";
import { requestUsage } from "../requests";
export interface UpdateStore {
lastUpdate: number;
remoteId: string;
remoteVersion: string;
getLatestCommitId: (force: boolean) => Promise<string>;
used?: number;
subscription?: number;
lastUpdateUsage: number;
version: string;
getLatestVersion: (force?: boolean) => Promise<void>;
updateUsage: (force?: boolean) => Promise<void>;
}
export const UPDATE_KEY = "chat-update";
function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}
const ONE_MINUTE = 60 * 1000;
export const useUpdateStore = create<UpdateStore>()(
persist(
(set, get) => ({
lastUpdate: 0,
remoteId: "",
remoteVersion: "",
async getLatestCommitId(force = false) {
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
const shouldFetch = force || overTenMins;
if (!shouldFetch) {
return getCurrentVersion();
}
lastUpdateUsage: 0,
version: "unknown",
async getLatestVersion(force = false) {
set(() => ({ version: queryMeta("version") ?? "unknown" }));
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
if (!force && !overTenMins) return;
set(() => ({
lastUpdate: Date.now(),
}));
try {
// const data = await (await fetch(FETCH_TAG_URL)).json();
@@ -31,14 +60,26 @@ export const useUpdateStore = create<UpdateStore>()(
const data = await (await fetch(FETCH_COMMIT_URL)).json();
const remoteId = (data[0].sha as string).substring(0, 7);
set(() => ({
lastUpdate: Date.now(),
remoteId,
remoteVersion: remoteId,
}));
console.log("[Got Upstream] ", remoteId);
return remoteId;
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
return getCurrentVersion();
}
},
async updateUsage(force = false) {
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
if (!overOneMinute && !force) return;
set(() => ({
lastUpdateUsage: Date.now(),
}));
const usage = await requestUsage();
if (usage) {
set(() => usage);
}
},
}),

View File

@@ -1,4 +1,6 @@
@mixin light {
--theme: light;
/* color */
--white: white;
--black: rgb(48, 48, 48);
@@ -18,6 +20,8 @@
}
@mixin dark {
--theme: dark;
/* color */
--white: rgb(30, 30, 30);
--black: rgb(187, 187, 187);
@@ -31,6 +35,10 @@
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
--theme-color: var(--gray);
div:not(.no-dark) > svg {
filter: invert(0.5);
}
}
.light {
@@ -132,6 +140,7 @@ label {
input {
text-align: center;
font-family: inherit;
}
input[type="checkbox"] {
@@ -216,6 +225,7 @@ input[type="password"] {
color: var(--black);
padding: 0 10px;
max-width: 50%;
font-family: inherit;
}
div.math {
@@ -282,10 +292,6 @@ pre {
.clickable {
cursor: pointer;
div:not(.no-dark) > svg {
filter: invert(0.5);
}
&:hover {
filter: brightness(0.9);
}

View File

@@ -51,6 +51,12 @@ export function isMobileScreen() {
return window.innerWidth <= 600;
}
export function isFirefox() {
return (
typeof navigator !== "undefined" && /firefox/i.test(navigator.userAgent)
);
}
export function selectOrCopy(el: HTMLElement, content: string) {
const currentSelection = window.getSelection();
@@ -63,31 +69,58 @@ export function selectOrCopy(el: HTMLElement, content: string) {
return true;
}
export function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}
let currentId: string;
export function getCurrentVersion() {
if (currentId) {
return currentId;
}
currentId = queryMeta("version");
return currentId;
}
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
}
function getDomContentWidth(dom: HTMLElement) {
const style = window.getComputedStyle(dom);
const paddingWidth =
parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
const width = dom.clientWidth - paddingWidth;
return width;
}
function getOrCreateMeasureDom(id: string, init?: (dom: HTMLElement) => void) {
let dom = document.getElementById(id);
if (!dom) {
dom = document.createElement("span");
dom.style.position = "absolute";
dom.style.wordBreak = "break-word";
dom.style.fontSize = "14px";
dom.style.transform = "translateY(-200vh)";
dom.style.pointerEvents = "none";
dom.style.opacity = "0";
dom.id = id;
document.body.appendChild(dom);
init?.(dom);
}
return dom!;
}
export function autoGrowTextArea(dom: HTMLTextAreaElement) {
const measureDom = getOrCreateMeasureDom("__measure");
const singleLineDom = getOrCreateMeasureDom("__single_measure", (dom) => {
dom.innerText = "TEXT_FOR_MEASURE";
});
const width = getDomContentWidth(dom);
measureDom.style.width = width + "px";
measureDom.innerHTML = dom.value.trim().length > 0 ? dom.value : "1";
const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1);
const height = parseFloat(window.getComputedStyle(measureDom).height);
const singleLineHeight = parseFloat(
window.getComputedStyle(singleLineDom).height,
);
const rows = Math.round(height / singleLineHeight) + lineWrapCount;
return rows;
}
export function getCSSVar(varName: string) {
return getComputedStyle(document.body).getPropertyValue(varName).trim();
}

View File

@@ -1,13 +1,13 @@
# 常见问题
> We are sorry that there is currently no English version of the FAQ. English users can use translation tools to access this document. We look forward to receiving your PR for an English version of the documentation.
## 如何快速获得帮助?
1. 询问ChatGPT / Bing / 百度 / Google等。
2. 询问网友。请提供问题的背景信息和碰到问题的详细描述。高质量的提问容易获得有用的答案。
# 部署相关问题
各种部署方式详细教程参考https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b
## 为什么 Docker 部署版本一直提示更新
Docker 版本相当于稳定版latest Docker 总是与 latest release version 一致,目前我们的发版频率是一到两天发一次,所以 Docker 版本会总是落后最新的提交一到两天,这在预期内。
@@ -53,6 +53,18 @@ keepalive_timeout 300; # 设定keep-alive超时时间为65秒
- 到服务器的路由通吗?
- 域名正确解析了吗?
## 什么是代理,如何使用?
由于OpenAI的IP限制中国和其他一些国家/地区无法直接连接OpenAI API需要通过代理。你可以使用代理服务器正向代理或者已经设置好的OpenAI API反向代理。
- 正向代理例子科学上网梯子。docker部署的情况下设置环境变量HTTP_PROXY为你的代理地址例如10.10.10.10:8002
- 反向代理例子可以用别人搭建的代理地址或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。
## 国内服务器可以部署吗?
可以但需要解决的问题:
- 需要代理才能连接github和openAI等网站
- 国内服务器要设置域名解析的话需要备案;
- 国内政策限制代理访问外网/ChatGPT相关应用可能被封。
# 使用相关问题
## 为什么会一直提示“出错了,稍后重试吧”
@@ -71,17 +83,6 @@ keepalive_timeout 300; # 设定keep-alive超时时间为65秒
## 使用时提示"You exceeded your current quota, ..."
API KEY有问题。余额不足。
## 什么是代理,如何使用?
由于OpenAI的IP限制中国和其他一些国家/地区无法直接连接OpenAI API需要通过代理。你可以使用代理服务器正向代理或者已经设置好的OpenAI API反向代理。
- 正向代理例子科学上网梯子。docker部署的情况下设置环境变量HTTP_PROXY为你的代理地址http://地址:端口)。
- 反向代理例子可以用别人搭建的代理地址或者通过Cloudflare免费设置。设置项目环境变量BASE_URL为你的代理地址。
## 国内服务器可以部署吗?
可以但需要解决的问题:
- 需要代理才能连接github和openAI等网站
- 国内服务器要设置域名解析的话需要备案;
- 国内政策限制代理访问外网/ChatGPT相关应用可能被封。
# 网络服务相关问题
## Cloudflare是什么
CloudflareCF是一个提供CDN域名管理静态页面托管边缘计算函数部署等的网络服务供应商。常见的用途购买和/或托管你的域名解析、动态域名等给你的服务器套上CDN可以隐藏ip免被墙部署网站CF Pages。CF免费提供大多数服务。
@@ -100,6 +101,17 @@ Vercel 是一个全球化的云平台,旨在帮助开发人员更快地构建
- 国内服务器供应商:阿里云,腾讯等;
国内服务器事项解析域名需要备案国内服务器带宽较贵访问国外网站Github, openAI等需要代理。
## 什么情况下服务器要备案?
在中国大陆经营的网站按监管要求需要备案。实际操作中,服务器位于国内且有域名解析的情况下,服务器供应商会执行监管的备案要求,否则会关停服务。通常的规则如下:
|服务器位置|域名供应商|是否需要备案|
|---|---|---|
|国内|国内|是|
|国内|国外|是|
|国外|国外|否|
|国外|国内|通常否|
换服务器供应商后需要转备案。
# OpenAI相关问题
## 如何注册OpenAI账号
去chat.openai.com注册。你需要
@@ -121,8 +133,9 @@ OpenAI只接受指定地区的信用卡中国信用卡无法使用。一
3. 网上找人代充
## 如何使用GPT-4的API访问
4月6日更新GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist准备好你的OpenAI组织IDhttps://openai.com/waitlist/gpt-4-api
- GPT-4的API访问需要单独申请。到以下地址填写你的信息进入申请队列waitlist准备好你的OpenAI组织IDhttps://openai.com/waitlist/gpt-4-api
之后等待邮件消息。
- 开通 ChatGPT Plus 不代表有 GPT-4 权限,两者毫无关系。
## 如何使用 Azure OpenAI 接口
请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
@@ -136,3 +149,17 @@ OpenAI只接受指定地区的信用卡中国信用卡无法使用。一
- 通过上述两个方法就可以定位到你的 token 被快速消耗的原因:
- 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露;
- 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。
## API是怎么计费的
OpenAI网站计费说明https://openai.com/pricing#language-models
OpenAI根据token数收费1000个token通常可代表750个英文单词或500个汉字。输入Prompt和输出Completion分别统计费用。
|模型|用户输入Prompt计费|模型输出Completion计费|每次交互最大token数|
|----|----|----|----|
|gpt-3.5|$0.002 / 1千tokens|$0.002 / 1千tokens|4096|
|gpt-4|$0.03 / 1千tokens|$0.06 / 1千tokens|8192|
|gpt-4-32K|$0.06 / 1千tokens|$0.12 / 1千tokens|32768|
## gpt-3.5-turbo和gpt3.5-turbo-0301(或者gpt3.5-turbo-mmdd)模型有什么区别?
官方文档说明https://platform.openai.com/docs/models/gpt-3-5
- gpt-3.5-turbo是最新的模型会不断得到更新。
- gpt-3.5-turbo-0301是3月1日定格的模型快照不会变化预期3个月后被新快照替代。

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

39
docs/vercel-cn.md Normal file
View File

@@ -0,0 +1,39 @@
# Vercel 的使用说明
## 如何新建项目
当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。
![vercel-create-1](./images/vercel/vercel-create-1.jpg)
1. 进入 Vercel 控制台首页;
2. 点击 Add New
3. 选择 Project。
![vercel-create-2](./images/vercel/vercel-create-2.jpg)
1. 在 Import Git Repository 处,搜索 chatgpt-next-web
2. 选中新 fork 的项目,点击 Import。
![vercel-create-3](./images/vercel/vercel-create-3.jpg)
1. 在项目配置页,点开 Environmane Variables 开始配置环境变量;
2. 依次新增名为 OPENAI_API_KEY 和 CODE 的环境变量;
3. 填入环境变量对应的值;
4. 点击 Add 确认增加环境变量;
5. 请确保你添加了 OPENAI_API_KEY否则无法使用
6. 点击 Deploy创建完成耐心等待 5 分钟左右部署完成。
## 如何增加自定义域名
[TODO]
## 如何更改环境变量
![vercel-env-edit](./images/vercel/vercel-env-edit.jpg)
1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮;
2. 点击左侧的 Environment Variables
3. 点击已有条目的右侧按钮;
4. 选择 Edit 进行编辑,然后保存即可。
⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效!
## 如何重新部署
![vercel-redeploy](./images/vercel/vercel-redeploy.jpg)
1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮;
2. 选择列表最顶部一条的右侧按钮;
3. 点击 Redeploy 即可重新部署。

View File

@@ -1,21 +1,36 @@
import { NextRequest, NextResponse } from "next/server";
import { ACCESS_CODES } from "./app/api/access";
import { getServerSideConfig } from "./app/config/server";
import md5 from "spark-md5";
export const config = {
matcher: ["/api/openai", "/api/chat-stream"],
};
const serverConfig = getServerSideConfig();
function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip");
const forwardedFor = req.headers.get("x-forwarded-for");
if (!ip && forwardedFor) {
ip = forwardedFor.split(",").at(0) ?? "";
}
return ip;
}
export function middleware(req: NextRequest) {
const accessCode = req.headers.get("access-code");
const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]);
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode);
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) {
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return NextResponse.json(
{
error: true,
@@ -30,7 +45,7 @@ export function middleware(req: NextRequest) {
// inject api key
if (!token) {
const apiKey = process.env.OPENAI_API_KEY;
const apiKey = serverConfig.apiKey;
if (apiKey) {
console.log("[Auth] set system token");
req.headers.set("token", apiKey);

View File

@@ -8,14 +8,11 @@ const nextConfig = {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
}); // 针对 SVG 的处理规则
});
return config;
}
},
output: "standalone",
};
if (process.env.DOCKER) {
nextConfig.output = 'standalone'
}
module.exports = nextConfig;

View File

@@ -9,7 +9,8 @@
"start": "next start",
"lint": "next lint",
"fetch": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install"
"prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
},
"dependencies": {
"@hello-pangea/dnd": "^16.2.0",
@@ -18,7 +19,7 @@
"emoji-picker-react": "^4.4.7",
"eventsource-parser": "^0.1.0",
"fuse.js": "^6.6.2",
"next": "^13.2.3",
"next": "^13.3.1-canary.8",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",

1
scripts/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
proxychains.conf

View File

@@ -1,14 +1,13 @@
import fetch from "node-fetch";
import fs from "fs/promises";
const RAW_CN_URL =
"https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
const CN_URL =
"https://cdn.jsdelivr.net/gh/PlexPt/awesome-chatgpt-prompts-zh@main/prompts-zh.json";
const RAW_EN_URL =
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
const EN_URL =
"https://cdn.jsdelivr.net/gh/f/awesome-chatgpt-prompts@main/prompts.csv";
const RAW_FILE_URL = "https://raw.githubusercontent.com/";
const MIRRORF_FILE_URL = "https://raw.fgit.ml/";
const RAW_CN_URL = "PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json";
const CN_URL = MIRRORF_FILE_URL + RAW_CN_URL;
const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv";
const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL;
const FILE = "./public/prompts.json";
async function fetchCN() {

5
scripts/init-proxy.sh Normal file
View File

@@ -0,0 +1,5 @@
dir="$(dirname "$0")"
config=$dir/proxychains.conf
host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //')
cp $dir/proxychains.template.conf $config
sed -i "\$s/.*/http $host_ip 7890/" $config

View File

@@ -0,0 +1,12 @@
strict_chain
proxy_dns
remote_dns_subnet 224
tcp_read_time_out 15000
tcp_connect_time_out 8000
localnet 127.0.0.0/255.0.0.0
[ProxyList]
socks4 127.0.0.1 9050

View File

@@ -23,6 +23,6 @@
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"],
"exclude": ["node_modules"]
}

5
vercel.json Normal file
View File

@@ -0,0 +1,5 @@
{
"github": {
"silent": true
}
}

145
yarn.lock
View File

@@ -1099,10 +1099,10 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@next/env@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.2.4.tgz#8b763700262b2445140a44a8c8d088cef676dbae"
integrity sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==
"@next/env@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1-canary.8.tgz#9f5cf57999e4f4b59ef6407924803a247cc4e451"
integrity sha512-xZfNu7yq3OfiC4rkGuGMcqb25se+ZHRqajSdny8dp+nZzkNSK1SHuNT3W8faI+KGk6dqzO/zAdHR9YrqnQlCAg==
"@next/eslint-plugin-next@13.2.3":
version "13.2.3"
@@ -1111,70 +1111,50 @@
dependencies:
glob "7.1.7"
"@next/swc-android-arm-eabi@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz#758d0403771e549f9cee71cbabc0cb16a6c947c0"
integrity sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==
"@next/swc-darwin-arm64@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1-canary.8.tgz#66786ba76d37c210c184739624c6f84eaf2dc52b"
integrity sha512-BLbvhcaSzwuXbREOmJiqAdXVD7Jl9830hDY5ZTTNg7hXqEZgoMg2LxAEmtaaBMVZRfDQjd5bH3QPBV8fbG4UKg==
"@next/swc-android-arm64@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz#834d586523045110d5602e0c8aae9028835ac427"
integrity sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==
"@next/swc-darwin-x64@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1-canary.8.tgz#289296bd3cc55db7fef42037eb89ce4a6260ba31"
integrity sha512-n4tJKPIvFTZshS1TVWrsqaW7h9VW+BmguO/AlZ3Q3NJ9hWxC5L4lxn2T6CTQ4M30Gf+t5u+dPzYLQ5IDtJFnFQ==
"@next/swc-darwin-arm64@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz#5006fca179a36ef3a24d293abadec7438dbb48c6"
integrity sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==
"@next/swc-linux-arm64-gnu@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1-canary.8.tgz#dc79e8005849b6482241b460abdce9334665c766"
integrity sha512-AxnsgZ56whwVAeejyEZMk8xc8Vapwzb3Zn0YdZzPCR42WKfkcSkM+AWfq33zUOZnjvCmQBDyfHIo4CURVweR6g==
"@next/swc-darwin-x64@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz#6549c7c04322766acc3264ccdb3e1b43fcaf7946"
integrity sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==
"@next/swc-linux-arm64-musl@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1-canary.8.tgz#f70873add4aad7ced36f760d1640adc008b7dc03"
integrity sha512-zc7rzhtrHMWZ/phvjCNplHGo+ZLembjtluI5J8Xl4iwQQCyZwAtnmQhs37/zkdi6dHZou+wcFBZWRz14awRDBw==
"@next/swc-freebsd-x64@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz#0bbe28979e3e868debc2cc06e45e186ce195b7f4"
integrity sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==
"@next/swc-linux-x64-gnu@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1-canary.8.tgz#fe81b8033628c6cf74e154f2db8c8c7f1593008f"
integrity sha512-vNbFDiuZ9fWmcznlilDbflZLb04evWPUQlyDT7Tqjd964PlSIaaX3tr64pdYjJOljDaqTr2Kbx0YW74mWF/PEw==
"@next/swc-linux-arm-gnueabihf@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz#1d28d2203f5a7427d6e7119d7bcb5fc40959fb3e"
integrity sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==
"@next/swc-linux-x64-musl@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1-canary.8.tgz#ada4585046a7937f96f2d39fc4aaca12826dde5f"
integrity sha512-/FVBPJEBDZYCNraocRWtd5ObAgNi9VFnzJYGYDYIj4jKkFRWWm/CaWu9A7toQACC/JDy262uPyDPathXT9BAqQ==
"@next/swc-linux-arm64-gnu@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz#eb26448190948cdf4c44b8f34110a3ecea32f1d0"
integrity sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==
"@next/swc-win32-arm64-msvc@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1-canary.8.tgz#21b4f6c4be61845759753df9313bd9bcbb241969"
integrity sha512-8jMwRCeI26yVZLPwG0AjOi4b1yqSeqYmbHA7r+dqiV0OgFdYjnbyHU1FmiKDaC5SnnJN6LWV2Qjer9GDD0Kcuw==
"@next/swc-linux-arm64-musl@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz#c4227c0acd94a420bb14924820710e6284d234d3"
integrity sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==
"@next/swc-win32-ia32-msvc@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1-canary.8.tgz#e23192e1d1b1a32b0eb805363b02360c5b523a77"
integrity sha512-kcYB9iSEikFhv0I9uQDdgQ2lm8i3O8LA+GhnED9e5VtURBwOSwED7c6ZpaRQBYSPgnEA9/xiJVChICE/I7Ig1g==
"@next/swc-linux-x64-gnu@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz#6bcb540944ee9b0209b33bfc23b240c2044dfc3e"
integrity sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==
"@next/swc-linux-x64-musl@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz#ce21e43251eaf09a09df39372b2c3e38028c30ff"
integrity sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==
"@next/swc-win32-arm64-msvc@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz#68220063d8e5e082f5465498675640dedb670ff1"
integrity sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==
"@next/swc-win32-ia32-msvc@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz#7c120ab54a081be9566df310bed834f168252990"
integrity sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==
"@next/swc-win32-x64-msvc@13.2.4":
version "13.2.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz#5abda92fe12b9829bf7951c4a221282c56041144"
integrity sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==
"@next/swc-win32-x64-msvc@13.3.1-canary.8":
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1-canary.8.tgz#a3f29404955cba2193de5e74fd5d9fcfdcb0ab51"
integrity sha512-UKrGHonKVWBNg+HI4J8pXE6Jjjl8GwjhygFau71s8M0+jSy99y5Y+nGH9EmMNWKNvrObukyYvrs6OsAusKdCqw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1730,6 +1710,13 @@ browserslist@^4.21.3, browserslist@^4.21.5:
node-releases "^2.0.8"
update-browserslist-db "^1.0.10"
busboy@1.6.0:
version "1.6.0"
resolved "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -3937,30 +3924,27 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@^13.2.3:
version "13.2.4"
resolved "https://registry.yarnpkg.com/next/-/next-13.2.4.tgz#2363330392b0f7da02ab41301f60857ffa7f67d6"
integrity sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==
next@^13.3.1-canary.8:
version "13.3.1-canary.8"
resolved "https://registry.yarnpkg.com/next/-/next-13.3.1-canary.8.tgz#f0846e5eada1491884326786a0749d5adc04c24d"
integrity sha512-z4QUgyAN+hSWSEqb4pvGvC3iRktE6NH2DVLU4AvfqNYpzP+prePiJC8HN/cJpFhGW9YbhyRLi5FliDC631OOag==
dependencies:
"@next/env" "13.2.4"
"@next/env" "13.3.1-canary.8"
"@swc/helpers" "0.4.14"
busboy "1.6.0"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.1.1"
optionalDependencies:
"@next/swc-android-arm-eabi" "13.2.4"
"@next/swc-android-arm64" "13.2.4"
"@next/swc-darwin-arm64" "13.2.4"
"@next/swc-darwin-x64" "13.2.4"
"@next/swc-freebsd-x64" "13.2.4"
"@next/swc-linux-arm-gnueabihf" "13.2.4"
"@next/swc-linux-arm64-gnu" "13.2.4"
"@next/swc-linux-arm64-musl" "13.2.4"
"@next/swc-linux-x64-gnu" "13.2.4"
"@next/swc-linux-x64-musl" "13.2.4"
"@next/swc-win32-arm64-msvc" "13.2.4"
"@next/swc-win32-ia32-msvc" "13.2.4"
"@next/swc-win32-x64-msvc" "13.2.4"
"@next/swc-darwin-arm64" "13.3.1-canary.8"
"@next/swc-darwin-x64" "13.3.1-canary.8"
"@next/swc-linux-arm64-gnu" "13.3.1-canary.8"
"@next/swc-linux-arm64-musl" "13.3.1-canary.8"
"@next/swc-linux-x64-gnu" "13.3.1-canary.8"
"@next/swc-linux-x64-musl" "13.3.1-canary.8"
"@next/swc-win32-arm64-msvc" "13.3.1-canary.8"
"@next/swc-win32-ia32-msvc" "13.3.1-canary.8"
"@next/swc-win32-x64-msvc" "13.3.1-canary.8"
node-domexception@^1.0.0:
version "1.0.0"
@@ -4668,6 +4652,11 @@ stop-iteration-iterator@^1.0.0:
dependencies:
internal-slot "^1.0.4"
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
string-argv@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"