Upload 150 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .eslintignore +1 -0
- .eslintrc.json +4 -0
- .github/ISSUE_TEMPLATE/bug_report.md +43 -0
- .github/ISSUE_TEMPLATE/feature_request.md +20 -0
- .github/ISSUE_TEMPLATE/功能建议.md +20 -0
- .github/ISSUE_TEMPLATE/反馈问题.md +32 -0
- .github/workflows/docker.yml +52 -0
- .github/workflows/sync.yml +40 -0
- .gitignore +42 -0
- .gitpod.yml +11 -0
- .husky/pre-commit +4 -0
- .lintstagedrc.json +6 -0
- .prettierrc.js +10 -0
- CODE_OF_CONDUCT.md +128 -0
- Dockerfile +59 -0
- LICENSE +75 -0
- app/api/chat-stream/route.ts +62 -0
- app/api/common.ts +37 -0
- app/api/config/route.ts +23 -0
- app/api/openai/route.ts +33 -0
- app/api/openai/typing.ts +9 -0
- app/components/button.module.scss +64 -0
- app/components/button.tsx +45 -0
- app/components/chat-list.tsx +147 -0
- app/components/chat.module.scss +109 -0
- app/components/chat.tsx +787 -0
- app/components/emoji.tsx +59 -0
- app/components/error.tsx +73 -0
- app/components/home.module.scss +572 -0
- app/components/home.tsx +139 -0
- app/components/input-range.module.scss +7 -0
- app/components/input-range.tsx +37 -0
- app/components/markdown.tsx +123 -0
- app/components/mask.module.scss +108 -0
- app/components/mask.tsx +402 -0
- app/components/model-config.tsx +141 -0
- app/components/new-chat.module.scss +115 -0
- app/components/new-chat.tsx +185 -0
- app/components/settings.module.scss +77 -0
- app/components/settings.tsx +557 -0
- app/components/sidebar.tsx +181 -0
- app/components/ui-lib.module.scss +203 -0
- app/components/ui-lib.tsx +246 -0
- app/config/build.ts +27 -0
- app/config/server.ts +42 -0
- app/constant.ts +38 -0
- app/global.d.ts +11 -0
- app/icons/add.svg +23 -0
- app/icons/auto.svg +1 -0
- app/icons/black-bot.svg +28 -0
.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
public/serviceWorker.js
|
.eslintrc.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "next/core-web-vitals",
|
| 3 |
+
"plugins": ["prettier"]
|
| 4 |
+
}
|
.github/ISSUE_TEMPLATE/bug_report.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: Bug report
|
| 3 |
+
about: Create a report to help us improve
|
| 4 |
+
title: "[Bug] "
|
| 5 |
+
labels: ''
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
**Describe the bug**
|
| 11 |
+
A clear and concise description of what the bug is.
|
| 12 |
+
|
| 13 |
+
**To Reproduce**
|
| 14 |
+
Steps to reproduce the behavior:
|
| 15 |
+
1. Go to '...'
|
| 16 |
+
2. Click on '....'
|
| 17 |
+
3. Scroll down to '....'
|
| 18 |
+
4. See error
|
| 19 |
+
|
| 20 |
+
**Expected behavior**
|
| 21 |
+
A clear and concise description of what you expected to happen.
|
| 22 |
+
|
| 23 |
+
**Screenshots**
|
| 24 |
+
If applicable, add screenshots to help explain your problem.
|
| 25 |
+
|
| 26 |
+
**Deployment**
|
| 27 |
+
- [ ] Docker
|
| 28 |
+
- [ ] Vercel
|
| 29 |
+
- [ ] Server
|
| 30 |
+
|
| 31 |
+
**Desktop (please complete the following information):**
|
| 32 |
+
- OS: [e.g. iOS]
|
| 33 |
+
- Browser [e.g. chrome, safari]
|
| 34 |
+
- Version [e.g. 22]
|
| 35 |
+
|
| 36 |
+
**Smartphone (please complete the following information):**
|
| 37 |
+
- Device: [e.g. iPhone6]
|
| 38 |
+
- OS: [e.g. iOS8.1]
|
| 39 |
+
- Browser [e.g. stock browser, safari]
|
| 40 |
+
- Version [e.g. 22]
|
| 41 |
+
|
| 42 |
+
**Additional Logs**
|
| 43 |
+
Add any logs about the problem here.
|
.github/ISSUE_TEMPLATE/feature_request.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: Feature request
|
| 3 |
+
about: Suggest an idea for this project
|
| 4 |
+
title: "[Feature] "
|
| 5 |
+
labels: ''
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
**Is your feature request related to a problem? Please describe.**
|
| 11 |
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
| 12 |
+
|
| 13 |
+
**Describe the solution you'd like**
|
| 14 |
+
A clear and concise description of what you want to happen.
|
| 15 |
+
|
| 16 |
+
**Describe alternatives you've considered**
|
| 17 |
+
A clear and concise description of any alternative solutions or features you've considered.
|
| 18 |
+
|
| 19 |
+
**Additional context**
|
| 20 |
+
Add any other context or screenshots about the feature request here.
|
.github/ISSUE_TEMPLATE/功能建议.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: 功能建议
|
| 3 |
+
about: 请告诉我们你的灵光一闪
|
| 4 |
+
title: "[Feature] "
|
| 5 |
+
labels: ''
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
**这个功能与现有的问题有关吗?**
|
| 11 |
+
如果有关,请在此列出链接或者描述问题。
|
| 12 |
+
|
| 13 |
+
**你想要什么功能或者有什么建议?**
|
| 14 |
+
尽管告诉我们。
|
| 15 |
+
|
| 16 |
+
**有没有可以参考的同类竞品?**
|
| 17 |
+
可以给出参考产品的链接或者截图。
|
| 18 |
+
|
| 19 |
+
**其他信息**
|
| 20 |
+
可以说说你的其他考虑。
|
.github/ISSUE_TEMPLATE/反馈问题.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: 反馈问题
|
| 3 |
+
about: 请告诉我们你遇到的问题
|
| 4 |
+
title: "[Bug] "
|
| 5 |
+
labels: ''
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
**反馈须知**
|
| 11 |
+
|
| 12 |
+
⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭。
|
| 13 |
+
|
| 14 |
+
请在下方中括号内输入 x 来表示你已经知晓相关内容。
|
| 15 |
+
- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
|
| 16 |
+
- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
|
| 17 |
+
- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。
|
| 18 |
+
|
| 19 |
+
**描述问题**
|
| 20 |
+
请在此描述你遇到了什么问题。
|
| 21 |
+
|
| 22 |
+
**如何复现**
|
| 23 |
+
请告诉我们你是通过什么操作触发的该问题。
|
| 24 |
+
|
| 25 |
+
**截图**
|
| 26 |
+
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
|
| 27 |
+
|
| 28 |
+
**一些必要的信息**
|
| 29 |
+
- 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
|
| 30 |
+
- 浏览器: [比如 chrome, safari]
|
| 31 |
+
- 版本: [填写设置页面的版本号]
|
| 32 |
+
- 部署方式:[比如 vercel、docker 或者服务器部署]
|
.github/workflows/docker.yml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish Docker image
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
release:
|
| 6 |
+
types: [published]
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
push_to_registry:
|
| 10 |
+
name: Push Docker image to Docker Hub
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
-
|
| 14 |
+
name: Check out the repo
|
| 15 |
+
uses: actions/checkout@v3
|
| 16 |
+
-
|
| 17 |
+
name: Log in to Docker Hub
|
| 18 |
+
uses: docker/login-action@v2
|
| 19 |
+
with:
|
| 20 |
+
username: ${{ secrets.DOCKER_USERNAME }}
|
| 21 |
+
password: ${{ secrets.DOCKER_PASSWORD }}
|
| 22 |
+
|
| 23 |
+
-
|
| 24 |
+
name: Extract metadata (tags, labels) for Docker
|
| 25 |
+
id: meta
|
| 26 |
+
uses: docker/metadata-action@v4
|
| 27 |
+
with:
|
| 28 |
+
images: yidadaa/chatgpt-next-web
|
| 29 |
+
tags: |
|
| 30 |
+
type=raw,value=latest
|
| 31 |
+
type=ref,event=tag
|
| 32 |
+
|
| 33 |
+
-
|
| 34 |
+
name: Set up QEMU
|
| 35 |
+
uses: docker/setup-qemu-action@v2
|
| 36 |
+
|
| 37 |
+
-
|
| 38 |
+
name: Set up Docker Buildx
|
| 39 |
+
uses: docker/setup-buildx-action@v2
|
| 40 |
+
|
| 41 |
+
-
|
| 42 |
+
name: Build and push Docker image
|
| 43 |
+
uses: docker/build-push-action@v4
|
| 44 |
+
with:
|
| 45 |
+
context: .
|
| 46 |
+
platforms: linux/amd64,linux/arm64
|
| 47 |
+
push: true
|
| 48 |
+
tags: ${{ steps.meta.outputs.tags }}
|
| 49 |
+
labels: ${{ steps.meta.outputs.labels }}
|
| 50 |
+
cache-from: type=gha
|
| 51 |
+
cache-to: type=gha,mode=max
|
| 52 |
+
|
.github/workflows/sync.yml
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Upstream Sync
|
| 2 |
+
|
| 3 |
+
permissions:
|
| 4 |
+
contents: write
|
| 5 |
+
|
| 6 |
+
on:
|
| 7 |
+
schedule:
|
| 8 |
+
- cron: "0 * * * *" # every hour
|
| 9 |
+
workflow_dispatch:
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
sync_latest_from_upstream:
|
| 13 |
+
name: Sync latest commits from upstream repo
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
if: ${{ github.event.repository.fork }}
|
| 16 |
+
|
| 17 |
+
steps:
|
| 18 |
+
# Step 1: run a standard checkout action
|
| 19 |
+
- name: Checkout target repo
|
| 20 |
+
uses: actions/checkout@v3
|
| 21 |
+
|
| 22 |
+
# Step 2: run the sync action
|
| 23 |
+
- name: Sync upstream changes
|
| 24 |
+
id: sync
|
| 25 |
+
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
| 26 |
+
with:
|
| 27 |
+
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
| 28 |
+
upstream_sync_branch: main
|
| 29 |
+
target_sync_branch: main
|
| 30 |
+
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
| 31 |
+
|
| 32 |
+
# Set test_mode true to run tests instead of the true action!!
|
| 33 |
+
test_mode: false
|
| 34 |
+
|
| 35 |
+
- name: Sync check
|
| 36 |
+
if: failure()
|
| 37 |
+
run: |
|
| 38 |
+
echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
|
| 39 |
+
echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
|
| 40 |
+
exit 1
|
.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# next.js
|
| 12 |
+
/.next/
|
| 13 |
+
/out/
|
| 14 |
+
|
| 15 |
+
# production
|
| 16 |
+
/build
|
| 17 |
+
|
| 18 |
+
# misc
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.pem
|
| 21 |
+
|
| 22 |
+
# debug
|
| 23 |
+
npm-debug.log*
|
| 24 |
+
yarn-debug.log*
|
| 25 |
+
yarn-error.log*
|
| 26 |
+
.pnpm-debug.log*
|
| 27 |
+
|
| 28 |
+
# local env files
|
| 29 |
+
.env*.local
|
| 30 |
+
|
| 31 |
+
# vercel
|
| 32 |
+
.vercel
|
| 33 |
+
|
| 34 |
+
# typescript
|
| 35 |
+
*.tsbuildinfo
|
| 36 |
+
next-env.d.ts
|
| 37 |
+
dev
|
| 38 |
+
|
| 39 |
+
public/prompts.json
|
| 40 |
+
|
| 41 |
+
.vscode
|
| 42 |
+
.idea
|
.gitpod.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This configuration file was automatically generated by Gitpod.
|
| 2 |
+
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
|
| 3 |
+
# and commit this file to your remote git repository to share the goodness with others.
|
| 4 |
+
|
| 5 |
+
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
|
| 6 |
+
|
| 7 |
+
tasks:
|
| 8 |
+
- init: yarn install && yarn run dev
|
| 9 |
+
command: yarn run dev
|
| 10 |
+
|
| 11 |
+
|
.husky/pre-commit
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env sh
|
| 2 |
+
. "$(dirname -- "$0")/_/husky.sh"
|
| 3 |
+
|
| 4 |
+
npx lint-staged
|
.lintstagedrc.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
|
| 3 |
+
"eslint --fix",
|
| 4 |
+
"prettier --write"
|
| 5 |
+
]
|
| 6 |
+
}
|
.prettierrc.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
printWidth: 80,
|
| 3 |
+
tabWidth: 2,
|
| 4 |
+
useTabs: false,
|
| 5 |
+
semi: true,
|
| 6 |
+
singleQuote: false,
|
| 7 |
+
trailingComma: 'all',
|
| 8 |
+
bracketSpacing: true,
|
| 9 |
+
arrowParens: 'always',
|
| 10 |
+
};
|
CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributor Covenant Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
| 6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
| 7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
| 8 |
+
identity and expression, level of experience, education, socio-economic status,
|
| 9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
| 10 |
+
and orientation.
|
| 11 |
+
|
| 12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
| 13 |
+
diverse, inclusive, and healthy community.
|
| 14 |
+
|
| 15 |
+
## Our Standards
|
| 16 |
+
|
| 17 |
+
Examples of behavior that contributes to a positive environment for our
|
| 18 |
+
community include:
|
| 19 |
+
|
| 20 |
+
* Demonstrating empathy and kindness toward other people
|
| 21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
| 22 |
+
* Giving and gracefully accepting constructive feedback
|
| 23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
| 24 |
+
and learning from the experience
|
| 25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
| 26 |
+
overall community
|
| 27 |
+
|
| 28 |
+
Examples of unacceptable behavior include:
|
| 29 |
+
|
| 30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
| 31 |
+
advances of any kind
|
| 32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
| 33 |
+
* Public or private harassment
|
| 34 |
+
* Publishing others' private information, such as a physical or email
|
| 35 |
+
address, without their explicit permission
|
| 36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
| 37 |
+
professional setting
|
| 38 |
+
|
| 39 |
+
## Enforcement Responsibilities
|
| 40 |
+
|
| 41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
| 42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
| 43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
| 44 |
+
or harmful.
|
| 45 |
+
|
| 46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
| 47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
| 48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
| 49 |
+
decisions when appropriate.
|
| 50 |
+
|
| 51 |
+
## Scope
|
| 52 |
+
|
| 53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
| 54 |
+
an individual is officially representing the community in public spaces.
|
| 55 |
+
Examples of representing our community include using an official e-mail address,
|
| 56 |
+
posting via an official social media account, or acting as an appointed
|
| 57 |
+
representative at an online or offline event.
|
| 58 |
+
|
| 59 |
+
## Enforcement
|
| 60 |
+
|
| 61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
| 62 |
+
reported to the community leaders responsible for enforcement at
|
| 63 |
+
flynn.zhang@foxmail.com.
|
| 64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
| 65 |
+
|
| 66 |
+
All community leaders are obligated to respect the privacy and security of the
|
| 67 |
+
reporter of any incident.
|
| 68 |
+
|
| 69 |
+
## Enforcement Guidelines
|
| 70 |
+
|
| 71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
| 72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
| 73 |
+
|
| 74 |
+
### 1. Correction
|
| 75 |
+
|
| 76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
| 77 |
+
unprofessional or unwelcome in the community.
|
| 78 |
+
|
| 79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
| 80 |
+
clarity around the nature of the violation and an explanation of why the
|
| 81 |
+
behavior was inappropriate. A public apology may be requested.
|
| 82 |
+
|
| 83 |
+
### 2. Warning
|
| 84 |
+
|
| 85 |
+
**Community Impact**: A violation through a single incident or series
|
| 86 |
+
of actions.
|
| 87 |
+
|
| 88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
| 89 |
+
interaction with the people involved, including unsolicited interaction with
|
| 90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
| 91 |
+
includes avoiding interactions in community spaces as well as external channels
|
| 92 |
+
like social media. Violating these terms may lead to a temporary or
|
| 93 |
+
permanent ban.
|
| 94 |
+
|
| 95 |
+
### 3. Temporary Ban
|
| 96 |
+
|
| 97 |
+
**Community Impact**: A serious violation of community standards, including
|
| 98 |
+
sustained inappropriate behavior.
|
| 99 |
+
|
| 100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
| 101 |
+
communication with the community for a specified period of time. No public or
|
| 102 |
+
private interaction with the people involved, including unsolicited interaction
|
| 103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
| 104 |
+
Violating these terms may lead to a permanent ban.
|
| 105 |
+
|
| 106 |
+
### 4. Permanent Ban
|
| 107 |
+
|
| 108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
| 109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
| 110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
| 111 |
+
|
| 112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
| 113 |
+
the community.
|
| 114 |
+
|
| 115 |
+
## Attribution
|
| 116 |
+
|
| 117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
| 118 |
+
version 2.0, available at
|
| 119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
| 120 |
+
|
| 121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
| 122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
| 123 |
+
|
| 124 |
+
[homepage]: https://www.contributor-covenant.org
|
| 125 |
+
|
| 126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
| 127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
| 128 |
+
https://www.contributor-covenant.org/translations.
|
Dockerfile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine AS base
|
| 2 |
+
|
| 3 |
+
FROM base AS deps
|
| 4 |
+
|
| 5 |
+
RUN apk add --no-cache libc6-compat
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY package.json yarn.lock ./
|
| 10 |
+
|
| 11 |
+
RUN yarn config set registry 'https://registry.npm.taobao.org'
|
| 12 |
+
RUN yarn install
|
| 13 |
+
|
| 14 |
+
FROM base AS builder
|
| 15 |
+
|
| 16 |
+
RUN apk update && apk add --no-cache git
|
| 17 |
+
|
| 18 |
+
ENV OPENAI_API_KEY=""
|
| 19 |
+
ENV CODE=""
|
| 20 |
+
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 23 |
+
COPY . .
|
| 24 |
+
|
| 25 |
+
RUN yarn build
|
| 26 |
+
|
| 27 |
+
FROM base AS runner
|
| 28 |
+
WORKDIR /app
|
| 29 |
+
|
| 30 |
+
RUN apk add proxychains-ng
|
| 31 |
+
|
| 32 |
+
ENV PROXY_URL=""
|
| 33 |
+
ENV OPENAI_API_KEY=""
|
| 34 |
+
ENV CODE=""
|
| 35 |
+
|
| 36 |
+
COPY --from=builder /app/public ./public
|
| 37 |
+
COPY --from=builder /app/.next/standalone ./
|
| 38 |
+
COPY --from=builder /app/.next/static ./.next/static
|
| 39 |
+
COPY --from=builder /app/.next/server ./.next/server
|
| 40 |
+
|
| 41 |
+
EXPOSE 7860
|
| 42 |
+
|
| 43 |
+
CMD if [ -n "$PROXY_URL" ]; then \
|
| 44 |
+
protocol=$(echo $PROXY_URL | cut -d: -f1); \
|
| 45 |
+
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
|
| 46 |
+
port=$(echo $PROXY_URL | cut -d: -f3); \
|
| 47 |
+
conf=/etc/proxychains.conf; \
|
| 48 |
+
echo "strict_chain" > $conf; \
|
| 49 |
+
echo "proxy_dns" >> $conf; \
|
| 50 |
+
echo "remote_dns_subnet 224" >> $conf; \
|
| 51 |
+
echo "tcp_read_time_out 15000" >> $conf; \
|
| 52 |
+
echo "tcp_connect_time_out 8000" >> $conf; \
|
| 53 |
+
echo "[ProxyList]" >> $conf; \
|
| 54 |
+
echo "$protocol $host $port" >> $conf; \
|
| 55 |
+
cat /etc/proxychains.conf; \
|
| 56 |
+
proxychains -f $conf node server.js; \
|
| 57 |
+
else \
|
| 58 |
+
node server.js; \
|
| 59 |
+
fi
|
LICENSE
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
版权所有(c)<2023><Zhang Yifei>
|
| 2 |
+
|
| 3 |
+
反996许可证版本1.0
|
| 4 |
+
|
| 5 |
+
在符合下列条件的情况下,
|
| 6 |
+
特此免费向任何得到本授权作品的副本(包括源代码、文件和/或相关内容,以下统称为“授权作品”
|
| 7 |
+
)的个人和法人实体授权:被授权个人或法人实体有权以任何目的处置授权作品,包括但不限于使
|
| 8 |
+
用、复制,修改,衍生利用、散布,发布和再许可:
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
1. 个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不
|
| 12 |
+
得自行修改。
|
| 13 |
+
2. 个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或
|
| 14 |
+
经营地(以较严格者为准)的司法管辖区所有适用的与劳动和就业相关法律、法规、规则和
|
| 15 |
+
标准。如果该司法管辖区没有此类法律、法规、规章和标准或其法律、法规、规章和标准不可
|
| 16 |
+
执行,则个人或法人实体必须遵守国际劳工标准的核心公约。
|
| 17 |
+
3. 个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同
|
| 18 |
+
意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和
|
| 19 |
+
标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该
|
| 20 |
+
等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规
|
| 21 |
+
情况的有关当局报告或投诉上述违反许可证的行为的权利。
|
| 22 |
+
|
| 23 |
+
该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用
|
| 24 |
+
性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均
|
| 25 |
+
不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
------------------------- ENGLISH ------------------------------
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
Copyright (c) <2023> <Zhang Yifei>
|
| 32 |
+
|
| 33 |
+
Anti 996 License Version 1.0 (Draft)
|
| 34 |
+
|
| 35 |
+
Permission is hereby granted to any individual or legal entity obtaining a copy
|
| 36 |
+
of this licensed work (including the source code, documentation and/or related
|
| 37 |
+
items, hereinafter collectively referred to as the "licensed work"), free of
|
| 38 |
+
charge, to deal with the licensed work for any purpose, including without
|
| 39 |
+
limitation, the rights to use, reproduce, modify, prepare derivative works of,
|
| 40 |
+
publish, distribute and sublicense the licensed work, subject to the following
|
| 41 |
+
conditions:
|
| 42 |
+
|
| 43 |
+
1. The individual or the legal entity must conspicuously display, without
|
| 44 |
+
modification, this License on each redistributed or derivative copy of the
|
| 45 |
+
Licensed Work.
|
| 46 |
+
|
| 47 |
+
2. The individual or the legal entity must strictly comply with all applicable
|
| 48 |
+
laws, regulations, rules and standards of the jurisdiction relating to
|
| 49 |
+
labor and employment where the individual is physically located or where
|
| 50 |
+
the individual was born or naturalized; or where the legal entity is
|
| 51 |
+
registered or is operating (whichever is stricter). In case that the
|
| 52 |
+
jurisdiction has no such laws, regulations, rules and standards or its
|
| 53 |
+
laws, regulations, rules and standards are unenforceable, the individual
|
| 54 |
+
or the legal entity are required to comply with Core International Labor
|
| 55 |
+
Standards.
|
| 56 |
+
|
| 57 |
+
3. The individual or the legal entity shall not induce or force its
|
| 58 |
+
employee(s), whether full-time or part-time, or its independent
|
| 59 |
+
contractor(s), in any methods, to agree in oral or written form,
|
| 60 |
+
to directly or indirectly restrict, weaken or relinquish his or
|
| 61 |
+
her rights or remedies under such laws, regulations, rules and
|
| 62 |
+
standards relating to labor and employment as mentioned above,
|
| 63 |
+
no matter whether such written or oral agreement are enforceable
|
| 64 |
+
under the laws of the said jurisdiction, nor shall such individual
|
| 65 |
+
or the legal entity limit, in any methods, the rights of its employee(s)
|
| 66 |
+
or independent contractor(s) from reporting or complaining to the copyright
|
| 67 |
+
holder or relevant authorities monitoring the compliance of the license
|
| 68 |
+
about its violation(s) of the said license.
|
| 69 |
+
|
| 70 |
+
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 71 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
| 72 |
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
|
| 73 |
+
HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
| 74 |
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION
|
| 75 |
+
WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
|
app/api/chat-stream/route.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createParser } from "eventsource-parser";
|
| 2 |
+
import { NextRequest } from "next/server";
|
| 3 |
+
import { requestOpenai } from "../common";
|
| 4 |
+
|
| 5 |
+
async function createStream(req: NextRequest) {
|
| 6 |
+
const encoder = new TextEncoder();
|
| 7 |
+
const decoder = new TextDecoder();
|
| 8 |
+
|
| 9 |
+
const res = await requestOpenai(req);
|
| 10 |
+
|
| 11 |
+
const contentType = res.headers.get("Content-Type") ?? "";
|
| 12 |
+
if (!contentType.includes("stream")) {
|
| 13 |
+
const content = await (
|
| 14 |
+
await res.text()
|
| 15 |
+
).replace(/provided:.*. You/, "provided: ***. You");
|
| 16 |
+
console.log("[Stream] error ", content);
|
| 17 |
+
return "```json\n" + content + "```";
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const stream = new ReadableStream({
|
| 21 |
+
async start(controller) {
|
| 22 |
+
function onParse(event: any) {
|
| 23 |
+
if (event.type === "event") {
|
| 24 |
+
const data = event.data;
|
| 25 |
+
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
|
| 26 |
+
if (data === "[DONE]") {
|
| 27 |
+
controller.close();
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
try {
|
| 31 |
+
const json = JSON.parse(data);
|
| 32 |
+
const text = json.choices[0].delta.content;
|
| 33 |
+
const queue = encoder.encode(text);
|
| 34 |
+
controller.enqueue(queue);
|
| 35 |
+
} catch (e) {
|
| 36 |
+
controller.error(e);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const parser = createParser(onParse);
|
| 42 |
+
for await (const chunk of res.body as any) {
|
| 43 |
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
});
|
| 47 |
+
return stream;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export async function POST(req: NextRequest) {
|
| 51 |
+
try {
|
| 52 |
+
const stream = await createStream(req);
|
| 53 |
+
return new Response(stream);
|
| 54 |
+
} catch (error) {
|
| 55 |
+
console.error("[Chat Stream]", error);
|
| 56 |
+
return new Response(
|
| 57 |
+
["```json\n", JSON.stringify(error, null, " "), "\n```"].join(""),
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export const runtime = "edge";
|
app/api/common.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest } from "next/server";
|
| 2 |
+
|
| 3 |
+
const OPENAI_URL = "api.openai.com";
|
| 4 |
+
const DEFAULT_PROTOCOL = "https";
|
| 5 |
+
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
|
| 6 |
+
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
|
| 7 |
+
|
| 8 |
+
export async function requestOpenai(req: NextRequest) {
|
| 9 |
+
const apiKey = req.headers.get("token");
|
| 10 |
+
const openaiPath = req.headers.get("path");
|
| 11 |
+
|
| 12 |
+
let baseUrl = BASE_URL;
|
| 13 |
+
|
| 14 |
+
if (!baseUrl.startsWith("http")) {
|
| 15 |
+
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log("[Proxy] ", openaiPath);
|
| 19 |
+
console.log("[Base Url]", baseUrl);
|
| 20 |
+
|
| 21 |
+
if (process.env.OPENAI_ORG_ID) {
|
| 22 |
+
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
return fetch(`${baseUrl}/${openaiPath}`, {
|
| 26 |
+
headers: {
|
| 27 |
+
"Content-Type": "application/json",
|
| 28 |
+
Authorization: `Bearer ${apiKey}`,
|
| 29 |
+
...(process.env.OPENAI_ORG_ID && {
|
| 30 |
+
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
| 31 |
+
}),
|
| 32 |
+
},
|
| 33 |
+
cache: "no-store",
|
| 34 |
+
method: req.method,
|
| 35 |
+
body: req.body,
|
| 36 |
+
});
|
| 37 |
+
}
|
app/api/config/route.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
import { getServerSideConfig } from "../../config/server";
|
| 4 |
+
|
| 5 |
+
const serverConfig = getServerSideConfig();
|
| 6 |
+
|
| 7 |
+
// Danger! Don not write any secret value here!
|
| 8 |
+
// 警告!不要在这里写入任何敏感信息!
|
| 9 |
+
const DANGER_CONFIG = {
|
| 10 |
+
needCode: serverConfig.needCode,
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
declare global {
|
| 14 |
+
type DangerConfig = typeof DANGER_CONFIG;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export async function POST(req: NextRequest) {
|
| 18 |
+
return NextResponse.json({
|
| 19 |
+
needCode: serverConfig.needCode,
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export const runtime = "edge";
|
app/api/openai/route.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { requestOpenai } from "../common";
|
| 3 |
+
|
| 4 |
+
async function makeRequest(req: NextRequest) {
|
| 5 |
+
try {
|
| 6 |
+
const api = await requestOpenai(req);
|
| 7 |
+
const res = new NextResponse(api.body);
|
| 8 |
+
res.headers.set("Content-Type", "application/json");
|
| 9 |
+
res.headers.set("Cache-Control", "no-cache");
|
| 10 |
+
return res;
|
| 11 |
+
} catch (e) {
|
| 12 |
+
console.error("[OpenAI] ", req.body, e);
|
| 13 |
+
return NextResponse.json(
|
| 14 |
+
{
|
| 15 |
+
error: true,
|
| 16 |
+
msg: JSON.stringify(e),
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
status: 500,
|
| 20 |
+
},
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export async function POST(req: NextRequest) {
|
| 26 |
+
return makeRequest(req);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export async function GET(req: NextRequest) {
|
| 30 |
+
return makeRequest(req);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export const runtime = "edge";
|
app/api/openai/typing.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
CreateChatCompletionRequest,
|
| 3 |
+
CreateChatCompletionResponse,
|
| 4 |
+
} from "openai";
|
| 5 |
+
|
| 6 |
+
export type ChatRequest = CreateChatCompletionRequest;
|
| 7 |
+
export type ChatResponse = CreateChatCompletionResponse;
|
| 8 |
+
|
| 9 |
+
export type Updater<T> = (updater: (value: T) => void) => void;
|
app/components/button.module.scss
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.icon-button {
|
| 2 |
+
background-color: var(--white);
|
| 3 |
+
border-radius: 10px;
|
| 4 |
+
display: flex;
|
| 5 |
+
align-items: center;
|
| 6 |
+
justify-content: center;
|
| 7 |
+
padding: 10px;
|
| 8 |
+
|
| 9 |
+
cursor: pointer;
|
| 10 |
+
transition: all 0.3s ease;
|
| 11 |
+
overflow: hidden;
|
| 12 |
+
user-select: none;
|
| 13 |
+
outline: none;
|
| 14 |
+
border: none;
|
| 15 |
+
color: var(--black);
|
| 16 |
+
|
| 17 |
+
&[disabled] {
|
| 18 |
+
cursor: not-allowed;
|
| 19 |
+
opacity: 0.5;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
&.primary {
|
| 23 |
+
background-color: var(--primary);
|
| 24 |
+
color: white;
|
| 25 |
+
|
| 26 |
+
path {
|
| 27 |
+
fill: white !important;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.shadow {
|
| 33 |
+
box-shadow: var(--card-shadow);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.border {
|
| 37 |
+
border: var(--border-in-light);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.icon-button:hover {
|
| 41 |
+
border-color: var(--primary);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.icon-button-icon {
|
| 45 |
+
width: 16px;
|
| 46 |
+
height: 16px;
|
| 47 |
+
display: flex;
|
| 48 |
+
justify-content: center;
|
| 49 |
+
align-items: center;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
@media only screen and (max-width: 600px) {
|
| 53 |
+
.icon-button {
|
| 54 |
+
padding: 16px;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.icon-button-text {
|
| 59 |
+
margin-left: 5px;
|
| 60 |
+
font-size: 12px;
|
| 61 |
+
overflow: hidden;
|
| 62 |
+
text-overflow: ellipsis;
|
| 63 |
+
white-space: nowrap;
|
| 64 |
+
}
|
app/components/button.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react";
|
| 2 |
+
|
| 3 |
+
import styles from "./button.module.scss";
|
| 4 |
+
|
| 5 |
+
export function IconButton(props: {
|
| 6 |
+
onClick?: () => void;
|
| 7 |
+
icon?: JSX.Element;
|
| 8 |
+
type?: "primary" | "danger";
|
| 9 |
+
text?: string;
|
| 10 |
+
bordered?: boolean;
|
| 11 |
+
shadow?: boolean;
|
| 12 |
+
className?: string;
|
| 13 |
+
title?: string;
|
| 14 |
+
disabled?: boolean;
|
| 15 |
+
}) {
|
| 16 |
+
return (
|
| 17 |
+
<button
|
| 18 |
+
className={
|
| 19 |
+
styles["icon-button"] +
|
| 20 |
+
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
|
| 21 |
+
props.className ?? ""
|
| 22 |
+
} clickable ${styles[props.type ?? ""]}`
|
| 23 |
+
}
|
| 24 |
+
onClick={props.onClick}
|
| 25 |
+
title={props.title}
|
| 26 |
+
disabled={props.disabled}
|
| 27 |
+
role="button"
|
| 28 |
+
>
|
| 29 |
+
{props.icon && (
|
| 30 |
+
<div
|
| 31 |
+
className={
|
| 32 |
+
styles["icon-button-icon"] +
|
| 33 |
+
` ${props.type === "primary" && "no-dark"}`
|
| 34 |
+
}
|
| 35 |
+
>
|
| 36 |
+
{props.icon}
|
| 37 |
+
</div>
|
| 38 |
+
)}
|
| 39 |
+
|
| 40 |
+
{props.text && (
|
| 41 |
+
<div className={styles["icon-button-text"]}>{props.text}</div>
|
| 42 |
+
)}
|
| 43 |
+
</button>
|
| 44 |
+
);
|
| 45 |
+
}
|
app/components/chat-list.tsx
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import DeleteIcon from "../icons/delete.svg";
|
| 2 |
+
import BotIcon from "../icons/bot.svg";
|
| 3 |
+
|
| 4 |
+
import styles from "./home.module.scss";
|
| 5 |
+
import {
|
| 6 |
+
DragDropContext,
|
| 7 |
+
Droppable,
|
| 8 |
+
Draggable,
|
| 9 |
+
OnDragEndResponder,
|
| 10 |
+
} from "@hello-pangea/dnd";
|
| 11 |
+
|
| 12 |
+
import { useChatStore } from "../store";
|
| 13 |
+
|
| 14 |
+
import Locale from "../locales";
|
| 15 |
+
import { Link, useNavigate } from "react-router-dom";
|
| 16 |
+
import { Path } from "../constant";
|
| 17 |
+
import { MaskAvatar } from "./mask";
|
| 18 |
+
import { Mask } from "../store/mask";
|
| 19 |
+
|
| 20 |
+
export function ChatItem(props: {
|
| 21 |
+
onClick?: () => void;
|
| 22 |
+
onDelete?: () => void;
|
| 23 |
+
title: string;
|
| 24 |
+
count: number;
|
| 25 |
+
time: string;
|
| 26 |
+
selected: boolean;
|
| 27 |
+
id: number;
|
| 28 |
+
index: number;
|
| 29 |
+
narrow?: boolean;
|
| 30 |
+
mask: Mask;
|
| 31 |
+
}) {
|
| 32 |
+
return (
|
| 33 |
+
<Draggable draggableId={`${props.id}`} index={props.index}>
|
| 34 |
+
{(provided) => (
|
| 35 |
+
<div
|
| 36 |
+
className={`${styles["chat-item"]} ${
|
| 37 |
+
props.selected && styles["chat-item-selected"]
|
| 38 |
+
}`}
|
| 39 |
+
onClick={props.onClick}
|
| 40 |
+
ref={provided.innerRef}
|
| 41 |
+
{...provided.draggableProps}
|
| 42 |
+
{...provided.dragHandleProps}
|
| 43 |
+
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
|
| 44 |
+
props.count,
|
| 45 |
+
)}`}
|
| 46 |
+
>
|
| 47 |
+
{props.narrow ? (
|
| 48 |
+
<div className={styles["chat-item-narrow"]}>
|
| 49 |
+
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
| 50 |
+
<MaskAvatar mask={props.mask} />
|
| 51 |
+
</div>
|
| 52 |
+
<div className={styles["chat-item-narrow-count"]}>
|
| 53 |
+
{props.count}
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
) : (
|
| 57 |
+
<>
|
| 58 |
+
<div className={styles["chat-item-title"]}>{props.title}</div>
|
| 59 |
+
<div className={styles["chat-item-info"]}>
|
| 60 |
+
<div className={styles["chat-item-count"]}>
|
| 61 |
+
{Locale.ChatItem.ChatItemCount(props.count)}
|
| 62 |
+
</div>
|
| 63 |
+
<div className={styles["chat-item-date"]}>
|
| 64 |
+
{new Date(props.time).toLocaleString()}
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</>
|
| 68 |
+
)}
|
| 69 |
+
|
| 70 |
+
<div
|
| 71 |
+
className={styles["chat-item-delete"]}
|
| 72 |
+
onClickCapture={props.onDelete}
|
| 73 |
+
>
|
| 74 |
+
<DeleteIcon />
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
)}
|
| 78 |
+
</Draggable>
|
| 79 |
+
);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export function ChatList(props: { narrow?: boolean }) {
|
| 83 |
+
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
|
| 84 |
+
(state) => [
|
| 85 |
+
state.sessions,
|
| 86 |
+
state.currentSessionIndex,
|
| 87 |
+
state.selectSession,
|
| 88 |
+
state.moveSession,
|
| 89 |
+
],
|
| 90 |
+
);
|
| 91 |
+
const chatStore = useChatStore();
|
| 92 |
+
const navigate = useNavigate();
|
| 93 |
+
|
| 94 |
+
const onDragEnd: OnDragEndResponder = (result) => {
|
| 95 |
+
const { destination, source } = result;
|
| 96 |
+
if (!destination) {
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if (
|
| 101 |
+
destination.droppableId === source.droppableId &&
|
| 102 |
+
destination.index === source.index
|
| 103 |
+
) {
|
| 104 |
+
return;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
moveSession(source.index, destination.index);
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
return (
|
| 111 |
+
<DragDropContext onDragEnd={onDragEnd}>
|
| 112 |
+
<Droppable droppableId="chat-list">
|
| 113 |
+
{(provided) => (
|
| 114 |
+
<div
|
| 115 |
+
className={styles["chat-list"]}
|
| 116 |
+
ref={provided.innerRef}
|
| 117 |
+
{...provided.droppableProps}
|
| 118 |
+
>
|
| 119 |
+
{sessions.map((item, i) => (
|
| 120 |
+
<ChatItem
|
| 121 |
+
title={item.topic}
|
| 122 |
+
time={new Date(item.lastUpdate).toLocaleString()}
|
| 123 |
+
count={item.messages.length}
|
| 124 |
+
key={item.id}
|
| 125 |
+
id={item.id}
|
| 126 |
+
index={i}
|
| 127 |
+
selected={i === selectedIndex}
|
| 128 |
+
onClick={() => {
|
| 129 |
+
navigate(Path.Chat);
|
| 130 |
+
selectSession(i);
|
| 131 |
+
}}
|
| 132 |
+
onDelete={() => {
|
| 133 |
+
if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
|
| 134 |
+
chatStore.deleteSession(i);
|
| 135 |
+
}
|
| 136 |
+
}}
|
| 137 |
+
narrow={props.narrow}
|
| 138 |
+
mask={item.mask}
|
| 139 |
+
/>
|
| 140 |
+
))}
|
| 141 |
+
{provided.placeholder}
|
| 142 |
+
</div>
|
| 143 |
+
)}
|
| 144 |
+
</Droppable>
|
| 145 |
+
</DragDropContext>
|
| 146 |
+
);
|
| 147 |
+
}
|
app/components/chat.module.scss
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "../styles/animation.scss";
|
| 2 |
+
|
| 3 |
+
.chat-input-actions {
|
| 4 |
+
display: flex;
|
| 5 |
+
flex-wrap: wrap;
|
| 6 |
+
|
| 7 |
+
.chat-input-action {
|
| 8 |
+
display: inline-flex;
|
| 9 |
+
border-radius: 20px;
|
| 10 |
+
font-size: 12px;
|
| 11 |
+
background-color: var(--white);
|
| 12 |
+
color: var(--black);
|
| 13 |
+
border: var(--border-in-light);
|
| 14 |
+
padding: 4px 10px;
|
| 15 |
+
animation: slide-in ease 0.3s;
|
| 16 |
+
box-shadow: var(--card-shadow);
|
| 17 |
+
transition: all ease 0.3s;
|
| 18 |
+
margin-bottom: 10px;
|
| 19 |
+
align-items: center;
|
| 20 |
+
|
| 21 |
+
&:not(:last-child) {
|
| 22 |
+
margin-right: 5px;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.prompt-toast {
|
| 28 |
+
position: absolute;
|
| 29 |
+
bottom: -50px;
|
| 30 |
+
z-index: 999;
|
| 31 |
+
display: flex;
|
| 32 |
+
justify-content: center;
|
| 33 |
+
width: calc(100% - 40px);
|
| 34 |
+
|
| 35 |
+
.prompt-toast-inner {
|
| 36 |
+
display: flex;
|
| 37 |
+
justify-content: center;
|
| 38 |
+
align-items: center;
|
| 39 |
+
font-size: 12px;
|
| 40 |
+
background-color: var(--white);
|
| 41 |
+
color: var(--black);
|
| 42 |
+
|
| 43 |
+
border: var(--border-in-light);
|
| 44 |
+
box-shadow: var(--card-shadow);
|
| 45 |
+
padding: 10px 20px;
|
| 46 |
+
border-radius: 100px;
|
| 47 |
+
|
| 48 |
+
animation: slide-in-from-top ease 0.3s;
|
| 49 |
+
|
| 50 |
+
.prompt-toast-content {
|
| 51 |
+
margin-left: 10px;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.section-title {
|
| 57 |
+
font-size: 12px;
|
| 58 |
+
font-weight: bold;
|
| 59 |
+
margin-bottom: 10px;
|
| 60 |
+
display: flex;
|
| 61 |
+
justify-content: space-between;
|
| 62 |
+
align-items: center;
|
| 63 |
+
|
| 64 |
+
.section-title-action {
|
| 65 |
+
display: flex;
|
| 66 |
+
align-items: center;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.context-prompt {
|
| 71 |
+
.context-prompt-row {
|
| 72 |
+
display: flex;
|
| 73 |
+
justify-content: center;
|
| 74 |
+
width: 100%;
|
| 75 |
+
margin-bottom: 10px;
|
| 76 |
+
|
| 77 |
+
.context-role {
|
| 78 |
+
margin-right: 10px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.context-content {
|
| 82 |
+
flex: 1;
|
| 83 |
+
max-width: 100%;
|
| 84 |
+
text-align: left;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.context-delete-button {
|
| 88 |
+
margin-left: 10px;
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.context-prompt-button {
|
| 93 |
+
flex: 1;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.memory-prompt {
|
| 98 |
+
margin: 20px 0;
|
| 99 |
+
|
| 100 |
+
.memory-prompt-content {
|
| 101 |
+
background-color: var(--white);
|
| 102 |
+
color: var(--black);
|
| 103 |
+
border: var(--border-in-light);
|
| 104 |
+
border-radius: 10px;
|
| 105 |
+
padding: 10px;
|
| 106 |
+
font-size: 12px;
|
| 107 |
+
user-select: text;
|
| 108 |
+
}
|
| 109 |
+
}
|
app/components/chat.tsx
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useDebouncedCallback } from "use-debounce";
|
| 2 |
+
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
| 3 |
+
|
| 4 |
+
import SendWhiteIcon from "../icons/send-white.svg";
|
| 5 |
+
import BrainIcon from "../icons/brain.svg";
|
| 6 |
+
import RenameIcon from "../icons/rename.svg";
|
| 7 |
+
import ExportIcon from "../icons/share.svg";
|
| 8 |
+
import ReturnIcon from "../icons/return.svg";
|
| 9 |
+
import CopyIcon from "../icons/copy.svg";
|
| 10 |
+
import DownloadIcon from "../icons/download.svg";
|
| 11 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
| 12 |
+
import PromptIcon from "../icons/prompt.svg";
|
| 13 |
+
import MaskIcon from "../icons/mask.svg";
|
| 14 |
+
import MaxIcon from "../icons/max.svg";
|
| 15 |
+
import MinIcon from "../icons/min.svg";
|
| 16 |
+
import ResetIcon from "../icons/reload.svg";
|
| 17 |
+
|
| 18 |
+
import LightIcon from "../icons/light.svg";
|
| 19 |
+
import DarkIcon from "../icons/dark.svg";
|
| 20 |
+
import AutoIcon from "../icons/auto.svg";
|
| 21 |
+
import BottomIcon from "../icons/bottom.svg";
|
| 22 |
+
import StopIcon from "../icons/pause.svg";
|
| 23 |
+
|
| 24 |
+
import {
|
| 25 |
+
Message,
|
| 26 |
+
SubmitKey,
|
| 27 |
+
useChatStore,
|
| 28 |
+
BOT_HELLO,
|
| 29 |
+
ROLES,
|
| 30 |
+
createMessage,
|
| 31 |
+
useAccessStore,
|
| 32 |
+
Theme,
|
| 33 |
+
useAppConfig,
|
| 34 |
+
ModelConfig,
|
| 35 |
+
DEFAULT_TOPIC,
|
| 36 |
+
} from "../store";
|
| 37 |
+
|
| 38 |
+
import {
|
| 39 |
+
copyToClipboard,
|
| 40 |
+
downloadAs,
|
| 41 |
+
selectOrCopy,
|
| 42 |
+
autoGrowTextArea,
|
| 43 |
+
useMobileScreen,
|
| 44 |
+
} from "../utils";
|
| 45 |
+
|
| 46 |
+
import dynamic from "next/dynamic";
|
| 47 |
+
|
| 48 |
+
import { ControllerPool } from "../requests";
|
| 49 |
+
import { Prompt, usePromptStore } from "../store/prompt";
|
| 50 |
+
import Locale from "../locales";
|
| 51 |
+
|
| 52 |
+
import { IconButton } from "./button";
|
| 53 |
+
import styles from "./home.module.scss";
|
| 54 |
+
import chatStyle from "./chat.module.scss";
|
| 55 |
+
|
| 56 |
+
import { ListItem, Modal, showModal } from "./ui-lib";
|
| 57 |
+
import { useNavigate } from "react-router-dom";
|
| 58 |
+
import { Path } from "../constant";
|
| 59 |
+
import { Avatar } from "./emoji";
|
| 60 |
+
import { MaskAvatar, MaskConfig } from "./mask";
|
| 61 |
+
import {
|
| 62 |
+
DEFAULT_MASK_AVATAR,
|
| 63 |
+
DEFAULT_MASK_ID,
|
| 64 |
+
useMaskStore,
|
| 65 |
+
} from "../store/mask";
|
| 66 |
+
|
| 67 |
+
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
| 68 |
+
loading: () => <LoadingIcon />,
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
function exportMessages(messages: Message[], topic: string) {
|
| 72 |
+
const mdText =
|
| 73 |
+
`# ${topic}\n\n` +
|
| 74 |
+
messages
|
| 75 |
+
.map((m) => {
|
| 76 |
+
return m.role === "user"
|
| 77 |
+
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
|
| 78 |
+
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
|
| 79 |
+
})
|
| 80 |
+
.join("\n\n");
|
| 81 |
+
const filename = `${topic}.md`;
|
| 82 |
+
|
| 83 |
+
showModal({
|
| 84 |
+
title: Locale.Export.Title,
|
| 85 |
+
children: (
|
| 86 |
+
<div className="markdown-body">
|
| 87 |
+
<pre className={styles["export-content"]}>{mdText}</pre>
|
| 88 |
+
</div>
|
| 89 |
+
),
|
| 90 |
+
actions: [
|
| 91 |
+
<IconButton
|
| 92 |
+
key="copy"
|
| 93 |
+
icon={<CopyIcon />}
|
| 94 |
+
bordered
|
| 95 |
+
text={Locale.Export.Copy}
|
| 96 |
+
onClick={() => copyToClipboard(mdText)}
|
| 97 |
+
/>,
|
| 98 |
+
<IconButton
|
| 99 |
+
key="download"
|
| 100 |
+
icon={<DownloadIcon />}
|
| 101 |
+
bordered
|
| 102 |
+
text={Locale.Export.Download}
|
| 103 |
+
onClick={() => downloadAs(mdText, filename)}
|
| 104 |
+
/>,
|
| 105 |
+
],
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export function SessionConfigModel(props: { onClose: () => void }) {
|
| 110 |
+
const chatStore = useChatStore();
|
| 111 |
+
const session = chatStore.currentSession();
|
| 112 |
+
const maskStore = useMaskStore();
|
| 113 |
+
const navigate = useNavigate();
|
| 114 |
+
|
| 115 |
+
return (
|
| 116 |
+
<div className="modal-mask">
|
| 117 |
+
<Modal
|
| 118 |
+
title={Locale.Context.Edit}
|
| 119 |
+
onClose={() => props.onClose()}
|
| 120 |
+
actions={[
|
| 121 |
+
<IconButton
|
| 122 |
+
key="reset"
|
| 123 |
+
icon={<ResetIcon />}
|
| 124 |
+
bordered
|
| 125 |
+
text={Locale.Chat.Config.Reset}
|
| 126 |
+
onClick={() =>
|
| 127 |
+
confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
|
| 128 |
+
}
|
| 129 |
+
/>,
|
| 130 |
+
<IconButton
|
| 131 |
+
key="copy"
|
| 132 |
+
icon={<CopyIcon />}
|
| 133 |
+
bordered
|
| 134 |
+
text={Locale.Chat.Config.SaveAs}
|
| 135 |
+
onClick={() => {
|
| 136 |
+
navigate(Path.Masks);
|
| 137 |
+
setTimeout(() => {
|
| 138 |
+
maskStore.create(session.mask);
|
| 139 |
+
}, 500);
|
| 140 |
+
}}
|
| 141 |
+
/>,
|
| 142 |
+
]}
|
| 143 |
+
>
|
| 144 |
+
<MaskConfig
|
| 145 |
+
mask={session.mask}
|
| 146 |
+
updateMask={(updater) => {
|
| 147 |
+
const mask = { ...session.mask };
|
| 148 |
+
updater(mask);
|
| 149 |
+
chatStore.updateCurrentSession((session) => (session.mask = mask));
|
| 150 |
+
}}
|
| 151 |
+
extraListItems={
|
| 152 |
+
session.mask.modelConfig.sendMemory ? (
|
| 153 |
+
<ListItem
|
| 154 |
+
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
|
| 155 |
+
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
| 156 |
+
></ListItem>
|
| 157 |
+
) : (
|
| 158 |
+
<></>
|
| 159 |
+
)
|
| 160 |
+
}
|
| 161 |
+
></MaskConfig>
|
| 162 |
+
</Modal>
|
| 163 |
+
</div>
|
| 164 |
+
);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function PromptToast(props: {
|
| 168 |
+
showToast?: boolean;
|
| 169 |
+
showModal?: boolean;
|
| 170 |
+
setShowModal: (_: boolean) => void;
|
| 171 |
+
}) {
|
| 172 |
+
const chatStore = useChatStore();
|
| 173 |
+
const session = chatStore.currentSession();
|
| 174 |
+
const context = session.mask.context;
|
| 175 |
+
|
| 176 |
+
return (
|
| 177 |
+
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
| 178 |
+
{props.showToast && (
|
| 179 |
+
<div
|
| 180 |
+
className={chatStyle["prompt-toast-inner"] + " clickable"}
|
| 181 |
+
role="button"
|
| 182 |
+
onClick={() => props.setShowModal(true)}
|
| 183 |
+
>
|
| 184 |
+
<BrainIcon />
|
| 185 |
+
<span className={chatStyle["prompt-toast-content"]}>
|
| 186 |
+
{Locale.Context.Toast(context.length)}
|
| 187 |
+
</span>
|
| 188 |
+
</div>
|
| 189 |
+
)}
|
| 190 |
+
{props.showModal && (
|
| 191 |
+
<SessionConfigModel onClose={() => props.setShowModal(false)} />
|
| 192 |
+
)}
|
| 193 |
+
</div>
|
| 194 |
+
);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
function useSubmitHandler() {
|
| 198 |
+
const config = useAppConfig();
|
| 199 |
+
const submitKey = config.submitKey;
|
| 200 |
+
|
| 201 |
+
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
| 202 |
+
if (e.key !== "Enter") return false;
|
| 203 |
+
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
|
| 204 |
+
return (
|
| 205 |
+
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
| 206 |
+
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
| 207 |
+
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
|
| 208 |
+
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
|
| 209 |
+
(config.submitKey === SubmitKey.Enter &&
|
| 210 |
+
!e.altKey &&
|
| 211 |
+
!e.ctrlKey &&
|
| 212 |
+
!e.shiftKey &&
|
| 213 |
+
!e.metaKey)
|
| 214 |
+
);
|
| 215 |
+
};
|
| 216 |
+
|
| 217 |
+
return {
|
| 218 |
+
submitKey,
|
| 219 |
+
shouldSubmit,
|
| 220 |
+
};
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
export function PromptHints(props: {
|
| 224 |
+
prompts: Prompt[];
|
| 225 |
+
onPromptSelect: (prompt: Prompt) => void;
|
| 226 |
+
}) {
|
| 227 |
+
if (props.prompts.length === 0) return null;
|
| 228 |
+
|
| 229 |
+
return (
|
| 230 |
+
<div className={styles["prompt-hints"]}>
|
| 231 |
+
{props.prompts.map((prompt, i) => (
|
| 232 |
+
<div
|
| 233 |
+
className={styles["prompt-hint"]}
|
| 234 |
+
key={prompt.title + i.toString()}
|
| 235 |
+
onClick={() => props.onPromptSelect(prompt)}
|
| 236 |
+
>
|
| 237 |
+
<div className={styles["hint-title"]}>{prompt.title}</div>
|
| 238 |
+
<div className={styles["hint-content"]}>{prompt.content}</div>
|
| 239 |
+
</div>
|
| 240 |
+
))}
|
| 241 |
+
</div>
|
| 242 |
+
);
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
function useScrollToBottom() {
|
| 246 |
+
// for auto-scroll
|
| 247 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 248 |
+
const [autoScroll, setAutoScroll] = useState(true);
|
| 249 |
+
const scrollToBottom = () => {
|
| 250 |
+
const dom = scrollRef.current;
|
| 251 |
+
if (dom) {
|
| 252 |
+
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
| 253 |
+
}
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
// auto scroll
|
| 257 |
+
useLayoutEffect(() => {
|
| 258 |
+
autoScroll && scrollToBottom();
|
| 259 |
+
});
|
| 260 |
+
|
| 261 |
+
return {
|
| 262 |
+
scrollRef,
|
| 263 |
+
autoScroll,
|
| 264 |
+
setAutoScroll,
|
| 265 |
+
scrollToBottom,
|
| 266 |
+
};
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
export function ChatActions(props: {
|
| 270 |
+
showPromptModal: () => void;
|
| 271 |
+
scrollToBottom: () => void;
|
| 272 |
+
showPromptHints: () => void;
|
| 273 |
+
hitBottom: boolean;
|
| 274 |
+
}) {
|
| 275 |
+
const config = useAppConfig();
|
| 276 |
+
const navigate = useNavigate();
|
| 277 |
+
|
| 278 |
+
// switch themes
|
| 279 |
+
const theme = config.theme;
|
| 280 |
+
function nextTheme() {
|
| 281 |
+
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
| 282 |
+
const themeIndex = themes.indexOf(theme);
|
| 283 |
+
const nextIndex = (themeIndex + 1) % themes.length;
|
| 284 |
+
const nextTheme = themes[nextIndex];
|
| 285 |
+
config.update((config) => (config.theme = nextTheme));
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// stop all responses
|
| 289 |
+
const couldStop = ControllerPool.hasPending();
|
| 290 |
+
const stopAll = () => ControllerPool.stopAll();
|
| 291 |
+
|
| 292 |
+
return (
|
| 293 |
+
<div className={chatStyle["chat-input-actions"]}>
|
| 294 |
+
{couldStop && (
|
| 295 |
+
<div
|
| 296 |
+
className={`${chatStyle["chat-input-action"]} clickable`}
|
| 297 |
+
onClick={stopAll}
|
| 298 |
+
>
|
| 299 |
+
<StopIcon />
|
| 300 |
+
</div>
|
| 301 |
+
)}
|
| 302 |
+
{!props.hitBottom && (
|
| 303 |
+
<div
|
| 304 |
+
className={`${chatStyle["chat-input-action"]} clickable`}
|
| 305 |
+
onClick={props.scrollToBottom}
|
| 306 |
+
>
|
| 307 |
+
<BottomIcon />
|
| 308 |
+
</div>
|
| 309 |
+
)}
|
| 310 |
+
{props.hitBottom && (
|
| 311 |
+
<div
|
| 312 |
+
className={`${chatStyle["chat-input-action"]} clickable`}
|
| 313 |
+
onClick={props.showPromptModal}
|
| 314 |
+
>
|
| 315 |
+
<BrainIcon />
|
| 316 |
+
</div>
|
| 317 |
+
)}
|
| 318 |
+
|
| 319 |
+
<div
|
| 320 |
+
className={`${chatStyle["chat-input-action"]} clickable`}
|
| 321 |
+
onClick={nextTheme}
|
| 322 |
+
>
|
| 323 |
+
{theme === Theme.Auto ? (
|
| 324 |
+
<AutoIcon />
|
| 325 |
+
) : theme === Theme.Light ? (
|
| 326 |
+
<LightIcon />
|
| 327 |
+
) : theme === Theme.Dark ? (
|
| 328 |
+
<DarkIcon />
|
| 329 |
+
) : null}
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<div
|
| 333 |
+
className={`${chatStyle["chat-input-action"]} clickable`}
|
| 334 |
+
onClick={props.showPromptHints}
|
| 335 |
+
>
|
| 336 |
+
<PromptIcon />
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
<div
|
| 340 |
+
className={`${chatStyle["chat-input-action"]} clickable`}
|
| 341 |
+
onClick={() => {
|
| 342 |
+
navigate(Path.Masks);
|
| 343 |
+
}}
|
| 344 |
+
>
|
| 345 |
+
<MaskIcon />
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
export function Chat() {
|
| 352 |
+
type RenderMessage = Message & { preview?: boolean };
|
| 353 |
+
|
| 354 |
+
const chatStore = useChatStore();
|
| 355 |
+
const [session, sessionIndex] = useChatStore((state) => [
|
| 356 |
+
state.currentSession(),
|
| 357 |
+
state.currentSessionIndex,
|
| 358 |
+
]);
|
| 359 |
+
const config = useAppConfig();
|
| 360 |
+
const fontSize = config.fontSize;
|
| 361 |
+
|
| 362 |
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
| 363 |
+
const [userInput, setUserInput] = useState("");
|
| 364 |
+
const [beforeInput, setBeforeInput] = useState("");
|
| 365 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 366 |
+
const { submitKey, shouldSubmit } = useSubmitHandler();
|
| 367 |
+
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
| 368 |
+
const [hitBottom, setHitBottom] = useState(true);
|
| 369 |
+
const isMobileScreen = useMobileScreen();
|
| 370 |
+
const navigate = useNavigate();
|
| 371 |
+
|
| 372 |
+
const onChatBodyScroll = (e: HTMLElement) => {
|
| 373 |
+
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
| 374 |
+
setHitBottom(isTouchBottom);
|
| 375 |
+
};
|
| 376 |
+
|
| 377 |
+
// prompt hints
|
| 378 |
+
const promptStore = usePromptStore();
|
| 379 |
+
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
| 380 |
+
const onSearch = useDebouncedCallback(
|
| 381 |
+
(text: string) => {
|
| 382 |
+
setPromptHints(promptStore.search(text));
|
| 383 |
+
},
|
| 384 |
+
100,
|
| 385 |
+
{ leading: true, trailing: true },
|
| 386 |
+
);
|
| 387 |
+
|
| 388 |
+
const onPromptSelect = (prompt: Prompt) => {
|
| 389 |
+
setPromptHints([]);
|
| 390 |
+
inputRef.current?.focus();
|
| 391 |
+
setTimeout(() => setUserInput(prompt.content), 60);
|
| 392 |
+
};
|
| 393 |
+
|
| 394 |
+
// auto grow input
|
| 395 |
+
const [inputRows, setInputRows] = useState(2);
|
| 396 |
+
const measure = useDebouncedCallback(
|
| 397 |
+
() => {
|
| 398 |
+
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
| 399 |
+
const inputRows = Math.min(
|
| 400 |
+
5,
|
| 401 |
+
Math.max(2 + Number(!isMobileScreen), rows),
|
| 402 |
+
);
|
| 403 |
+
setInputRows(inputRows);
|
| 404 |
+
},
|
| 405 |
+
100,
|
| 406 |
+
{
|
| 407 |
+
leading: true,
|
| 408 |
+
trailing: true,
|
| 409 |
+
},
|
| 410 |
+
);
|
| 411 |
+
|
| 412 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 413 |
+
useEffect(measure, [userInput]);
|
| 414 |
+
|
| 415 |
+
// only search prompts when user input is short
|
| 416 |
+
const SEARCH_TEXT_LIMIT = 30;
|
| 417 |
+
const onInput = (text: string) => {
|
| 418 |
+
setUserInput(text);
|
| 419 |
+
const n = text.trim().length;
|
| 420 |
+
|
| 421 |
+
// clear search results
|
| 422 |
+
if (n === 0) {
|
| 423 |
+
setPromptHints([]);
|
| 424 |
+
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
| 425 |
+
// check if need to trigger auto completion
|
| 426 |
+
if (text.startsWith("/")) {
|
| 427 |
+
let searchText = text.slice(1);
|
| 428 |
+
onSearch(searchText);
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
};
|
| 432 |
+
|
| 433 |
+
// submit user input
|
| 434 |
+
const onUserSubmit = () => {
|
| 435 |
+
if (userInput.length <= 0) return;
|
| 436 |
+
setIsLoading(true);
|
| 437 |
+
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
| 438 |
+
setBeforeInput(userInput);
|
| 439 |
+
setUserInput("");
|
| 440 |
+
setPromptHints([]);
|
| 441 |
+
if (!isMobileScreen) inputRef.current?.focus();
|
| 442 |
+
setAutoScroll(true);
|
| 443 |
+
};
|
| 444 |
+
|
| 445 |
+
// stop response
|
| 446 |
+
const onUserStop = (messageId: number) => {
|
| 447 |
+
ControllerPool.stop(sessionIndex, messageId);
|
| 448 |
+
};
|
| 449 |
+
|
| 450 |
+
// check if should send message
|
| 451 |
+
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
| 452 |
+
// if ArrowUp and no userInput
|
| 453 |
+
if (e.key === "ArrowUp" && userInput.length <= 0) {
|
| 454 |
+
setUserInput(beforeInput);
|
| 455 |
+
e.preventDefault();
|
| 456 |
+
return;
|
| 457 |
+
}
|
| 458 |
+
if (shouldSubmit(e)) {
|
| 459 |
+
onUserSubmit();
|
| 460 |
+
e.preventDefault();
|
| 461 |
+
}
|
| 462 |
+
};
|
| 463 |
+
const onRightClick = (e: any, message: Message) => {
|
| 464 |
+
// auto fill user input
|
| 465 |
+
if (message.role === "user") {
|
| 466 |
+
setUserInput(message.content);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
// copy to clipboard
|
| 470 |
+
if (selectOrCopy(e.currentTarget, message.content)) {
|
| 471 |
+
e.preventDefault();
|
| 472 |
+
}
|
| 473 |
+
};
|
| 474 |
+
|
| 475 |
+
const findLastUserIndex = (messageId: number) => {
|
| 476 |
+
// find last user input message and resend
|
| 477 |
+
let lastUserMessageIndex: number | null = null;
|
| 478 |
+
for (let i = 0; i < session.messages.length; i += 1) {
|
| 479 |
+
const message = session.messages[i];
|
| 480 |
+
if (message.id === messageId) {
|
| 481 |
+
break;
|
| 482 |
+
}
|
| 483 |
+
if (message.role === "user") {
|
| 484 |
+
lastUserMessageIndex = i;
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
return lastUserMessageIndex;
|
| 489 |
+
};
|
| 490 |
+
|
| 491 |
+
const deleteMessage = (userIndex: number) => {
|
| 492 |
+
chatStore.updateCurrentSession((session) =>
|
| 493 |
+
session.messages.splice(userIndex, 2),
|
| 494 |
+
);
|
| 495 |
+
};
|
| 496 |
+
|
| 497 |
+
const onDelete = (botMessageId: number) => {
|
| 498 |
+
const userIndex = findLastUserIndex(botMessageId);
|
| 499 |
+
if (userIndex === null) return;
|
| 500 |
+
deleteMessage(userIndex);
|
| 501 |
+
};
|
| 502 |
+
|
| 503 |
+
const onResend = (botMessageId: number) => {
|
| 504 |
+
// find last user input message and resend
|
| 505 |
+
const userIndex = findLastUserIndex(botMessageId);
|
| 506 |
+
if (userIndex === null) return;
|
| 507 |
+
|
| 508 |
+
setIsLoading(true);
|
| 509 |
+
const content = session.messages[userIndex].content;
|
| 510 |
+
deleteMessage(userIndex);
|
| 511 |
+
chatStore.onUserInput(content).then(() => setIsLoading(false));
|
| 512 |
+
inputRef.current?.focus();
|
| 513 |
+
};
|
| 514 |
+
|
| 515 |
+
const context: RenderMessage[] = session.mask.context.slice();
|
| 516 |
+
|
| 517 |
+
const accessStore = useAccessStore();
|
| 518 |
+
|
| 519 |
+
if (
|
| 520 |
+
context.length === 0 &&
|
| 521 |
+
session.messages.at(0)?.content !== BOT_HELLO.content
|
| 522 |
+
) {
|
| 523 |
+
const copiedHello = Object.assign({}, BOT_HELLO);
|
| 524 |
+
if (!accessStore.isAuthorized()) {
|
| 525 |
+
copiedHello.content = Locale.Error.Unauthorized;
|
| 526 |
+
}
|
| 527 |
+
context.push(copiedHello);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
// preview messages
|
| 531 |
+
const messages = context
|
| 532 |
+
.concat(session.messages as RenderMessage[])
|
| 533 |
+
.concat(
|
| 534 |
+
isLoading
|
| 535 |
+
? [
|
| 536 |
+
{
|
| 537 |
+
...createMessage({
|
| 538 |
+
role: "assistant",
|
| 539 |
+
content: "……",
|
| 540 |
+
}),
|
| 541 |
+
preview: true,
|
| 542 |
+
},
|
| 543 |
+
]
|
| 544 |
+
: [],
|
| 545 |
+
)
|
| 546 |
+
.concat(
|
| 547 |
+
userInput.length > 0 && config.sendPreviewBubble
|
| 548 |
+
? [
|
| 549 |
+
{
|
| 550 |
+
...createMessage({
|
| 551 |
+
role: "user",
|
| 552 |
+
content: userInput,
|
| 553 |
+
}),
|
| 554 |
+
preview: true,
|
| 555 |
+
},
|
| 556 |
+
]
|
| 557 |
+
: [],
|
| 558 |
+
);
|
| 559 |
+
|
| 560 |
+
const [showPromptModal, setShowPromptModal] = useState(false);
|
| 561 |
+
|
| 562 |
+
const renameSession = () => {
|
| 563 |
+
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
| 564 |
+
if (newTopic && newTopic !== session.topic) {
|
| 565 |
+
chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
|
| 566 |
+
}
|
| 567 |
+
};
|
| 568 |
+
|
| 569 |
+
// Auto focus
|
| 570 |
+
useEffect(() => {
|
| 571 |
+
if (isMobileScreen) return;
|
| 572 |
+
inputRef.current?.focus();
|
| 573 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 574 |
+
}, []);
|
| 575 |
+
|
| 576 |
+
return (
|
| 577 |
+
<div className={styles.chat} key={session.id}>
|
| 578 |
+
<div className="window-header">
|
| 579 |
+
<div className="window-header-title">
|
| 580 |
+
<div
|
| 581 |
+
className={`window-header-main-title " ${styles["chat-body-title"]}`}
|
| 582 |
+
onClickCapture={renameSession}
|
| 583 |
+
>
|
| 584 |
+
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
| 585 |
+
</div>
|
| 586 |
+
<div className="window-header-sub-title">
|
| 587 |
+
{Locale.Chat.SubTitle(session.messages.length)}
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
<div className="window-actions">
|
| 591 |
+
<div className={"window-action-button" + " " + styles.mobile}>
|
| 592 |
+
<IconButton
|
| 593 |
+
icon={<ReturnIcon />}
|
| 594 |
+
bordered
|
| 595 |
+
title={Locale.Chat.Actions.ChatList}
|
| 596 |
+
onClick={() => navigate(Path.Home)}
|
| 597 |
+
/>
|
| 598 |
+
</div>
|
| 599 |
+
<div className="window-action-button">
|
| 600 |
+
<IconButton
|
| 601 |
+
icon={<RenameIcon />}
|
| 602 |
+
bordered
|
| 603 |
+
onClick={renameSession}
|
| 604 |
+
/>
|
| 605 |
+
</div>
|
| 606 |
+
<div className="window-action-button">
|
| 607 |
+
<IconButton
|
| 608 |
+
icon={<ExportIcon />}
|
| 609 |
+
bordered
|
| 610 |
+
title={Locale.Chat.Actions.Export}
|
| 611 |
+
onClick={() => {
|
| 612 |
+
exportMessages(
|
| 613 |
+
session.messages.filter((msg) => !msg.isError),
|
| 614 |
+
session.topic,
|
| 615 |
+
);
|
| 616 |
+
}}
|
| 617 |
+
/>
|
| 618 |
+
</div>
|
| 619 |
+
{!isMobileScreen && (
|
| 620 |
+
<div className="window-action-button">
|
| 621 |
+
<IconButton
|
| 622 |
+
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
| 623 |
+
bordered
|
| 624 |
+
onClick={() => {
|
| 625 |
+
config.update(
|
| 626 |
+
(config) => (config.tightBorder = !config.tightBorder),
|
| 627 |
+
);
|
| 628 |
+
}}
|
| 629 |
+
/>
|
| 630 |
+
</div>
|
| 631 |
+
)}
|
| 632 |
+
</div>
|
| 633 |
+
|
| 634 |
+
<PromptToast
|
| 635 |
+
showToast={!hitBottom}
|
| 636 |
+
showModal={showPromptModal}
|
| 637 |
+
setShowModal={setShowPromptModal}
|
| 638 |
+
/>
|
| 639 |
+
</div>
|
| 640 |
+
|
| 641 |
+
<div
|
| 642 |
+
className={styles["chat-body"]}
|
| 643 |
+
ref={scrollRef}
|
| 644 |
+
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
| 645 |
+
onMouseDown={() => inputRef.current?.blur()}
|
| 646 |
+
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
|
| 647 |
+
onTouchStart={() => {
|
| 648 |
+
inputRef.current?.blur();
|
| 649 |
+
setAutoScroll(false);
|
| 650 |
+
}}
|
| 651 |
+
>
|
| 652 |
+
{messages.map((message, i) => {
|
| 653 |
+
const isUser = message.role === "user";
|
| 654 |
+
const showActions =
|
| 655 |
+
!isUser &&
|
| 656 |
+
i > 0 &&
|
| 657 |
+
!(message.preview || message.content.length === 0);
|
| 658 |
+
const showTyping = message.preview || message.streaming;
|
| 659 |
+
|
| 660 |
+
return (
|
| 661 |
+
<div
|
| 662 |
+
key={i}
|
| 663 |
+
className={
|
| 664 |
+
isUser ? styles["chat-message-user"] : styles["chat-message"]
|
| 665 |
+
}
|
| 666 |
+
>
|
| 667 |
+
<div className={styles["chat-message-container"]}>
|
| 668 |
+
<div className={styles["chat-message-avatar"]}>
|
| 669 |
+
{message.role === "user" ? (
|
| 670 |
+
<Avatar avatar={config.avatar} />
|
| 671 |
+
) : (
|
| 672 |
+
<MaskAvatar mask={session.mask} />
|
| 673 |
+
)}
|
| 674 |
+
</div>
|
| 675 |
+
{showTyping && (
|
| 676 |
+
<div className={styles["chat-message-status"]}>
|
| 677 |
+
{Locale.Chat.Typing}
|
| 678 |
+
</div>
|
| 679 |
+
)}
|
| 680 |
+
<div className={styles["chat-message-item"]}>
|
| 681 |
+
{showActions && (
|
| 682 |
+
<div className={styles["chat-message-top-actions"]}>
|
| 683 |
+
{message.streaming ? (
|
| 684 |
+
<div
|
| 685 |
+
className={styles["chat-message-top-action"]}
|
| 686 |
+
onClick={() => onUserStop(message.id ?? i)}
|
| 687 |
+
>
|
| 688 |
+
{Locale.Chat.Actions.Stop}
|
| 689 |
+
</div>
|
| 690 |
+
) : (
|
| 691 |
+
<>
|
| 692 |
+
<div
|
| 693 |
+
className={styles["chat-message-top-action"]}
|
| 694 |
+
onClick={() => onDelete(message.id ?? i)}
|
| 695 |
+
>
|
| 696 |
+
{Locale.Chat.Actions.Delete}
|
| 697 |
+
</div>
|
| 698 |
+
<div
|
| 699 |
+
className={styles["chat-message-top-action"]}
|
| 700 |
+
onClick={() => onResend(message.id ?? i)}
|
| 701 |
+
>
|
| 702 |
+
{Locale.Chat.Actions.Retry}
|
| 703 |
+
</div>
|
| 704 |
+
</>
|
| 705 |
+
)}
|
| 706 |
+
|
| 707 |
+
<div
|
| 708 |
+
className={styles["chat-message-top-action"]}
|
| 709 |
+
onClick={() => copyToClipboard(message.content)}
|
| 710 |
+
>
|
| 711 |
+
{Locale.Chat.Actions.Copy}
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
)}
|
| 715 |
+
<Markdown
|
| 716 |
+
content={message.content}
|
| 717 |
+
loading={
|
| 718 |
+
(message.preview || message.content.length === 0) &&
|
| 719 |
+
!isUser
|
| 720 |
+
}
|
| 721 |
+
onContextMenu={(e) => onRightClick(e, message)}
|
| 722 |
+
onDoubleClickCapture={() => {
|
| 723 |
+
if (!isMobileScreen) return;
|
| 724 |
+
setUserInput(message.content);
|
| 725 |
+
}}
|
| 726 |
+
fontSize={fontSize}
|
| 727 |
+
parentRef={scrollRef}
|
| 728 |
+
defaultShow={i >= messages.length - 10}
|
| 729 |
+
/>
|
| 730 |
+
</div>
|
| 731 |
+
{!isUser && !message.preview && (
|
| 732 |
+
<div className={styles["chat-message-actions"]}>
|
| 733 |
+
<div className={styles["chat-message-action-date"]}>
|
| 734 |
+
{message.date.toLocaleString()}
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
)}
|
| 738 |
+
</div>
|
| 739 |
+
</div>
|
| 740 |
+
);
|
| 741 |
+
})}
|
| 742 |
+
</div>
|
| 743 |
+
|
| 744 |
+
<div className={styles["chat-input-panel"]}>
|
| 745 |
+
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
| 746 |
+
|
| 747 |
+
<ChatActions
|
| 748 |
+
showPromptModal={() => setShowPromptModal(true)}
|
| 749 |
+
scrollToBottom={scrollToBottom}
|
| 750 |
+
hitBottom={hitBottom}
|
| 751 |
+
showPromptHints={() => {
|
| 752 |
+
inputRef.current?.focus();
|
| 753 |
+
onSearch("");
|
| 754 |
+
}}
|
| 755 |
+
/>
|
| 756 |
+
<div className={styles["chat-input-panel-inner"]}>
|
| 757 |
+
<textarea
|
| 758 |
+
ref={inputRef}
|
| 759 |
+
className={styles["chat-input"]}
|
| 760 |
+
placeholder={Locale.Chat.Input(submitKey)}
|
| 761 |
+
onInput={(e) => onInput(e.currentTarget.value)}
|
| 762 |
+
value={userInput}
|
| 763 |
+
onKeyDown={onInputKeyDown}
|
| 764 |
+
onFocus={() => setAutoScroll(true)}
|
| 765 |
+
onBlur={() => {
|
| 766 |
+
setTimeout(() => {
|
| 767 |
+
if (document.activeElement !== inputRef.current) {
|
| 768 |
+
setAutoScroll(false);
|
| 769 |
+
setPromptHints([]);
|
| 770 |
+
}
|
| 771 |
+
}, 100);
|
| 772 |
+
}}
|
| 773 |
+
autoFocus
|
| 774 |
+
rows={inputRows}
|
| 775 |
+
/>
|
| 776 |
+
<IconButton
|
| 777 |
+
icon={<SendWhiteIcon />}
|
| 778 |
+
text={Locale.Chat.Send}
|
| 779 |
+
className={styles["chat-input-send"]}
|
| 780 |
+
type="primary"
|
| 781 |
+
onClick={onUserSubmit}
|
| 782 |
+
/>
|
| 783 |
+
</div>
|
| 784 |
+
</div>
|
| 785 |
+
</div>
|
| 786 |
+
);
|
| 787 |
+
}
|
app/components/emoji.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import EmojiPicker, {
|
| 2 |
+
Emoji,
|
| 3 |
+
EmojiStyle,
|
| 4 |
+
Theme as EmojiTheme,
|
| 5 |
+
} from "emoji-picker-react";
|
| 6 |
+
|
| 7 |
+
import { ModelType } from "../store";
|
| 8 |
+
|
| 9 |
+
import BotIcon from "../icons/bot.svg";
|
| 10 |
+
import BlackBotIcon from "../icons/black-bot.svg";
|
| 11 |
+
|
| 12 |
+
export function getEmojiUrl(unified: string, style: EmojiStyle) {
|
| 13 |
+
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function AvatarPicker(props: {
|
| 17 |
+
onEmojiClick: (emojiId: string) => void;
|
| 18 |
+
}) {
|
| 19 |
+
return (
|
| 20 |
+
<EmojiPicker
|
| 21 |
+
lazyLoadEmojis
|
| 22 |
+
theme={EmojiTheme.AUTO}
|
| 23 |
+
getEmojiUrl={getEmojiUrl}
|
| 24 |
+
onEmojiClick={(e) => {
|
| 25 |
+
props.onEmojiClick(e.unified);
|
| 26 |
+
}}
|
| 27 |
+
/>
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
| 32 |
+
if (props.model) {
|
| 33 |
+
return (
|
| 34 |
+
<div className="no-dark">
|
| 35 |
+
{props.model?.startsWith("gpt-4") ? (
|
| 36 |
+
<BlackBotIcon className="user-avatar" />
|
| 37 |
+
) : (
|
| 38 |
+
<BotIcon className="user-avatar" />
|
| 39 |
+
)}
|
| 40 |
+
</div>
|
| 41 |
+
);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<div className="user-avatar">
|
| 46 |
+
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
|
| 47 |
+
</div>
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export function EmojiAvatar(props: { avatar: string; size?: number }) {
|
| 52 |
+
return (
|
| 53 |
+
<Emoji
|
| 54 |
+
unified={props.avatar}
|
| 55 |
+
size={props.size ?? 18}
|
| 56 |
+
getEmojiUrl={getEmojiUrl}
|
| 57 |
+
/>
|
| 58 |
+
);
|
| 59 |
+
}
|
app/components/error.tsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import { IconButton } from "./button";
|
| 3 |
+
import GithubIcon from "../icons/github.svg";
|
| 4 |
+
import ResetIcon from "../icons/reload.svg";
|
| 5 |
+
import { ISSUE_URL } from "../constant";
|
| 6 |
+
import Locale from "../locales";
|
| 7 |
+
import { downloadAs } from "../utils";
|
| 8 |
+
|
| 9 |
+
interface IErrorBoundaryState {
|
| 10 |
+
hasError: boolean;
|
| 11 |
+
error: Error | null;
|
| 12 |
+
info: React.ErrorInfo | null;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
| 16 |
+
constructor(props: any) {
|
| 17 |
+
super(props);
|
| 18 |
+
this.state = { hasError: false, error: null, info: null };
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
| 22 |
+
// Update state with error details
|
| 23 |
+
this.setState({ hasError: true, error, info });
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
clearAndSaveData() {
|
| 27 |
+
try {
|
| 28 |
+
downloadAs(
|
| 29 |
+
JSON.stringify(localStorage),
|
| 30 |
+
"chatgpt-next-web-snapshot.json",
|
| 31 |
+
);
|
| 32 |
+
} finally {
|
| 33 |
+
localStorage.clear();
|
| 34 |
+
location.reload();
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
render() {
|
| 39 |
+
if (this.state.hasError) {
|
| 40 |
+
// Render error message
|
| 41 |
+
return (
|
| 42 |
+
<div className="error">
|
| 43 |
+
<h2>Oops, something went wrong!</h2>
|
| 44 |
+
<pre>
|
| 45 |
+
<code>{this.state.error?.toString()}</code>
|
| 46 |
+
<code>{this.state.info?.componentStack}</code>
|
| 47 |
+
</pre>
|
| 48 |
+
|
| 49 |
+
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
| 50 |
+
<a href={ISSUE_URL} className="report">
|
| 51 |
+
<IconButton
|
| 52 |
+
text="Report This Error"
|
| 53 |
+
icon={<GithubIcon />}
|
| 54 |
+
bordered
|
| 55 |
+
/>
|
| 56 |
+
</a>
|
| 57 |
+
<IconButton
|
| 58 |
+
icon={<ResetIcon />}
|
| 59 |
+
text="Clear All Data"
|
| 60 |
+
onClick={() =>
|
| 61 |
+
confirm(Locale.Settings.Actions.ConfirmClearAll) &&
|
| 62 |
+
this.clearAndSaveData()
|
| 63 |
+
}
|
| 64 |
+
bordered
|
| 65 |
+
/>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
);
|
| 69 |
+
}
|
| 70 |
+
// if no error occurred, render children
|
| 71 |
+
return this.props.children;
|
| 72 |
+
}
|
| 73 |
+
}
|
app/components/home.module.scss
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@mixin container {
|
| 2 |
+
background-color: var(--white);
|
| 3 |
+
border: var(--border-in-light);
|
| 4 |
+
border-radius: 20px;
|
| 5 |
+
box-shadow: var(--shadow);
|
| 6 |
+
color: var(--black);
|
| 7 |
+
background-color: var(--white);
|
| 8 |
+
min-width: 600px;
|
| 9 |
+
min-height: 480px;
|
| 10 |
+
max-width: 1200px;
|
| 11 |
+
|
| 12 |
+
display: flex;
|
| 13 |
+
overflow: hidden;
|
| 14 |
+
box-sizing: border-box;
|
| 15 |
+
|
| 16 |
+
width: var(--window-width);
|
| 17 |
+
height: var(--window-height);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.container {
|
| 21 |
+
@include container();
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
@media only screen and (min-width: 600px) {
|
| 25 |
+
.tight-container {
|
| 26 |
+
--window-width: 100vw;
|
| 27 |
+
--window-height: var(--full-height);
|
| 28 |
+
--window-content-width: calc(100% - var(--sidebar-width));
|
| 29 |
+
|
| 30 |
+
@include container();
|
| 31 |
+
|
| 32 |
+
max-width: 100vw;
|
| 33 |
+
max-height: var(--full-height);
|
| 34 |
+
|
| 35 |
+
border-radius: 0;
|
| 36 |
+
border: 0;
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.sidebar {
|
| 41 |
+
top: 0;
|
| 42 |
+
width: var(--sidebar-width);
|
| 43 |
+
box-sizing: border-box;
|
| 44 |
+
padding: 20px;
|
| 45 |
+
background-color: var(--second);
|
| 46 |
+
display: flex;
|
| 47 |
+
flex-direction: column;
|
| 48 |
+
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
| 49 |
+
position: relative;
|
| 50 |
+
transition: width ease 0.05s;
|
| 51 |
+
|
| 52 |
+
.sidebar-header-bar {
|
| 53 |
+
display: flex;
|
| 54 |
+
margin-bottom: 20px;
|
| 55 |
+
|
| 56 |
+
.sidebar-bar-button {
|
| 57 |
+
flex-grow: 1;
|
| 58 |
+
|
| 59 |
+
&:not(:last-child) {
|
| 60 |
+
margin-right: 10px;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.sidebar-drag {
|
| 67 |
+
$width: 10px;
|
| 68 |
+
|
| 69 |
+
position: absolute;
|
| 70 |
+
top: 0;
|
| 71 |
+
right: 0;
|
| 72 |
+
height: 100%;
|
| 73 |
+
width: $width;
|
| 74 |
+
background-color: var(--black);
|
| 75 |
+
cursor: ew-resize;
|
| 76 |
+
opacity: 0;
|
| 77 |
+
transition: all ease 0.3s;
|
| 78 |
+
|
| 79 |
+
&:hover,
|
| 80 |
+
&:active {
|
| 81 |
+
opacity: 0.2;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.window-content {
|
| 86 |
+
width: var(--window-content-width);
|
| 87 |
+
height: 100%;
|
| 88 |
+
display: flex;
|
| 89 |
+
flex-direction: column;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.mobile {
|
| 93 |
+
display: none;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
@media only screen and (max-width: 600px) {
|
| 97 |
+
.container {
|
| 98 |
+
min-height: unset;
|
| 99 |
+
min-width: unset;
|
| 100 |
+
max-height: unset;
|
| 101 |
+
min-width: unset;
|
| 102 |
+
border: 0;
|
| 103 |
+
border-radius: 0;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.sidebar {
|
| 107 |
+
position: absolute;
|
| 108 |
+
left: -100%;
|
| 109 |
+
z-index: 1000;
|
| 110 |
+
height: var(--full-height);
|
| 111 |
+
transition: all ease 0.3s;
|
| 112 |
+
box-shadow: none;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.sidebar-show {
|
| 116 |
+
left: 0;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.mobile {
|
| 120 |
+
display: block;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.sidebar-header {
|
| 125 |
+
position: relative;
|
| 126 |
+
padding-top: 20px;
|
| 127 |
+
padding-bottom: 20px;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.sidebar-logo {
|
| 131 |
+
position: absolute;
|
| 132 |
+
right: 0;
|
| 133 |
+
bottom: 18px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.sidebar-title {
|
| 137 |
+
font-size: 20px;
|
| 138 |
+
font-weight: bold;
|
| 139 |
+
animation: slide-in ease 0.3s;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.sidebar-sub-title {
|
| 143 |
+
font-size: 12px;
|
| 144 |
+
font-weight: 400px;
|
| 145 |
+
animation: slide-in ease 0.3s;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.sidebar-body {
|
| 149 |
+
flex: 1;
|
| 150 |
+
overflow: auto;
|
| 151 |
+
overflow-x: hidden;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.chat-item {
|
| 155 |
+
padding: 10px 14px;
|
| 156 |
+
background-color: var(--white);
|
| 157 |
+
border-radius: 10px;
|
| 158 |
+
margin-bottom: 10px;
|
| 159 |
+
box-shadow: var(--card-shadow);
|
| 160 |
+
transition: background-color 0.3s ease;
|
| 161 |
+
cursor: pointer;
|
| 162 |
+
user-select: none;
|
| 163 |
+
border: 2px solid transparent;
|
| 164 |
+
position: relative;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.chat-item:hover {
|
| 168 |
+
background-color: var(--hover-color);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.chat-item-selected {
|
| 172 |
+
border-color: var(--primary);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.chat-item-title {
|
| 176 |
+
font-size: 14px;
|
| 177 |
+
font-weight: bolder;
|
| 178 |
+
display: block;
|
| 179 |
+
width: 200px;
|
| 180 |
+
overflow: hidden;
|
| 181 |
+
text-overflow: ellipsis;
|
| 182 |
+
white-space: nowrap;
|
| 183 |
+
animation: slide-in ease 0.3s;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.chat-item-delete {
|
| 187 |
+
position: absolute;
|
| 188 |
+
top: 10px;
|
| 189 |
+
right: -20px;
|
| 190 |
+
transition: all ease 0.3s;
|
| 191 |
+
opacity: 0;
|
| 192 |
+
cursor: pointer;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.chat-item:hover > .chat-item-delete {
|
| 196 |
+
opacity: 0.5;
|
| 197 |
+
right: 10px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.chat-item:hover > .chat-item-delete:hover {
|
| 201 |
+
opacity: 1;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.chat-item-info {
|
| 205 |
+
display: flex;
|
| 206 |
+
justify-content: space-between;
|
| 207 |
+
color: rgb(166, 166, 166);
|
| 208 |
+
font-size: 12px;
|
| 209 |
+
margin-top: 8px;
|
| 210 |
+
animation: slide-in ease 0.3s;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.chat-item-count,
|
| 214 |
+
.chat-item-date {
|
| 215 |
+
overflow: hidden;
|
| 216 |
+
text-overflow: ellipsis;
|
| 217 |
+
white-space: nowrap;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.narrow-sidebar {
|
| 221 |
+
.sidebar-title,
|
| 222 |
+
.sidebar-sub-title {
|
| 223 |
+
display: none;
|
| 224 |
+
}
|
| 225 |
+
.sidebar-logo {
|
| 226 |
+
position: relative;
|
| 227 |
+
display: flex;
|
| 228 |
+
justify-content: center;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.sidebar-header-bar {
|
| 232 |
+
flex-direction: column;
|
| 233 |
+
|
| 234 |
+
.sidebar-bar-button {
|
| 235 |
+
&:not(:last-child) {
|
| 236 |
+
margin-right: 0;
|
| 237 |
+
margin-bottom: 10px;
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.chat-item {
|
| 243 |
+
padding: 0;
|
| 244 |
+
min-height: 50px;
|
| 245 |
+
display: flex;
|
| 246 |
+
justify-content: center;
|
| 247 |
+
align-items: center;
|
| 248 |
+
transition: all ease 0.3s;
|
| 249 |
+
overflow: hidden;
|
| 250 |
+
|
| 251 |
+
&:hover {
|
| 252 |
+
.chat-item-narrow {
|
| 253 |
+
transform: scale(0.7) translateX(-50%);
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.chat-item-narrow {
|
| 259 |
+
line-height: 0;
|
| 260 |
+
font-weight: lighter;
|
| 261 |
+
color: var(--black);
|
| 262 |
+
transform: translateX(0);
|
| 263 |
+
transition: all ease 0.3s;
|
| 264 |
+
padding: 4px;
|
| 265 |
+
display: flex;
|
| 266 |
+
flex-direction: column;
|
| 267 |
+
justify-content: center;
|
| 268 |
+
|
| 269 |
+
.chat-item-avatar {
|
| 270 |
+
display: flex;
|
| 271 |
+
justify-content: center;
|
| 272 |
+
opacity: 0.2;
|
| 273 |
+
position: absolute;
|
| 274 |
+
transform: scale(4);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.chat-item-narrow-count {
|
| 278 |
+
font-size: 24px;
|
| 279 |
+
font-weight: bolder;
|
| 280 |
+
text-align: center;
|
| 281 |
+
color: var(--primary);
|
| 282 |
+
opacity: 0.6;
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.chat-item-delete {
|
| 287 |
+
top: 15px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.chat-item:hover > .chat-item-delete {
|
| 291 |
+
opacity: 0.5;
|
| 292 |
+
right: 5px;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.sidebar-tail {
|
| 296 |
+
flex-direction: column-reverse;
|
| 297 |
+
align-items: center;
|
| 298 |
+
|
| 299 |
+
.sidebar-actions {
|
| 300 |
+
flex-direction: column-reverse;
|
| 301 |
+
align-items: center;
|
| 302 |
+
|
| 303 |
+
.sidebar-action {
|
| 304 |
+
margin-right: 0;
|
| 305 |
+
margin-top: 15px;
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.sidebar-tail {
|
| 312 |
+
display: flex;
|
| 313 |
+
justify-content: space-between;
|
| 314 |
+
padding-top: 20px;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.sidebar-actions {
|
| 318 |
+
display: inline-flex;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.sidebar-action:not(:last-child) {
|
| 322 |
+
margin-right: 15px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.chat {
|
| 326 |
+
display: flex;
|
| 327 |
+
flex-direction: column;
|
| 328 |
+
position: relative;
|
| 329 |
+
height: 100%;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.chat-body {
|
| 333 |
+
flex: 1;
|
| 334 |
+
overflow: auto;
|
| 335 |
+
padding: 20px;
|
| 336 |
+
padding-bottom: 40px;
|
| 337 |
+
position: relative;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.chat-body-title {
|
| 341 |
+
cursor: pointer;
|
| 342 |
+
|
| 343 |
+
&:hover {
|
| 344 |
+
text-decoration: underline;
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.chat-message {
|
| 349 |
+
display: flex;
|
| 350 |
+
flex-direction: row;
|
| 351 |
+
|
| 352 |
+
&:last-child {
|
| 353 |
+
animation: slide-in ease 0.3s;
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.chat-message-user {
|
| 358 |
+
display: flex;
|
| 359 |
+
flex-direction: row-reverse;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.chat-message-container {
|
| 363 |
+
max-width: var(--message-max-width);
|
| 364 |
+
display: flex;
|
| 365 |
+
flex-direction: column;
|
| 366 |
+
align-items: flex-start;
|
| 367 |
+
|
| 368 |
+
&:hover {
|
| 369 |
+
.chat-message-top-actions {
|
| 370 |
+
opacity: 1;
|
| 371 |
+
right: 10px;
|
| 372 |
+
pointer-events: all;
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.chat-message-user > .chat-message-container {
|
| 378 |
+
align-items: flex-end;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.chat-message-avatar {
|
| 382 |
+
margin-top: 20px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.chat-message-status {
|
| 386 |
+
font-size: 12px;
|
| 387 |
+
color: #aaa;
|
| 388 |
+
line-height: 1.5;
|
| 389 |
+
margin-top: 5px;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.chat-message-item {
|
| 393 |
+
box-sizing: border-box;
|
| 394 |
+
max-width: 100%;
|
| 395 |
+
margin-top: 10px;
|
| 396 |
+
border-radius: 10px;
|
| 397 |
+
background-color: rgba(0, 0, 0, 0.05);
|
| 398 |
+
padding: 10px;
|
| 399 |
+
font-size: 14px;
|
| 400 |
+
user-select: text;
|
| 401 |
+
word-break: break-word;
|
| 402 |
+
border: var(--border-in-light);
|
| 403 |
+
position: relative;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.chat-message-top-actions {
|
| 407 |
+
font-size: 12px;
|
| 408 |
+
position: absolute;
|
| 409 |
+
right: 20px;
|
| 410 |
+
top: -26px;
|
| 411 |
+
left: 100px;
|
| 412 |
+
transition: all ease 0.3s;
|
| 413 |
+
opacity: 0;
|
| 414 |
+
pointer-events: none;
|
| 415 |
+
|
| 416 |
+
display: flex;
|
| 417 |
+
flex-direction: row-reverse;
|
| 418 |
+
|
| 419 |
+
.chat-message-top-action {
|
| 420 |
+
opacity: 0.5;
|
| 421 |
+
color: var(--black);
|
| 422 |
+
white-space: nowrap;
|
| 423 |
+
cursor: pointer;
|
| 424 |
+
|
| 425 |
+
&:hover {
|
| 426 |
+
opacity: 1;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
&:not(:first-child) {
|
| 430 |
+
margin-right: 10px;
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.chat-message-user > .chat-message-container > .chat-message-item {
|
| 436 |
+
background-color: var(--second);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.chat-message-actions {
|
| 440 |
+
display: flex;
|
| 441 |
+
flex-direction: row-reverse;
|
| 442 |
+
width: 100%;
|
| 443 |
+
padding-top: 5px;
|
| 444 |
+
box-sizing: border-box;
|
| 445 |
+
font-size: 12px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.chat-message-action-date {
|
| 449 |
+
color: #aaa;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.chat-input-panel {
|
| 453 |
+
position: relative;
|
| 454 |
+
width: 100%;
|
| 455 |
+
padding: 20px;
|
| 456 |
+
padding-top: 10px;
|
| 457 |
+
box-sizing: border-box;
|
| 458 |
+
flex-direction: column;
|
| 459 |
+
border-top-left-radius: 10px;
|
| 460 |
+
border-top-right-radius: 10px;
|
| 461 |
+
border-top: var(--border-in-light);
|
| 462 |
+
box-shadow: var(--card-shadow);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
@mixin single-line {
|
| 466 |
+
white-space: nowrap;
|
| 467 |
+
overflow: hidden;
|
| 468 |
+
text-overflow: ellipsis;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.prompt-hints {
|
| 472 |
+
min-height: 20px;
|
| 473 |
+
width: 100%;
|
| 474 |
+
max-height: 50vh;
|
| 475 |
+
overflow: auto;
|
| 476 |
+
display: flex;
|
| 477 |
+
flex-direction: column-reverse;
|
| 478 |
+
|
| 479 |
+
background-color: var(--white);
|
| 480 |
+
border: var(--border-in-light);
|
| 481 |
+
border-radius: 10px;
|
| 482 |
+
margin-bottom: 10px;
|
| 483 |
+
box-shadow: var(--shadow);
|
| 484 |
+
|
| 485 |
+
.prompt-hint {
|
| 486 |
+
color: var(--black);
|
| 487 |
+
padding: 6px 10px;
|
| 488 |
+
animation: slide-in ease 0.3s;
|
| 489 |
+
cursor: pointer;
|
| 490 |
+
transition: all ease 0.3s;
|
| 491 |
+
border: transparent 1px solid;
|
| 492 |
+
margin: 4px;
|
| 493 |
+
border-radius: 8px;
|
| 494 |
+
|
| 495 |
+
&:not(:last-child) {
|
| 496 |
+
margin-top: 0;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.hint-title {
|
| 500 |
+
font-size: 12px;
|
| 501 |
+
font-weight: bolder;
|
| 502 |
+
|
| 503 |
+
@include single-line();
|
| 504 |
+
}
|
| 505 |
+
.hint-content {
|
| 506 |
+
font-size: 12px;
|
| 507 |
+
|
| 508 |
+
@include single-line();
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
&-selected,
|
| 512 |
+
&:hover {
|
| 513 |
+
border-color: var(--primary);
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
.chat-input-panel-inner {
|
| 519 |
+
display: flex;
|
| 520 |
+
flex: 1;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.chat-input {
|
| 524 |
+
height: 100%;
|
| 525 |
+
width: 100%;
|
| 526 |
+
border-radius: 10px;
|
| 527 |
+
border: var(--border-in-light);
|
| 528 |
+
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
| 529 |
+
background-color: var(--white);
|
| 530 |
+
color: var(--black);
|
| 531 |
+
font-family: inherit;
|
| 532 |
+
padding: 10px 90px 10px 14px;
|
| 533 |
+
resize: none;
|
| 534 |
+
outline: none;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.chat-input:focus {
|
| 538 |
+
border: 1px solid var(--primary);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.chat-input-send {
|
| 542 |
+
background-color: var(--primary);
|
| 543 |
+
color: white;
|
| 544 |
+
|
| 545 |
+
position: absolute;
|
| 546 |
+
right: 30px;
|
| 547 |
+
bottom: 32px;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
@media only screen and (max-width: 600px) {
|
| 551 |
+
.chat-input {
|
| 552 |
+
font-size: 16px;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.chat-input-send {
|
| 556 |
+
bottom: 30px;
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.export-content {
|
| 561 |
+
white-space: break-spaces;
|
| 562 |
+
padding: 10px !important;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.loading-content {
|
| 566 |
+
display: flex;
|
| 567 |
+
flex-direction: column;
|
| 568 |
+
justify-content: center;
|
| 569 |
+
align-items: center;
|
| 570 |
+
height: 100%;
|
| 571 |
+
width: 100%;
|
| 572 |
+
}
|
app/components/home.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
require("../polyfill");
|
| 4 |
+
|
| 5 |
+
import { useState, useEffect } from "react";
|
| 6 |
+
|
| 7 |
+
import styles from "./home.module.scss";
|
| 8 |
+
|
| 9 |
+
import BotIcon from "../icons/bot.svg";
|
| 10 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
| 11 |
+
|
| 12 |
+
import { getCSSVar, useMobileScreen } from "../utils";
|
| 13 |
+
|
| 14 |
+
import dynamic from "next/dynamic";
|
| 15 |
+
import { Path, SlotID } from "../constant";
|
| 16 |
+
import { ErrorBoundary } from "./error";
|
| 17 |
+
|
| 18 |
+
import {
|
| 19 |
+
HashRouter as Router,
|
| 20 |
+
Routes,
|
| 21 |
+
Route,
|
| 22 |
+
useLocation,
|
| 23 |
+
} from "react-router-dom";
|
| 24 |
+
import { SideBar } from "./sidebar";
|
| 25 |
+
import { useAppConfig } from "../store/config";
|
| 26 |
+
|
| 27 |
+
export function Loading(props: { noLogo?: boolean }) {
|
| 28 |
+
return (
|
| 29 |
+
<div className={styles["loading-content"] + " no-dark"}>
|
| 30 |
+
{!props.noLogo && <BotIcon />}
|
| 31 |
+
<LoadingIcon />
|
| 32 |
+
</div>
|
| 33 |
+
);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
| 37 |
+
loading: () => <Loading noLogo />,
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
const Chat = dynamic(async () => (await import("./chat")).Chat, {
|
| 41 |
+
loading: () => <Loading noLogo />,
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, {
|
| 45 |
+
loading: () => <Loading noLogo />,
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
| 49 |
+
loading: () => <Loading noLogo />,
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
export function useSwitchTheme() {
|
| 53 |
+
const config = useAppConfig();
|
| 54 |
+
|
| 55 |
+
useEffect(() => {
|
| 56 |
+
document.body.classList.remove("light");
|
| 57 |
+
document.body.classList.remove("dark");
|
| 58 |
+
|
| 59 |
+
if (config.theme === "dark") {
|
| 60 |
+
document.body.classList.add("dark");
|
| 61 |
+
} else if (config.theme === "light") {
|
| 62 |
+
document.body.classList.add("light");
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const metaDescriptionDark = document.querySelector(
|
| 66 |
+
'meta[name="theme-color"][media]',
|
| 67 |
+
);
|
| 68 |
+
const metaDescriptionLight = document.querySelector(
|
| 69 |
+
'meta[name="theme-color"]:not([media])',
|
| 70 |
+
);
|
| 71 |
+
|
| 72 |
+
if (config.theme === "auto") {
|
| 73 |
+
metaDescriptionDark?.setAttribute("content", "#151515");
|
| 74 |
+
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
| 75 |
+
} else {
|
| 76 |
+
const themeColor = getCSSVar("--themeColor");
|
| 77 |
+
metaDescriptionDark?.setAttribute("content", themeColor);
|
| 78 |
+
metaDescriptionLight?.setAttribute("content", themeColor);
|
| 79 |
+
}
|
| 80 |
+
}, [config.theme]);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const useHasHydrated = () => {
|
| 84 |
+
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
| 85 |
+
|
| 86 |
+
useEffect(() => {
|
| 87 |
+
setHasHydrated(true);
|
| 88 |
+
}, []);
|
| 89 |
+
|
| 90 |
+
return hasHydrated;
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
function Screen() {
|
| 94 |
+
const config = useAppConfig();
|
| 95 |
+
const location = useLocation();
|
| 96 |
+
const isHome = location.pathname === Path.Home;
|
| 97 |
+
const isMobileScreen = useMobileScreen();
|
| 98 |
+
|
| 99 |
+
return (
|
| 100 |
+
<div
|
| 101 |
+
className={
|
| 102 |
+
styles.container +
|
| 103 |
+
` ${
|
| 104 |
+
config.tightBorder && !isMobileScreen
|
| 105 |
+
? styles["tight-container"]
|
| 106 |
+
: styles.container
|
| 107 |
+
}`
|
| 108 |
+
}
|
| 109 |
+
>
|
| 110 |
+
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
| 111 |
+
|
| 112 |
+
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
| 113 |
+
<Routes>
|
| 114 |
+
<Route path={Path.Home} element={<Chat />} />
|
| 115 |
+
<Route path={Path.NewChat} element={<NewChat />} />
|
| 116 |
+
<Route path={Path.Masks} element={<MaskPage />} />
|
| 117 |
+
<Route path={Path.Chat} element={<Chat />} />
|
| 118 |
+
<Route path={Path.Settings} element={<Settings />} />
|
| 119 |
+
</Routes>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export function Home() {
|
| 126 |
+
useSwitchTheme();
|
| 127 |
+
|
| 128 |
+
if (!useHasHydrated()) {
|
| 129 |
+
return <Loading />;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
return (
|
| 133 |
+
<ErrorBoundary>
|
| 134 |
+
<Router>
|
| 135 |
+
<Screen />
|
| 136 |
+
</Router>
|
| 137 |
+
</ErrorBoundary>
|
| 138 |
+
);
|
| 139 |
+
}
|
app/components/input-range.module.scss
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.input-range {
|
| 2 |
+
border: var(--border-in-light);
|
| 3 |
+
border-radius: 10px;
|
| 4 |
+
padding: 5px 15px 5px 10px;
|
| 5 |
+
font-size: 12px;
|
| 6 |
+
display: flex;
|
| 7 |
+
}
|
app/components/input-range.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react";
|
| 2 |
+
import styles from "./input-range.module.scss";
|
| 3 |
+
|
| 4 |
+
interface InputRangeProps {
|
| 5 |
+
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
| 6 |
+
title?: string;
|
| 7 |
+
value: number | string;
|
| 8 |
+
className?: string;
|
| 9 |
+
min: string;
|
| 10 |
+
max: string;
|
| 11 |
+
step: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export function InputRange({
|
| 15 |
+
onChange,
|
| 16 |
+
title,
|
| 17 |
+
value,
|
| 18 |
+
className,
|
| 19 |
+
min,
|
| 20 |
+
max,
|
| 21 |
+
step,
|
| 22 |
+
}: InputRangeProps) {
|
| 23 |
+
return (
|
| 24 |
+
<div className={styles["input-range"] + ` ${className ?? ""}`}>
|
| 25 |
+
{title || value}
|
| 26 |
+
<input
|
| 27 |
+
type="range"
|
| 28 |
+
title={title}
|
| 29 |
+
value={value}
|
| 30 |
+
min={min}
|
| 31 |
+
max={max}
|
| 32 |
+
step={step}
|
| 33 |
+
onChange={onChange}
|
| 34 |
+
></input>
|
| 35 |
+
</div>
|
| 36 |
+
);
|
| 37 |
+
}
|
app/components/markdown.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ReactMarkdown from "react-markdown";
|
| 2 |
+
import "katex/dist/katex.min.css";
|
| 3 |
+
import RemarkMath from "remark-math";
|
| 4 |
+
import RemarkBreaks from "remark-breaks";
|
| 5 |
+
import RehypeKatex from "rehype-katex";
|
| 6 |
+
import RemarkGfm from "remark-gfm";
|
| 7 |
+
import RehypeHighlight from "rehype-highlight";
|
| 8 |
+
import { useRef, useState, RefObject, useEffect } from "react";
|
| 9 |
+
import { copyToClipboard } from "../utils";
|
| 10 |
+
|
| 11 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
| 12 |
+
import React from "react";
|
| 13 |
+
|
| 14 |
+
export function PreCode(props: { children: any }) {
|
| 15 |
+
const ref = useRef<HTMLPreElement>(null);
|
| 16 |
+
|
| 17 |
+
return (
|
| 18 |
+
<pre ref={ref}>
|
| 19 |
+
<span
|
| 20 |
+
className="copy-code-button"
|
| 21 |
+
onClick={() => {
|
| 22 |
+
if (ref.current) {
|
| 23 |
+
const code = ref.current.innerText;
|
| 24 |
+
copyToClipboard(code);
|
| 25 |
+
}
|
| 26 |
+
}}
|
| 27 |
+
></span>
|
| 28 |
+
{props.children}
|
| 29 |
+
</pre>
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function _MarkDownContent(props: { content: string }) {
|
| 34 |
+
return (
|
| 35 |
+
<ReactMarkdown
|
| 36 |
+
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
| 37 |
+
rehypePlugins={[
|
| 38 |
+
RehypeKatex,
|
| 39 |
+
[
|
| 40 |
+
RehypeHighlight,
|
| 41 |
+
{
|
| 42 |
+
detect: false,
|
| 43 |
+
ignoreMissing: true,
|
| 44 |
+
},
|
| 45 |
+
],
|
| 46 |
+
]}
|
| 47 |
+
components={{
|
| 48 |
+
pre: PreCode,
|
| 49 |
+
a: (aProps) => {
|
| 50 |
+
const href = aProps.href || "";
|
| 51 |
+
const isInternal = /^\/#/i.test(href);
|
| 52 |
+
const target = isInternal ? "_self" : aProps.target ?? "_blank";
|
| 53 |
+
return <a {...aProps} target={target} />;
|
| 54 |
+
},
|
| 55 |
+
}}
|
| 56 |
+
>
|
| 57 |
+
{props.content}
|
| 58 |
+
</ReactMarkdown>
|
| 59 |
+
);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export const MarkdownContent = React.memo(_MarkDownContent);
|
| 63 |
+
|
| 64 |
+
export function Markdown(
|
| 65 |
+
props: {
|
| 66 |
+
content: string;
|
| 67 |
+
loading?: boolean;
|
| 68 |
+
fontSize?: number;
|
| 69 |
+
parentRef: RefObject<HTMLDivElement>;
|
| 70 |
+
defaultShow?: boolean;
|
| 71 |
+
} & React.DOMAttributes<HTMLDivElement>,
|
| 72 |
+
) {
|
| 73 |
+
const mdRef = useRef<HTMLDivElement>(null);
|
| 74 |
+
const renderedHeight = useRef(0);
|
| 75 |
+
const inView = useRef(!!props.defaultShow);
|
| 76 |
+
|
| 77 |
+
const parent = props.parentRef.current;
|
| 78 |
+
const md = mdRef.current;
|
| 79 |
+
|
| 80 |
+
const checkInView = () => {
|
| 81 |
+
if (parent && md) {
|
| 82 |
+
const parentBounds = parent.getBoundingClientRect();
|
| 83 |
+
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
|
| 84 |
+
const mdBounds = md.getBoundingClientRect();
|
| 85 |
+
const isInRange = (x: number) =>
|
| 86 |
+
x <= parentBounds.bottom + twoScreenHeight &&
|
| 87 |
+
x >= parentBounds.top - twoScreenHeight;
|
| 88 |
+
inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
if (inView.current && md) {
|
| 92 |
+
renderedHeight.current = Math.max(
|
| 93 |
+
renderedHeight.current,
|
| 94 |
+
md.getBoundingClientRect().height,
|
| 95 |
+
);
|
| 96 |
+
}
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
checkInView();
|
| 100 |
+
|
| 101 |
+
return (
|
| 102 |
+
<div
|
| 103 |
+
className="markdown-body"
|
| 104 |
+
style={{
|
| 105 |
+
fontSize: `${props.fontSize ?? 14}px`,
|
| 106 |
+
height:
|
| 107 |
+
!inView.current && renderedHeight.current > 0
|
| 108 |
+
? renderedHeight.current
|
| 109 |
+
: "auto",
|
| 110 |
+
}}
|
| 111 |
+
ref={mdRef}
|
| 112 |
+
onContextMenu={props.onContextMenu}
|
| 113 |
+
onDoubleClickCapture={props.onDoubleClickCapture}
|
| 114 |
+
>
|
| 115 |
+
{inView.current &&
|
| 116 |
+
(props.loading ? (
|
| 117 |
+
<LoadingIcon />
|
| 118 |
+
) : (
|
| 119 |
+
<MarkdownContent content={props.content} />
|
| 120 |
+
))}
|
| 121 |
+
</div>
|
| 122 |
+
);
|
| 123 |
+
}
|
app/components/mask.module.scss
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "../styles/animation.scss";
|
| 2 |
+
.mask-page {
|
| 3 |
+
height: 100%;
|
| 4 |
+
display: flex;
|
| 5 |
+
flex-direction: column;
|
| 6 |
+
|
| 7 |
+
.mask-page-body {
|
| 8 |
+
padding: 20px;
|
| 9 |
+
overflow-y: auto;
|
| 10 |
+
|
| 11 |
+
.mask-filter {
|
| 12 |
+
width: 100%;
|
| 13 |
+
max-width: 100%;
|
| 14 |
+
margin-bottom: 20px;
|
| 15 |
+
animation: slide-in ease 0.3s;
|
| 16 |
+
height: 40px;
|
| 17 |
+
|
| 18 |
+
display: flex;
|
| 19 |
+
|
| 20 |
+
.search-bar {
|
| 21 |
+
flex-grow: 1;
|
| 22 |
+
max-width: 100%;
|
| 23 |
+
min-width: 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.mask-filter-lang {
|
| 27 |
+
height: 100%;
|
| 28 |
+
margin-left: 10px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.mask-create {
|
| 32 |
+
height: 100%;
|
| 33 |
+
margin-left: 10px;
|
| 34 |
+
box-sizing: border-box;
|
| 35 |
+
min-width: 80px;
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.mask-item {
|
| 40 |
+
display: flex;
|
| 41 |
+
justify-content: space-between;
|
| 42 |
+
padding: 20px;
|
| 43 |
+
border: var(--border-in-light);
|
| 44 |
+
animation: slide-in ease 0.3s;
|
| 45 |
+
|
| 46 |
+
&:not(:last-child) {
|
| 47 |
+
border-bottom: 0;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
&:first-child {
|
| 51 |
+
border-top-left-radius: 10px;
|
| 52 |
+
border-top-right-radius: 10px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
&:last-child {
|
| 56 |
+
border-bottom-left-radius: 10px;
|
| 57 |
+
border-bottom-right-radius: 10px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.mask-header {
|
| 61 |
+
display: flex;
|
| 62 |
+
align-items: center;
|
| 63 |
+
|
| 64 |
+
.mask-icon {
|
| 65 |
+
display: flex;
|
| 66 |
+
align-items: center;
|
| 67 |
+
justify-content: center;
|
| 68 |
+
margin-right: 10px;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.mask-title {
|
| 72 |
+
.mask-name {
|
| 73 |
+
font-size: 14px;
|
| 74 |
+
font-weight: bold;
|
| 75 |
+
}
|
| 76 |
+
.mask-info {
|
| 77 |
+
font-size: 12px;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.mask-actions {
|
| 83 |
+
display: flex;
|
| 84 |
+
flex-wrap: nowrap;
|
| 85 |
+
transition: all ease 0.3s;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
@media screen and (max-width: 600px) {
|
| 89 |
+
display: flex;
|
| 90 |
+
flex-direction: column;
|
| 91 |
+
padding-bottom: 10px;
|
| 92 |
+
border-radius: 10px;
|
| 93 |
+
margin-bottom: 20px;
|
| 94 |
+
box-shadow: var(--card-shadow);
|
| 95 |
+
|
| 96 |
+
&:not(:last-child) {
|
| 97 |
+
border-bottom: var(--border-in-light);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.mask-actions {
|
| 101 |
+
width: 100%;
|
| 102 |
+
justify-content: space-between;
|
| 103 |
+
padding-top: 10px;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
}
|
app/components/mask.tsx
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IconButton } from "./button";
|
| 2 |
+
import { ErrorBoundary } from "./error";
|
| 3 |
+
|
| 4 |
+
import styles from "./mask.module.scss";
|
| 5 |
+
|
| 6 |
+
import DownloadIcon from "../icons/download.svg";
|
| 7 |
+
import UploadIcon from "../icons/upload.svg";
|
| 8 |
+
import EditIcon from "../icons/edit.svg";
|
| 9 |
+
import AddIcon from "../icons/add.svg";
|
| 10 |
+
import CloseIcon from "../icons/close.svg";
|
| 11 |
+
import DeleteIcon from "../icons/delete.svg";
|
| 12 |
+
import EyeIcon from "../icons/eye.svg";
|
| 13 |
+
import CopyIcon from "../icons/copy.svg";
|
| 14 |
+
|
| 15 |
+
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
|
| 16 |
+
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
|
| 17 |
+
import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
|
| 18 |
+
import { Avatar, AvatarPicker } from "./emoji";
|
| 19 |
+
import Locale, { AllLangs, Lang } from "../locales";
|
| 20 |
+
import { useNavigate } from "react-router-dom";
|
| 21 |
+
|
| 22 |
+
import chatStyle from "./chat.module.scss";
|
| 23 |
+
import { useEffect, useState } from "react";
|
| 24 |
+
import { downloadAs } from "../utils";
|
| 25 |
+
import { Updater } from "../api/openai/typing";
|
| 26 |
+
import { ModelConfigList } from "./model-config";
|
| 27 |
+
import { FileName, Path } from "../constant";
|
| 28 |
+
import { BUILTIN_MASK_STORE } from "../masks";
|
| 29 |
+
|
| 30 |
+
export function MaskAvatar(props: { mask: Mask }) {
|
| 31 |
+
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
| 32 |
+
<Avatar avatar={props.mask.avatar} />
|
| 33 |
+
) : (
|
| 34 |
+
<Avatar model={props.mask.modelConfig.model} />
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function MaskConfig(props: {
|
| 39 |
+
mask: Mask;
|
| 40 |
+
updateMask: Updater<Mask>;
|
| 41 |
+
extraListItems?: JSX.Element;
|
| 42 |
+
readonly?: boolean;
|
| 43 |
+
}) {
|
| 44 |
+
const [showPicker, setShowPicker] = useState(false);
|
| 45 |
+
|
| 46 |
+
const updateConfig = (updater: (config: ModelConfig) => void) => {
|
| 47 |
+
if (props.readonly) return;
|
| 48 |
+
|
| 49 |
+
const config = { ...props.mask.modelConfig };
|
| 50 |
+
updater(config);
|
| 51 |
+
props.updateMask((mask) => (mask.modelConfig = config));
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<>
|
| 56 |
+
<ContextPrompts
|
| 57 |
+
context={props.mask.context}
|
| 58 |
+
updateContext={(updater) => {
|
| 59 |
+
const context = props.mask.context.slice();
|
| 60 |
+
updater(context);
|
| 61 |
+
props.updateMask((mask) => (mask.context = context));
|
| 62 |
+
}}
|
| 63 |
+
/>
|
| 64 |
+
|
| 65 |
+
<List>
|
| 66 |
+
<ListItem title={Locale.Mask.Config.Avatar}>
|
| 67 |
+
<Popover
|
| 68 |
+
content={
|
| 69 |
+
<AvatarPicker
|
| 70 |
+
onEmojiClick={(emoji) => {
|
| 71 |
+
props.updateMask((mask) => (mask.avatar = emoji));
|
| 72 |
+
setShowPicker(false);
|
| 73 |
+
}}
|
| 74 |
+
></AvatarPicker>
|
| 75 |
+
}
|
| 76 |
+
open={showPicker}
|
| 77 |
+
onClose={() => setShowPicker(false)}
|
| 78 |
+
>
|
| 79 |
+
<div
|
| 80 |
+
onClick={() => setShowPicker(true)}
|
| 81 |
+
style={{ cursor: "pointer" }}
|
| 82 |
+
>
|
| 83 |
+
<MaskAvatar mask={props.mask} />
|
| 84 |
+
</div>
|
| 85 |
+
</Popover>
|
| 86 |
+
</ListItem>
|
| 87 |
+
<ListItem title={Locale.Mask.Config.Name}>
|
| 88 |
+
<input
|
| 89 |
+
type="text"
|
| 90 |
+
value={props.mask.name}
|
| 91 |
+
onInput={(e) =>
|
| 92 |
+
props.updateMask((mask) => (mask.name = e.currentTarget.value))
|
| 93 |
+
}
|
| 94 |
+
></input>
|
| 95 |
+
</ListItem>
|
| 96 |
+
</List>
|
| 97 |
+
|
| 98 |
+
<List>
|
| 99 |
+
<ModelConfigList
|
| 100 |
+
modelConfig={{ ...props.mask.modelConfig }}
|
| 101 |
+
updateConfig={updateConfig}
|
| 102 |
+
/>
|
| 103 |
+
{props.extraListItems}
|
| 104 |
+
</List>
|
| 105 |
+
</>
|
| 106 |
+
);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
export function ContextPrompts(props: {
|
| 110 |
+
context: Message[];
|
| 111 |
+
updateContext: (updater: (context: Message[]) => void) => void;
|
| 112 |
+
}) {
|
| 113 |
+
const context = props.context;
|
| 114 |
+
|
| 115 |
+
const addContextPrompt = (prompt: Message) => {
|
| 116 |
+
props.updateContext((context) => context.push(prompt));
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const removeContextPrompt = (i: number) => {
|
| 120 |
+
props.updateContext((context) => context.splice(i, 1));
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
const updateContextPrompt = (i: number, prompt: Message) => {
|
| 124 |
+
props.updateContext((context) => (context[i] = prompt));
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
return (
|
| 128 |
+
<>
|
| 129 |
+
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
|
| 130 |
+
{context.map((c, i) => (
|
| 131 |
+
<div className={chatStyle["context-prompt-row"]} key={i}>
|
| 132 |
+
<select
|
| 133 |
+
value={c.role}
|
| 134 |
+
className={chatStyle["context-role"]}
|
| 135 |
+
onChange={(e) =>
|
| 136 |
+
updateContextPrompt(i, {
|
| 137 |
+
...c,
|
| 138 |
+
role: e.target.value as any,
|
| 139 |
+
})
|
| 140 |
+
}
|
| 141 |
+
>
|
| 142 |
+
{ROLES.map((r) => (
|
| 143 |
+
<option key={r} value={r}>
|
| 144 |
+
{r}
|
| 145 |
+
</option>
|
| 146 |
+
))}
|
| 147 |
+
</select>
|
| 148 |
+
<Input
|
| 149 |
+
value={c.content}
|
| 150 |
+
type="text"
|
| 151 |
+
className={chatStyle["context-content"]}
|
| 152 |
+
rows={1}
|
| 153 |
+
onInput={(e) =>
|
| 154 |
+
updateContextPrompt(i, {
|
| 155 |
+
...c,
|
| 156 |
+
content: e.currentTarget.value as any,
|
| 157 |
+
})
|
| 158 |
+
}
|
| 159 |
+
/>
|
| 160 |
+
<IconButton
|
| 161 |
+
icon={<DeleteIcon />}
|
| 162 |
+
className={chatStyle["context-delete-button"]}
|
| 163 |
+
onClick={() => removeContextPrompt(i)}
|
| 164 |
+
bordered
|
| 165 |
+
/>
|
| 166 |
+
</div>
|
| 167 |
+
))}
|
| 168 |
+
|
| 169 |
+
<div className={chatStyle["context-prompt-row"]}>
|
| 170 |
+
<IconButton
|
| 171 |
+
icon={<AddIcon />}
|
| 172 |
+
text={Locale.Context.Add}
|
| 173 |
+
bordered
|
| 174 |
+
className={chatStyle["context-prompt-button"]}
|
| 175 |
+
onClick={() =>
|
| 176 |
+
addContextPrompt({
|
| 177 |
+
role: "system",
|
| 178 |
+
content: "",
|
| 179 |
+
date: "",
|
| 180 |
+
})
|
| 181 |
+
}
|
| 182 |
+
/>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
</>
|
| 186 |
+
);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
export function MaskPage() {
|
| 190 |
+
const navigate = useNavigate();
|
| 191 |
+
|
| 192 |
+
const maskStore = useMaskStore();
|
| 193 |
+
const chatStore = useChatStore();
|
| 194 |
+
|
| 195 |
+
const [filterLang, setFilterLang] = useState<Lang>();
|
| 196 |
+
|
| 197 |
+
const allMasks = maskStore
|
| 198 |
+
.getAll()
|
| 199 |
+
.filter((m) => !filterLang || m.lang === filterLang);
|
| 200 |
+
|
| 201 |
+
const [searchMasks, setSearchMasks] = useState<Mask[]>([]);
|
| 202 |
+
const [searchText, setSearchText] = useState("");
|
| 203 |
+
const masks = searchText.length > 0 ? searchMasks : allMasks;
|
| 204 |
+
|
| 205 |
+
// simple search, will refactor later
|
| 206 |
+
const onSearch = (text: string) => {
|
| 207 |
+
setSearchText(text);
|
| 208 |
+
if (text.length > 0) {
|
| 209 |
+
const result = allMasks.filter((m) => m.name.includes(text));
|
| 210 |
+
setSearchMasks(result);
|
| 211 |
+
} else {
|
| 212 |
+
setSearchMasks(allMasks);
|
| 213 |
+
}
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
|
| 217 |
+
const editingMask =
|
| 218 |
+
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
|
| 219 |
+
const closeMaskModal = () => setEditingMaskId(undefined);
|
| 220 |
+
|
| 221 |
+
const downloadAll = () => {
|
| 222 |
+
downloadAs(JSON.stringify(masks), FileName.Masks);
|
| 223 |
+
};
|
| 224 |
+
|
| 225 |
+
return (
|
| 226 |
+
<ErrorBoundary>
|
| 227 |
+
<div className={styles["mask-page"]}>
|
| 228 |
+
<div className="window-header">
|
| 229 |
+
<div className="window-header-title">
|
| 230 |
+
<div className="window-header-main-title">
|
| 231 |
+
{Locale.Mask.Page.Title}
|
| 232 |
+
</div>
|
| 233 |
+
<div className="window-header-submai-title">
|
| 234 |
+
{Locale.Mask.Page.SubTitle(allMasks.length)}
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<div className="window-actions">
|
| 239 |
+
<div className="window-action-button">
|
| 240 |
+
<IconButton
|
| 241 |
+
icon={<DownloadIcon />}
|
| 242 |
+
bordered
|
| 243 |
+
onClick={downloadAll}
|
| 244 |
+
/>
|
| 245 |
+
</div>
|
| 246 |
+
<div className="window-action-button">
|
| 247 |
+
<IconButton
|
| 248 |
+
icon={<UploadIcon />}
|
| 249 |
+
bordered
|
| 250 |
+
onClick={() => showToast(Locale.WIP)}
|
| 251 |
+
/>
|
| 252 |
+
</div>
|
| 253 |
+
<div className="window-action-button">
|
| 254 |
+
<IconButton
|
| 255 |
+
icon={<CloseIcon />}
|
| 256 |
+
bordered
|
| 257 |
+
onClick={() => navigate(-1)}
|
| 258 |
+
/>
|
| 259 |
+
</div>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
|
| 263 |
+
<div className={styles["mask-page-body"]}>
|
| 264 |
+
<div className={styles["mask-filter"]}>
|
| 265 |
+
<input
|
| 266 |
+
type="text"
|
| 267 |
+
className={styles["search-bar"]}
|
| 268 |
+
placeholder={Locale.Mask.Page.Search}
|
| 269 |
+
autoFocus
|
| 270 |
+
onInput={(e) => onSearch(e.currentTarget.value)}
|
| 271 |
+
/>
|
| 272 |
+
<select
|
| 273 |
+
className={styles["mask-filter-lang"]}
|
| 274 |
+
value={filterLang ?? Locale.Settings.Lang.All}
|
| 275 |
+
onChange={(e) => {
|
| 276 |
+
const value = e.currentTarget.value;
|
| 277 |
+
if (value === Locale.Settings.Lang.All) {
|
| 278 |
+
setFilterLang(undefined);
|
| 279 |
+
} else {
|
| 280 |
+
setFilterLang(value as Lang);
|
| 281 |
+
}
|
| 282 |
+
}}
|
| 283 |
+
>
|
| 284 |
+
<option key="all" value={Locale.Settings.Lang.All}>
|
| 285 |
+
{Locale.Settings.Lang.All}
|
| 286 |
+
</option>
|
| 287 |
+
{AllLangs.map((lang) => (
|
| 288 |
+
<option value={lang} key={lang}>
|
| 289 |
+
{Locale.Settings.Lang.Options[lang]}
|
| 290 |
+
</option>
|
| 291 |
+
))}
|
| 292 |
+
</select>
|
| 293 |
+
|
| 294 |
+
<IconButton
|
| 295 |
+
className={styles["mask-create"]}
|
| 296 |
+
icon={<AddIcon />}
|
| 297 |
+
text={Locale.Mask.Page.Create}
|
| 298 |
+
bordered
|
| 299 |
+
onClick={() => {
|
| 300 |
+
const createdMask = maskStore.create();
|
| 301 |
+
setEditingMaskId(createdMask.id);
|
| 302 |
+
}}
|
| 303 |
+
/>
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div>
|
| 307 |
+
{masks.map((m) => (
|
| 308 |
+
<div className={styles["mask-item"]} key={m.id}>
|
| 309 |
+
<div className={styles["mask-header"]}>
|
| 310 |
+
<div className={styles["mask-icon"]}>
|
| 311 |
+
<MaskAvatar mask={m} />
|
| 312 |
+
</div>
|
| 313 |
+
<div className={styles["mask-title"]}>
|
| 314 |
+
<div className={styles["mask-name"]}>{m.name}</div>
|
| 315 |
+
<div className={styles["mask-info"] + " one-line"}>
|
| 316 |
+
{`${Locale.Mask.Item.Info(m.context.length)} / ${
|
| 317 |
+
Locale.Settings.Lang.Options[m.lang]
|
| 318 |
+
} / ${m.modelConfig.model}`}
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
<div className={styles["mask-actions"]}>
|
| 323 |
+
<IconButton
|
| 324 |
+
icon={<AddIcon />}
|
| 325 |
+
text={Locale.Mask.Item.Chat}
|
| 326 |
+
onClick={() => {
|
| 327 |
+
chatStore.newSession(m);
|
| 328 |
+
navigate(Path.Chat);
|
| 329 |
+
}}
|
| 330 |
+
/>
|
| 331 |
+
{m.builtin ? (
|
| 332 |
+
<IconButton
|
| 333 |
+
icon={<EyeIcon />}
|
| 334 |
+
text={Locale.Mask.Item.View}
|
| 335 |
+
onClick={() => setEditingMaskId(m.id)}
|
| 336 |
+
/>
|
| 337 |
+
) : (
|
| 338 |
+
<IconButton
|
| 339 |
+
icon={<EditIcon />}
|
| 340 |
+
text={Locale.Mask.Item.Edit}
|
| 341 |
+
onClick={() => setEditingMaskId(m.id)}
|
| 342 |
+
/>
|
| 343 |
+
)}
|
| 344 |
+
{!m.builtin && (
|
| 345 |
+
<IconButton
|
| 346 |
+
icon={<DeleteIcon />}
|
| 347 |
+
text={Locale.Mask.Item.Delete}
|
| 348 |
+
onClick={() => {
|
| 349 |
+
if (confirm(Locale.Mask.Item.DeleteConfirm)) {
|
| 350 |
+
maskStore.delete(m.id);
|
| 351 |
+
}
|
| 352 |
+
}}
|
| 353 |
+
/>
|
| 354 |
+
)}
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
))}
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
|
| 362 |
+
{editingMask && (
|
| 363 |
+
<div className="modal-mask">
|
| 364 |
+
<Modal
|
| 365 |
+
title={Locale.Mask.EditModal.Title(editingMask?.builtin)}
|
| 366 |
+
onClose={closeMaskModal}
|
| 367 |
+
actions={[
|
| 368 |
+
<IconButton
|
| 369 |
+
icon={<DownloadIcon />}
|
| 370 |
+
text={Locale.Mask.EditModal.Download}
|
| 371 |
+
key="export"
|
| 372 |
+
bordered
|
| 373 |
+
onClick={() =>
|
| 374 |
+
downloadAs(JSON.stringify(editingMask), "mask.json")
|
| 375 |
+
}
|
| 376 |
+
/>,
|
| 377 |
+
<IconButton
|
| 378 |
+
key="copy"
|
| 379 |
+
icon={<CopyIcon />}
|
| 380 |
+
bordered
|
| 381 |
+
text={Locale.Mask.EditModal.Clone}
|
| 382 |
+
onClick={() => {
|
| 383 |
+
navigate(Path.Masks);
|
| 384 |
+
maskStore.create(editingMask);
|
| 385 |
+
setEditingMaskId(undefined);
|
| 386 |
+
}}
|
| 387 |
+
/>,
|
| 388 |
+
]}
|
| 389 |
+
>
|
| 390 |
+
<MaskConfig
|
| 391 |
+
mask={editingMask}
|
| 392 |
+
updateMask={(updater) =>
|
| 393 |
+
maskStore.update(editingMaskId!, updater)
|
| 394 |
+
}
|
| 395 |
+
readonly={editingMask.builtin}
|
| 396 |
+
/>
|
| 397 |
+
</Modal>
|
| 398 |
+
</div>
|
| 399 |
+
)}
|
| 400 |
+
</ErrorBoundary>
|
| 401 |
+
);
|
| 402 |
+
}
|
app/components/model-config.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import styles from "./settings.module.scss";
|
| 2 |
+
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
|
| 3 |
+
|
| 4 |
+
import Locale from "../locales";
|
| 5 |
+
import { InputRange } from "./input-range";
|
| 6 |
+
import { List, ListItem } from "./ui-lib";
|
| 7 |
+
|
| 8 |
+
export function ModelConfigList(props: {
|
| 9 |
+
modelConfig: ModelConfig;
|
| 10 |
+
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
| 11 |
+
}) {
|
| 12 |
+
return (
|
| 13 |
+
<>
|
| 14 |
+
<ListItem title={Locale.Settings.Model}>
|
| 15 |
+
<select
|
| 16 |
+
value={props.modelConfig.model}
|
| 17 |
+
onChange={(e) => {
|
| 18 |
+
props.updateConfig(
|
| 19 |
+
(config) =>
|
| 20 |
+
(config.model = ModalConfigValidator.model(
|
| 21 |
+
e.currentTarget.value,
|
| 22 |
+
)),
|
| 23 |
+
);
|
| 24 |
+
}}
|
| 25 |
+
>
|
| 26 |
+
{ALL_MODELS.map((v) => (
|
| 27 |
+
<option value={v.name} key={v.name} disabled={!v.available}>
|
| 28 |
+
{v.name}
|
| 29 |
+
</option>
|
| 30 |
+
))}
|
| 31 |
+
</select>
|
| 32 |
+
</ListItem>
|
| 33 |
+
<ListItem
|
| 34 |
+
title={Locale.Settings.Temperature.Title}
|
| 35 |
+
subTitle={Locale.Settings.Temperature.SubTitle}
|
| 36 |
+
>
|
| 37 |
+
<InputRange
|
| 38 |
+
value={props.modelConfig.temperature?.toFixed(1)}
|
| 39 |
+
min="0"
|
| 40 |
+
max="1" // lets limit it to 0-1
|
| 41 |
+
step="0.1"
|
| 42 |
+
onChange={(e) => {
|
| 43 |
+
props.updateConfig(
|
| 44 |
+
(config) =>
|
| 45 |
+
(config.temperature = ModalConfigValidator.temperature(
|
| 46 |
+
e.currentTarget.valueAsNumber,
|
| 47 |
+
)),
|
| 48 |
+
);
|
| 49 |
+
}}
|
| 50 |
+
></InputRange>
|
| 51 |
+
</ListItem>
|
| 52 |
+
<ListItem
|
| 53 |
+
title={Locale.Settings.MaxTokens.Title}
|
| 54 |
+
subTitle={Locale.Settings.MaxTokens.SubTitle}
|
| 55 |
+
>
|
| 56 |
+
<input
|
| 57 |
+
type="number"
|
| 58 |
+
min={100}
|
| 59 |
+
max={32000}
|
| 60 |
+
value={props.modelConfig.max_tokens}
|
| 61 |
+
onChange={(e) =>
|
| 62 |
+
props.updateConfig(
|
| 63 |
+
(config) =>
|
| 64 |
+
(config.max_tokens = ModalConfigValidator.max_tokens(
|
| 65 |
+
e.currentTarget.valueAsNumber,
|
| 66 |
+
)),
|
| 67 |
+
)
|
| 68 |
+
}
|
| 69 |
+
></input>
|
| 70 |
+
</ListItem>
|
| 71 |
+
<ListItem
|
| 72 |
+
title={Locale.Settings.PresencePenlty.Title}
|
| 73 |
+
subTitle={Locale.Settings.PresencePenlty.SubTitle}
|
| 74 |
+
>
|
| 75 |
+
<InputRange
|
| 76 |
+
value={props.modelConfig.presence_penalty?.toFixed(1)}
|
| 77 |
+
min="-2"
|
| 78 |
+
max="2"
|
| 79 |
+
step="0.1"
|
| 80 |
+
onChange={(e) => {
|
| 81 |
+
props.updateConfig(
|
| 82 |
+
(config) =>
|
| 83 |
+
(config.presence_penalty =
|
| 84 |
+
ModalConfigValidator.presence_penalty(
|
| 85 |
+
e.currentTarget.valueAsNumber,
|
| 86 |
+
)),
|
| 87 |
+
);
|
| 88 |
+
}}
|
| 89 |
+
></InputRange>
|
| 90 |
+
</ListItem>
|
| 91 |
+
|
| 92 |
+
<ListItem
|
| 93 |
+
title={Locale.Settings.HistoryCount.Title}
|
| 94 |
+
subTitle={Locale.Settings.HistoryCount.SubTitle}
|
| 95 |
+
>
|
| 96 |
+
<InputRange
|
| 97 |
+
title={props.modelConfig.historyMessageCount.toString()}
|
| 98 |
+
value={props.modelConfig.historyMessageCount}
|
| 99 |
+
min="0"
|
| 100 |
+
max="32"
|
| 101 |
+
step="1"
|
| 102 |
+
onChange={(e) =>
|
| 103 |
+
props.updateConfig(
|
| 104 |
+
(config) => (config.historyMessageCount = e.target.valueAsNumber),
|
| 105 |
+
)
|
| 106 |
+
}
|
| 107 |
+
></InputRange>
|
| 108 |
+
</ListItem>
|
| 109 |
+
|
| 110 |
+
<ListItem
|
| 111 |
+
title={Locale.Settings.CompressThreshold.Title}
|
| 112 |
+
subTitle={Locale.Settings.CompressThreshold.SubTitle}
|
| 113 |
+
>
|
| 114 |
+
<input
|
| 115 |
+
type="number"
|
| 116 |
+
min={500}
|
| 117 |
+
max={4000}
|
| 118 |
+
value={props.modelConfig.compressMessageLengthThreshold}
|
| 119 |
+
onChange={(e) =>
|
| 120 |
+
props.updateConfig(
|
| 121 |
+
(config) =>
|
| 122 |
+
(config.compressMessageLengthThreshold =
|
| 123 |
+
e.currentTarget.valueAsNumber),
|
| 124 |
+
)
|
| 125 |
+
}
|
| 126 |
+
></input>
|
| 127 |
+
</ListItem>
|
| 128 |
+
<ListItem title={Locale.Memory.Title} subTitle={Locale.Memory.Send}>
|
| 129 |
+
<input
|
| 130 |
+
type="checkbox"
|
| 131 |
+
checked={props.modelConfig.sendMemory}
|
| 132 |
+
onChange={(e) =>
|
| 133 |
+
props.updateConfig(
|
| 134 |
+
(config) => (config.sendMemory = e.currentTarget.checked),
|
| 135 |
+
)
|
| 136 |
+
}
|
| 137 |
+
></input>
|
| 138 |
+
</ListItem>
|
| 139 |
+
</>
|
| 140 |
+
);
|
| 141 |
+
}
|
app/components/new-chat.module.scss
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "../styles/animation.scss";
|
| 2 |
+
|
| 3 |
+
.new-chat {
|
| 4 |
+
height: 100%;
|
| 5 |
+
width: 100%;
|
| 6 |
+
display: flex;
|
| 7 |
+
align-items: center;
|
| 8 |
+
justify-content: center;
|
| 9 |
+
flex-direction: column;
|
| 10 |
+
|
| 11 |
+
.mask-header {
|
| 12 |
+
display: flex;
|
| 13 |
+
justify-content: space-between;
|
| 14 |
+
width: 100%;
|
| 15 |
+
padding: 10px;
|
| 16 |
+
box-sizing: border-box;
|
| 17 |
+
animation: slide-in-from-top ease 0.3s;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.mask-cards {
|
| 21 |
+
display: flex;
|
| 22 |
+
margin-top: 5vh;
|
| 23 |
+
margin-bottom: 20px;
|
| 24 |
+
animation: slide-in ease 0.3s;
|
| 25 |
+
|
| 26 |
+
.mask-card {
|
| 27 |
+
padding: 20px 10px;
|
| 28 |
+
border: var(--border-in-light);
|
| 29 |
+
box-shadow: var(--card-shadow);
|
| 30 |
+
border-radius: 14px;
|
| 31 |
+
background-color: var(--white);
|
| 32 |
+
transform: scale(1);
|
| 33 |
+
|
| 34 |
+
&:first-child {
|
| 35 |
+
transform: rotate(-15deg) translateY(5px);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
&:last-child {
|
| 39 |
+
transform: rotate(15deg) translateY(5px);
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.title {
|
| 45 |
+
font-size: 32px;
|
| 46 |
+
font-weight: bolder;
|
| 47 |
+
margin-bottom: 1vh;
|
| 48 |
+
animation: slide-in ease 0.35s;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.sub-title {
|
| 52 |
+
animation: slide-in ease 0.4s;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.actions {
|
| 56 |
+
margin-top: 5vh;
|
| 57 |
+
margin-bottom: 5vh;
|
| 58 |
+
animation: slide-in ease 0.45s;
|
| 59 |
+
display: flex;
|
| 60 |
+
justify-content: center;
|
| 61 |
+
|
| 62 |
+
.more {
|
| 63 |
+
font-size: 12px;
|
| 64 |
+
margin-left: 10px;
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.masks {
|
| 69 |
+
flex-grow: 1;
|
| 70 |
+
width: 100%;
|
| 71 |
+
overflow: hidden;
|
| 72 |
+
align-items: center;
|
| 73 |
+
padding-top: 20px;
|
| 74 |
+
|
| 75 |
+
animation: slide-in ease 0.5s;
|
| 76 |
+
|
| 77 |
+
.mask-row {
|
| 78 |
+
margin-bottom: 10px;
|
| 79 |
+
display: flex;
|
| 80 |
+
justify-content: center;
|
| 81 |
+
|
| 82 |
+
@for $i from 1 to 10 {
|
| 83 |
+
&:nth-child(#{$i * 2}) {
|
| 84 |
+
margin-left: 50px;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.mask {
|
| 89 |
+
display: flex;
|
| 90 |
+
align-items: center;
|
| 91 |
+
padding: 10px 14px;
|
| 92 |
+
border: var(--border-in-light);
|
| 93 |
+
box-shadow: var(--card-shadow);
|
| 94 |
+
background-color: var(--white);
|
| 95 |
+
border-radius: 10px;
|
| 96 |
+
margin-right: 10px;
|
| 97 |
+
max-width: 8em;
|
| 98 |
+
transform: scale(1);
|
| 99 |
+
cursor: pointer;
|
| 100 |
+
transition: all ease 0.3s;
|
| 101 |
+
|
| 102 |
+
&:hover {
|
| 103 |
+
transform: translateY(-5px) scale(1.1);
|
| 104 |
+
z-index: 999;
|
| 105 |
+
border-color: var(--primary);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.mask-name {
|
| 109 |
+
margin-left: 10px;
|
| 110 |
+
font-size: 14px;
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
app/components/new-chat.tsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef, useState } from "react";
|
| 2 |
+
import { Path, SlotID } from "../constant";
|
| 3 |
+
import { IconButton } from "./button";
|
| 4 |
+
import { EmojiAvatar } from "./emoji";
|
| 5 |
+
import styles from "./new-chat.module.scss";
|
| 6 |
+
|
| 7 |
+
import LeftIcon from "../icons/left.svg";
|
| 8 |
+
import LightningIcon from "../icons/lightning.svg";
|
| 9 |
+
import EyeIcon from "../icons/eye.svg";
|
| 10 |
+
|
| 11 |
+
import { useLocation, useNavigate } from "react-router-dom";
|
| 12 |
+
import { Mask, useMaskStore } from "../store/mask";
|
| 13 |
+
import Locale from "../locales";
|
| 14 |
+
import { useAppConfig, useChatStore } from "../store";
|
| 15 |
+
import { MaskAvatar } from "./mask";
|
| 16 |
+
|
| 17 |
+
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
| 18 |
+
const xmin = Math.max(aRect.x, bRect.x);
|
| 19 |
+
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
|
| 20 |
+
const ymin = Math.max(aRect.y, bRect.y);
|
| 21 |
+
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
|
| 22 |
+
const width = xmax - xmin;
|
| 23 |
+
const height = ymax - ymin;
|
| 24 |
+
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
|
| 25 |
+
return intersectionArea;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
| 29 |
+
const domRef = useRef<HTMLDivElement>(null);
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
const changeOpacity = () => {
|
| 33 |
+
const dom = domRef.current;
|
| 34 |
+
const parent = document.getElementById(SlotID.AppBody);
|
| 35 |
+
if (!parent || !dom) return;
|
| 36 |
+
|
| 37 |
+
const domRect = dom.getBoundingClientRect();
|
| 38 |
+
const parentRect = parent.getBoundingClientRect();
|
| 39 |
+
const intersectionArea = getIntersectionArea(domRect, parentRect);
|
| 40 |
+
const domArea = domRect.width * domRect.height;
|
| 41 |
+
const ratio = intersectionArea / domArea;
|
| 42 |
+
const opacity = ratio > 0.9 ? 1 : 0.4;
|
| 43 |
+
dom.style.opacity = opacity.toString();
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
setTimeout(changeOpacity, 30);
|
| 47 |
+
|
| 48 |
+
window.addEventListener("resize", changeOpacity);
|
| 49 |
+
|
| 50 |
+
return () => window.removeEventListener("resize", changeOpacity);
|
| 51 |
+
}, [domRef]);
|
| 52 |
+
|
| 53 |
+
return (
|
| 54 |
+
<div className={styles["mask"]} ref={domRef} onClick={props.onClick}>
|
| 55 |
+
<MaskAvatar mask={props.mask} />
|
| 56 |
+
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
| 57 |
+
</div>
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function useMaskGroup(masks: Mask[]) {
|
| 62 |
+
const [groups, setGroups] = useState<Mask[][]>([]);
|
| 63 |
+
|
| 64 |
+
useEffect(() => {
|
| 65 |
+
const appBody = document.getElementById(SlotID.AppBody);
|
| 66 |
+
if (!appBody || masks.length === 0) return;
|
| 67 |
+
|
| 68 |
+
const rect = appBody.getBoundingClientRect();
|
| 69 |
+
const maxWidth = rect.width;
|
| 70 |
+
const maxHeight = rect.height * 0.6;
|
| 71 |
+
const maskItemWidth = 120;
|
| 72 |
+
const maskItemHeight = 50;
|
| 73 |
+
|
| 74 |
+
const randomMask = () => masks[Math.floor(Math.random() * masks.length)];
|
| 75 |
+
let maskIndex = 0;
|
| 76 |
+
const nextMask = () => masks[maskIndex++ % masks.length];
|
| 77 |
+
|
| 78 |
+
const rows = Math.ceil(maxHeight / maskItemHeight);
|
| 79 |
+
const cols = Math.ceil(maxWidth / maskItemWidth);
|
| 80 |
+
|
| 81 |
+
const newGroups = new Array(rows)
|
| 82 |
+
.fill(0)
|
| 83 |
+
.map((_, _i) =>
|
| 84 |
+
new Array(cols)
|
| 85 |
+
.fill(0)
|
| 86 |
+
.map((_, j) => (j < 1 || j > cols - 2 ? randomMask() : nextMask())),
|
| 87 |
+
);
|
| 88 |
+
|
| 89 |
+
setGroups(newGroups);
|
| 90 |
+
|
| 91 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 92 |
+
}, []);
|
| 93 |
+
|
| 94 |
+
return groups;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export function NewChat() {
|
| 98 |
+
const chatStore = useChatStore();
|
| 99 |
+
const maskStore = useMaskStore();
|
| 100 |
+
|
| 101 |
+
const masks = maskStore.getAll();
|
| 102 |
+
const groups = useMaskGroup(masks);
|
| 103 |
+
|
| 104 |
+
const navigate = useNavigate();
|
| 105 |
+
const config = useAppConfig();
|
| 106 |
+
|
| 107 |
+
const { state } = useLocation();
|
| 108 |
+
|
| 109 |
+
const startChat = (mask?: Mask) => {
|
| 110 |
+
chatStore.newSession(mask);
|
| 111 |
+
navigate(Path.Chat);
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
return (
|
| 115 |
+
<div className={styles["new-chat"]}>
|
| 116 |
+
<div className={styles["mask-header"]}>
|
| 117 |
+
<IconButton
|
| 118 |
+
icon={<LeftIcon />}
|
| 119 |
+
text={Locale.NewChat.Return}
|
| 120 |
+
onClick={() => navigate(Path.Home)}
|
| 121 |
+
></IconButton>
|
| 122 |
+
{!state?.fromHome && (
|
| 123 |
+
<IconButton
|
| 124 |
+
text={Locale.NewChat.NotShow}
|
| 125 |
+
onClick={() => {
|
| 126 |
+
if (confirm(Locale.NewChat.ConfirmNoShow)) {
|
| 127 |
+
startChat();
|
| 128 |
+
config.update(
|
| 129 |
+
(config) => (config.dontShowMaskSplashScreen = true),
|
| 130 |
+
);
|
| 131 |
+
}
|
| 132 |
+
}}
|
| 133 |
+
></IconButton>
|
| 134 |
+
)}
|
| 135 |
+
</div>
|
| 136 |
+
<div className={styles["mask-cards"]}>
|
| 137 |
+
<div className={styles["mask-card"]}>
|
| 138 |
+
<EmojiAvatar avatar="1f606" size={24} />
|
| 139 |
+
</div>
|
| 140 |
+
<div className={styles["mask-card"]}>
|
| 141 |
+
<EmojiAvatar avatar="1f916" size={24} />
|
| 142 |
+
</div>
|
| 143 |
+
<div className={styles["mask-card"]}>
|
| 144 |
+
<EmojiAvatar avatar="1f479" size={24} />
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<div className={styles["title"]}>{Locale.NewChat.Title}</div>
|
| 149 |
+
<div className={styles["sub-title"]}>{Locale.NewChat.SubTitle}</div>
|
| 150 |
+
|
| 151 |
+
<div className={styles["actions"]}>
|
| 152 |
+
<IconButton
|
| 153 |
+
text={Locale.NewChat.Skip}
|
| 154 |
+
onClick={() => startChat()}
|
| 155 |
+
icon={<LightningIcon />}
|
| 156 |
+
type="primary"
|
| 157 |
+
shadow
|
| 158 |
+
/>
|
| 159 |
+
|
| 160 |
+
<IconButton
|
| 161 |
+
className={styles["more"]}
|
| 162 |
+
text={Locale.NewChat.More}
|
| 163 |
+
onClick={() => navigate(Path.Masks)}
|
| 164 |
+
icon={<EyeIcon />}
|
| 165 |
+
bordered
|
| 166 |
+
shadow
|
| 167 |
+
/>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<div className={styles["masks"]}>
|
| 171 |
+
{groups.map((masks, i) => (
|
| 172 |
+
<div key={i} className={styles["mask-row"]}>
|
| 173 |
+
{masks.map((mask, index) => (
|
| 174 |
+
<MaskItem
|
| 175 |
+
key={index}
|
| 176 |
+
mask={mask}
|
| 177 |
+
onClick={() => startChat(mask)}
|
| 178 |
+
/>
|
| 179 |
+
))}
|
| 180 |
+
</div>
|
| 181 |
+
))}
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
);
|
| 185 |
+
}
|
app/components/settings.module.scss
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.settings {
|
| 2 |
+
padding: 20px;
|
| 3 |
+
overflow: auto;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
.avatar {
|
| 7 |
+
cursor: pointer;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.edit-prompt-modal {
|
| 11 |
+
display: flex;
|
| 12 |
+
flex-direction: column;
|
| 13 |
+
|
| 14 |
+
.edit-prompt-title {
|
| 15 |
+
max-width: unset;
|
| 16 |
+
margin-bottom: 20px;
|
| 17 |
+
text-align: left;
|
| 18 |
+
}
|
| 19 |
+
.edit-prompt-content {
|
| 20 |
+
max-width: unset;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.user-prompt-modal {
|
| 25 |
+
min-height: 40vh;
|
| 26 |
+
|
| 27 |
+
.user-prompt-search {
|
| 28 |
+
width: 100%;
|
| 29 |
+
max-width: 100%;
|
| 30 |
+
margin-bottom: 10px;
|
| 31 |
+
background-color: var(--gray);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.user-prompt-list {
|
| 35 |
+
border: var(--border-in-light);
|
| 36 |
+
border-radius: 10px;
|
| 37 |
+
|
| 38 |
+
.user-prompt-item {
|
| 39 |
+
display: flex;
|
| 40 |
+
justify-content: space-between;
|
| 41 |
+
padding: 10px;
|
| 42 |
+
|
| 43 |
+
&:not(:last-child) {
|
| 44 |
+
border-bottom: var(--border-in-light);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.user-prompt-header {
|
| 48 |
+
max-width: calc(100% - 100px);
|
| 49 |
+
|
| 50 |
+
.user-prompt-title {
|
| 51 |
+
font-size: 14px;
|
| 52 |
+
line-height: 2;
|
| 53 |
+
font-weight: bold;
|
| 54 |
+
}
|
| 55 |
+
.user-prompt-content {
|
| 56 |
+
font-size: 12px;
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.user-prompt-buttons {
|
| 61 |
+
display: flex;
|
| 62 |
+
align-items: center;
|
| 63 |
+
|
| 64 |
+
.user-prompt-button {
|
| 65 |
+
height: 100%;
|
| 66 |
+
|
| 67 |
+
&:not(:last-child) {
|
| 68 |
+
margin-right: 5px;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.user-prompt-actions {
|
| 76 |
+
}
|
| 77 |
+
}
|
app/components/settings.tsx
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
|
| 2 |
+
|
| 3 |
+
import styles from "./settings.module.scss";
|
| 4 |
+
|
| 5 |
+
import ResetIcon from "../icons/reload.svg";
|
| 6 |
+
import AddIcon from "../icons/add.svg";
|
| 7 |
+
import CloseIcon from "../icons/close.svg";
|
| 8 |
+
import CopyIcon from "../icons/copy.svg";
|
| 9 |
+
import ClearIcon from "../icons/clear.svg";
|
| 10 |
+
import EditIcon from "../icons/edit.svg";
|
| 11 |
+
import EyeIcon from "../icons/eye.svg";
|
| 12 |
+
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
|
| 13 |
+
import { ModelConfigList } from "./model-config";
|
| 14 |
+
|
| 15 |
+
import { IconButton } from "./button";
|
| 16 |
+
import {
|
| 17 |
+
SubmitKey,
|
| 18 |
+
useChatStore,
|
| 19 |
+
Theme,
|
| 20 |
+
useUpdateStore,
|
| 21 |
+
useAccessStore,
|
| 22 |
+
useAppConfig,
|
| 23 |
+
} from "../store";
|
| 24 |
+
|
| 25 |
+
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
| 26 |
+
import { copyToClipboard } from "../utils";
|
| 27 |
+
import Link from "next/link";
|
| 28 |
+
import { Path, UPDATE_URL } from "../constant";
|
| 29 |
+
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
| 30 |
+
import { ErrorBoundary } from "./error";
|
| 31 |
+
import { InputRange } from "./input-range";
|
| 32 |
+
import { useNavigate } from "react-router-dom";
|
| 33 |
+
import { Avatar, AvatarPicker } from "./emoji";
|
| 34 |
+
|
| 35 |
+
function EditPromptModal(props: { id: number; onClose: () => void }) {
|
| 36 |
+
const promptStore = usePromptStore();
|
| 37 |
+
const prompt = promptStore.get(props.id);
|
| 38 |
+
|
| 39 |
+
return prompt ? (
|
| 40 |
+
<div className="modal-mask">
|
| 41 |
+
<Modal
|
| 42 |
+
title={Locale.Settings.Prompt.EditModal.Title}
|
| 43 |
+
onClose={props.onClose}
|
| 44 |
+
actions={[
|
| 45 |
+
<IconButton
|
| 46 |
+
key=""
|
| 47 |
+
onClick={props.onClose}
|
| 48 |
+
text={Locale.UI.Confirm}
|
| 49 |
+
bordered
|
| 50 |
+
/>,
|
| 51 |
+
]}
|
| 52 |
+
>
|
| 53 |
+
<div className={styles["edit-prompt-modal"]}>
|
| 54 |
+
<input
|
| 55 |
+
type="text"
|
| 56 |
+
value={prompt.title}
|
| 57 |
+
readOnly={!prompt.isUser}
|
| 58 |
+
className={styles["edit-prompt-title"]}
|
| 59 |
+
onInput={(e) =>
|
| 60 |
+
promptStore.update(
|
| 61 |
+
props.id,
|
| 62 |
+
(prompt) => (prompt.title = e.currentTarget.value),
|
| 63 |
+
)
|
| 64 |
+
}
|
| 65 |
+
></input>
|
| 66 |
+
<Input
|
| 67 |
+
value={prompt.content}
|
| 68 |
+
readOnly={!prompt.isUser}
|
| 69 |
+
className={styles["edit-prompt-content"]}
|
| 70 |
+
rows={10}
|
| 71 |
+
onInput={(e) =>
|
| 72 |
+
promptStore.update(
|
| 73 |
+
props.id,
|
| 74 |
+
(prompt) => (prompt.content = e.currentTarget.value),
|
| 75 |
+
)
|
| 76 |
+
}
|
| 77 |
+
></Input>
|
| 78 |
+
</div>
|
| 79 |
+
</Modal>
|
| 80 |
+
</div>
|
| 81 |
+
) : null;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function UserPromptModal(props: { onClose?: () => void }) {
|
| 85 |
+
const promptStore = usePromptStore();
|
| 86 |
+
const userPrompts = promptStore.getUserPrompts();
|
| 87 |
+
const builtinPrompts = SearchService.builtinPrompts;
|
| 88 |
+
const allPrompts = userPrompts.concat(builtinPrompts);
|
| 89 |
+
const [searchInput, setSearchInput] = useState("");
|
| 90 |
+
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
|
| 91 |
+
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
|
| 92 |
+
|
| 93 |
+
const [editingPromptId, setEditingPromptId] = useState<number>();
|
| 94 |
+
|
| 95 |
+
useEffect(() => {
|
| 96 |
+
if (searchInput.length > 0) {
|
| 97 |
+
const searchResult = SearchService.search(searchInput);
|
| 98 |
+
setSearchPrompts(searchResult);
|
| 99 |
+
} else {
|
| 100 |
+
setSearchPrompts([]);
|
| 101 |
+
}
|
| 102 |
+
}, [searchInput]);
|
| 103 |
+
|
| 104 |
+
return (
|
| 105 |
+
<div className="modal-mask">
|
| 106 |
+
<Modal
|
| 107 |
+
title={Locale.Settings.Prompt.Modal.Title}
|
| 108 |
+
onClose={() => props.onClose?.()}
|
| 109 |
+
actions={[
|
| 110 |
+
<IconButton
|
| 111 |
+
key="add"
|
| 112 |
+
onClick={() =>
|
| 113 |
+
promptStore.add({
|
| 114 |
+
title: "Empty Prompt",
|
| 115 |
+
content: "Empty Prompt Content",
|
| 116 |
+
})
|
| 117 |
+
}
|
| 118 |
+
icon={<AddIcon />}
|
| 119 |
+
bordered
|
| 120 |
+
text={Locale.Settings.Prompt.Modal.Add}
|
| 121 |
+
/>,
|
| 122 |
+
]}
|
| 123 |
+
>
|
| 124 |
+
<div className={styles["user-prompt-modal"]}>
|
| 125 |
+
<input
|
| 126 |
+
type="text"
|
| 127 |
+
className={styles["user-prompt-search"]}
|
| 128 |
+
placeholder={Locale.Settings.Prompt.Modal.Search}
|
| 129 |
+
value={searchInput}
|
| 130 |
+
onInput={(e) => setSearchInput(e.currentTarget.value)}
|
| 131 |
+
></input>
|
| 132 |
+
|
| 133 |
+
<div className={styles["user-prompt-list"]}>
|
| 134 |
+
{prompts.map((v, _) => (
|
| 135 |
+
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
|
| 136 |
+
<div className={styles["user-prompt-header"]}>
|
| 137 |
+
<div className={styles["user-prompt-title"]}>{v.title}</div>
|
| 138 |
+
<div className={styles["user-prompt-content"] + " one-line"}>
|
| 139 |
+
{v.content}
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
|
| 143 |
+
<div className={styles["user-prompt-buttons"]}>
|
| 144 |
+
{v.isUser && (
|
| 145 |
+
<IconButton
|
| 146 |
+
icon={<ClearIcon />}
|
| 147 |
+
className={styles["user-prompt-button"]}
|
| 148 |
+
onClick={() => promptStore.remove(v.id!)}
|
| 149 |
+
/>
|
| 150 |
+
)}
|
| 151 |
+
{v.isUser ? (
|
| 152 |
+
<IconButton
|
| 153 |
+
icon={<EditIcon />}
|
| 154 |
+
className={styles["user-prompt-button"]}
|
| 155 |
+
onClick={() => setEditingPromptId(v.id)}
|
| 156 |
+
/>
|
| 157 |
+
) : (
|
| 158 |
+
<IconButton
|
| 159 |
+
icon={<EyeIcon />}
|
| 160 |
+
className={styles["user-prompt-button"]}
|
| 161 |
+
onClick={() => setEditingPromptId(v.id)}
|
| 162 |
+
/>
|
| 163 |
+
)}
|
| 164 |
+
<IconButton
|
| 165 |
+
icon={<CopyIcon />}
|
| 166 |
+
className={styles["user-prompt-button"]}
|
| 167 |
+
onClick={() => copyToClipboard(v.content)}
|
| 168 |
+
/>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
))}
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
</Modal>
|
| 175 |
+
|
| 176 |
+
{editingPromptId !== undefined && (
|
| 177 |
+
<EditPromptModal
|
| 178 |
+
id={editingPromptId!}
|
| 179 |
+
onClose={() => setEditingPromptId(undefined)}
|
| 180 |
+
/>
|
| 181 |
+
)}
|
| 182 |
+
</div>
|
| 183 |
+
);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
export function Settings() {
|
| 187 |
+
const navigate = useNavigate();
|
| 188 |
+
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
| 189 |
+
const config = useAppConfig();
|
| 190 |
+
const updateConfig = config.update;
|
| 191 |
+
const resetConfig = config.reset;
|
| 192 |
+
const chatStore = useChatStore();
|
| 193 |
+
|
| 194 |
+
const updateStore = useUpdateStore();
|
| 195 |
+
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
| 196 |
+
const currentVersion = updateStore.version;
|
| 197 |
+
const remoteId = updateStore.remoteVersion;
|
| 198 |
+
const hasNewVersion = currentVersion !== remoteId;
|
| 199 |
+
|
| 200 |
+
function checkUpdate(force = false) {
|
| 201 |
+
setCheckingUpdate(true);
|
| 202 |
+
updateStore.getLatestVersion(force).then(() => {
|
| 203 |
+
setCheckingUpdate(false);
|
| 204 |
+
});
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
const usage = {
|
| 208 |
+
used: updateStore.used,
|
| 209 |
+
subscription: updateStore.subscription,
|
| 210 |
+
};
|
| 211 |
+
const [loadingUsage, setLoadingUsage] = useState(false);
|
| 212 |
+
function checkUsage(force = false) {
|
| 213 |
+
setLoadingUsage(true);
|
| 214 |
+
updateStore.updateUsage(force).finally(() => {
|
| 215 |
+
setLoadingUsage(false);
|
| 216 |
+
});
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
const accessStore = useAccessStore();
|
| 220 |
+
const enabledAccessControl = useMemo(
|
| 221 |
+
() => accessStore.enabledAccessControl(),
|
| 222 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 223 |
+
[],
|
| 224 |
+
);
|
| 225 |
+
|
| 226 |
+
const promptStore = usePromptStore();
|
| 227 |
+
const builtinCount = SearchService.count.builtin;
|
| 228 |
+
const customCount = promptStore.getUserPrompts().length ?? 0;
|
| 229 |
+
const [shouldShowPromptModal, setShowPromptModal] = useState(false);
|
| 230 |
+
|
| 231 |
+
const showUsage = accessStore.isAuthorized();
|
| 232 |
+
useEffect(() => {
|
| 233 |
+
// checks per minutes
|
| 234 |
+
checkUpdate();
|
| 235 |
+
showUsage && checkUsage();
|
| 236 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 237 |
+
}, []);
|
| 238 |
+
|
| 239 |
+
useEffect(() => {
|
| 240 |
+
const keydownEvent = (e: KeyboardEvent) => {
|
| 241 |
+
if (e.key === "Escape") {
|
| 242 |
+
navigate(Path.Home);
|
| 243 |
+
}
|
| 244 |
+
};
|
| 245 |
+
document.addEventListener("keydown", keydownEvent);
|
| 246 |
+
return () => {
|
| 247 |
+
document.removeEventListener("keydown", keydownEvent);
|
| 248 |
+
};
|
| 249 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 250 |
+
}, []);
|
| 251 |
+
|
| 252 |
+
return (
|
| 253 |
+
<ErrorBoundary>
|
| 254 |
+
<div className="window-header">
|
| 255 |
+
<div className="window-header-title">
|
| 256 |
+
<div className="window-header-main-title">
|
| 257 |
+
{Locale.Settings.Title}
|
| 258 |
+
</div>
|
| 259 |
+
<div className="window-header-sub-title">
|
| 260 |
+
{Locale.Settings.SubTitle}
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
<div className="window-actions">
|
| 264 |
+
<div className="window-action-button">
|
| 265 |
+
<IconButton
|
| 266 |
+
icon={<ClearIcon />}
|
| 267 |
+
onClick={() => {
|
| 268 |
+
if (confirm(Locale.Settings.Actions.ConfirmClearAll)) {
|
| 269 |
+
chatStore.clearAllData();
|
| 270 |
+
}
|
| 271 |
+
}}
|
| 272 |
+
bordered
|
| 273 |
+
title={Locale.Settings.Actions.ClearAll}
|
| 274 |
+
/>
|
| 275 |
+
</div>
|
| 276 |
+
<div className="window-action-button">
|
| 277 |
+
<IconButton
|
| 278 |
+
icon={<ResetIcon />}
|
| 279 |
+
onClick={() => {
|
| 280 |
+
if (confirm(Locale.Settings.Actions.ConfirmResetAll)) {
|
| 281 |
+
resetConfig();
|
| 282 |
+
}
|
| 283 |
+
}}
|
| 284 |
+
bordered
|
| 285 |
+
title={Locale.Settings.Actions.ResetAll}
|
| 286 |
+
/>
|
| 287 |
+
</div>
|
| 288 |
+
<div className="window-action-button">
|
| 289 |
+
<IconButton
|
| 290 |
+
icon={<CloseIcon />}
|
| 291 |
+
onClick={() => navigate(Path.Home)}
|
| 292 |
+
bordered
|
| 293 |
+
title={Locale.Settings.Actions.Close}
|
| 294 |
+
/>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
<div className={styles["settings"]}>
|
| 299 |
+
<List>
|
| 300 |
+
<ListItem title={Locale.Settings.Avatar}>
|
| 301 |
+
<Popover
|
| 302 |
+
onClose={() => setShowEmojiPicker(false)}
|
| 303 |
+
content={
|
| 304 |
+
<AvatarPicker
|
| 305 |
+
onEmojiClick={(avatar: string) => {
|
| 306 |
+
updateConfig((config) => (config.avatar = avatar));
|
| 307 |
+
setShowEmojiPicker(false);
|
| 308 |
+
}}
|
| 309 |
+
/>
|
| 310 |
+
}
|
| 311 |
+
open={showEmojiPicker}
|
| 312 |
+
>
|
| 313 |
+
<div
|
| 314 |
+
className={styles.avatar}
|
| 315 |
+
onClick={() => setShowEmojiPicker(true)}
|
| 316 |
+
>
|
| 317 |
+
<Avatar avatar={config.avatar} />
|
| 318 |
+
</div>
|
| 319 |
+
</Popover>
|
| 320 |
+
</ListItem>
|
| 321 |
+
|
| 322 |
+
<ListItem
|
| 323 |
+
title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
|
| 324 |
+
subTitle={
|
| 325 |
+
checkingUpdate
|
| 326 |
+
? Locale.Settings.Update.IsChecking
|
| 327 |
+
: hasNewVersion
|
| 328 |
+
? Locale.Settings.Update.FoundUpdate(remoteId ?? "ERROR")
|
| 329 |
+
: Locale.Settings.Update.IsLatest
|
| 330 |
+
}
|
| 331 |
+
>
|
| 332 |
+
{checkingUpdate ? (
|
| 333 |
+
<div />
|
| 334 |
+
) : hasNewVersion ? (
|
| 335 |
+
<Link href={UPDATE_URL} target="_blank" className="link">
|
| 336 |
+
{Locale.Settings.Update.GoToUpdate}
|
| 337 |
+
</Link>
|
| 338 |
+
) : (
|
| 339 |
+
<IconButton
|
| 340 |
+
icon={<ResetIcon></ResetIcon>}
|
| 341 |
+
text={Locale.Settings.Update.CheckUpdate}
|
| 342 |
+
onClick={() => checkUpdate(true)}
|
| 343 |
+
/>
|
| 344 |
+
)}
|
| 345 |
+
</ListItem>
|
| 346 |
+
|
| 347 |
+
<ListItem title={Locale.Settings.SendKey}>
|
| 348 |
+
<select
|
| 349 |
+
value={config.submitKey}
|
| 350 |
+
onChange={(e) => {
|
| 351 |
+
updateConfig(
|
| 352 |
+
(config) =>
|
| 353 |
+
(config.submitKey = e.target.value as any as SubmitKey),
|
| 354 |
+
);
|
| 355 |
+
}}
|
| 356 |
+
>
|
| 357 |
+
{Object.values(SubmitKey).map((v) => (
|
| 358 |
+
<option value={v} key={v}>
|
| 359 |
+
{v}
|
| 360 |
+
</option>
|
| 361 |
+
))}
|
| 362 |
+
</select>
|
| 363 |
+
</ListItem>
|
| 364 |
+
|
| 365 |
+
<ListItem title={Locale.Settings.Theme}>
|
| 366 |
+
<select
|
| 367 |
+
value={config.theme}
|
| 368 |
+
onChange={(e) => {
|
| 369 |
+
updateConfig(
|
| 370 |
+
(config) => (config.theme = e.target.value as any as Theme),
|
| 371 |
+
);
|
| 372 |
+
}}
|
| 373 |
+
>
|
| 374 |
+
{Object.values(Theme).map((v) => (
|
| 375 |
+
<option value={v} key={v}>
|
| 376 |
+
{v}
|
| 377 |
+
</option>
|
| 378 |
+
))}
|
| 379 |
+
</select>
|
| 380 |
+
</ListItem>
|
| 381 |
+
|
| 382 |
+
<ListItem title={Locale.Settings.Lang.Name}>
|
| 383 |
+
<select
|
| 384 |
+
value={getLang()}
|
| 385 |
+
onChange={(e) => {
|
| 386 |
+
changeLang(e.target.value as any);
|
| 387 |
+
}}
|
| 388 |
+
>
|
| 389 |
+
{AllLangs.map((lang) => (
|
| 390 |
+
<option value={lang} key={lang}>
|
| 391 |
+
{Locale.Settings.Lang.Options[lang]}
|
| 392 |
+
</option>
|
| 393 |
+
))}
|
| 394 |
+
</select>
|
| 395 |
+
</ListItem>
|
| 396 |
+
|
| 397 |
+
<ListItem
|
| 398 |
+
title={Locale.Settings.FontSize.Title}
|
| 399 |
+
subTitle={Locale.Settings.FontSize.SubTitle}
|
| 400 |
+
>
|
| 401 |
+
<InputRange
|
| 402 |
+
title={`${config.fontSize ?? 14}px`}
|
| 403 |
+
value={config.fontSize}
|
| 404 |
+
min="12"
|
| 405 |
+
max="18"
|
| 406 |
+
step="1"
|
| 407 |
+
onChange={(e) =>
|
| 408 |
+
updateConfig(
|
| 409 |
+
(config) =>
|
| 410 |
+
(config.fontSize = Number.parseInt(e.currentTarget.value)),
|
| 411 |
+
)
|
| 412 |
+
}
|
| 413 |
+
></InputRange>
|
| 414 |
+
</ListItem>
|
| 415 |
+
|
| 416 |
+
<ListItem
|
| 417 |
+
title={Locale.Settings.SendPreviewBubble.Title}
|
| 418 |
+
subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
|
| 419 |
+
>
|
| 420 |
+
<input
|
| 421 |
+
type="checkbox"
|
| 422 |
+
checked={config.sendPreviewBubble}
|
| 423 |
+
onChange={(e) =>
|
| 424 |
+
updateConfig(
|
| 425 |
+
(config) =>
|
| 426 |
+
(config.sendPreviewBubble = e.currentTarget.checked),
|
| 427 |
+
)
|
| 428 |
+
}
|
| 429 |
+
></input>
|
| 430 |
+
</ListItem>
|
| 431 |
+
|
| 432 |
+
<ListItem
|
| 433 |
+
title={Locale.Settings.Mask.Title}
|
| 434 |
+
subTitle={Locale.Settings.Mask.SubTitle}
|
| 435 |
+
>
|
| 436 |
+
<input
|
| 437 |
+
type="checkbox"
|
| 438 |
+
checked={!config.dontShowMaskSplashScreen}
|
| 439 |
+
onChange={(e) =>
|
| 440 |
+
updateConfig(
|
| 441 |
+
(config) =>
|
| 442 |
+
(config.dontShowMaskSplashScreen =
|
| 443 |
+
!e.currentTarget.checked),
|
| 444 |
+
)
|
| 445 |
+
}
|
| 446 |
+
></input>
|
| 447 |
+
</ListItem>
|
| 448 |
+
</List>
|
| 449 |
+
|
| 450 |
+
<List>
|
| 451 |
+
{enabledAccessControl ? (
|
| 452 |
+
<ListItem
|
| 453 |
+
title={Locale.Settings.AccessCode.Title}
|
| 454 |
+
subTitle={Locale.Settings.AccessCode.SubTitle}
|
| 455 |
+
>
|
| 456 |
+
<PasswordInput
|
| 457 |
+
value={accessStore.accessCode}
|
| 458 |
+
type="text"
|
| 459 |
+
placeholder={Locale.Settings.AccessCode.Placeholder}
|
| 460 |
+
onChange={(e) => {
|
| 461 |
+
accessStore.updateCode(e.currentTarget.value);
|
| 462 |
+
}}
|
| 463 |
+
/>
|
| 464 |
+
</ListItem>
|
| 465 |
+
) : (
|
| 466 |
+
<></>
|
| 467 |
+
)}
|
| 468 |
+
|
| 469 |
+
<ListItem
|
| 470 |
+
title={Locale.Settings.Token.Title}
|
| 471 |
+
subTitle={Locale.Settings.Token.SubTitle}
|
| 472 |
+
>
|
| 473 |
+
<PasswordInput
|
| 474 |
+
value={accessStore.token}
|
| 475 |
+
type="text"
|
| 476 |
+
placeholder={Locale.Settings.Token.Placeholder}
|
| 477 |
+
onChange={(e) => {
|
| 478 |
+
accessStore.updateToken(e.currentTarget.value);
|
| 479 |
+
}}
|
| 480 |
+
/>
|
| 481 |
+
</ListItem>
|
| 482 |
+
|
| 483 |
+
<ListItem
|
| 484 |
+
title={Locale.Settings.Usage.Title}
|
| 485 |
+
subTitle={
|
| 486 |
+
showUsage
|
| 487 |
+
? loadingUsage
|
| 488 |
+
? Locale.Settings.Usage.IsChecking
|
| 489 |
+
: Locale.Settings.Usage.SubTitle(
|
| 490 |
+
usage?.used ?? "[?]",
|
| 491 |
+
usage?.subscription ?? "[?]",
|
| 492 |
+
)
|
| 493 |
+
: Locale.Settings.Usage.NoAccess
|
| 494 |
+
}
|
| 495 |
+
>
|
| 496 |
+
{!showUsage || loadingUsage ? (
|
| 497 |
+
<div />
|
| 498 |
+
) : (
|
| 499 |
+
<IconButton
|
| 500 |
+
icon={<ResetIcon></ResetIcon>}
|
| 501 |
+
text={Locale.Settings.Usage.Check}
|
| 502 |
+
onClick={() => checkUsage(true)}
|
| 503 |
+
/>
|
| 504 |
+
)}
|
| 505 |
+
</ListItem>
|
| 506 |
+
</List>
|
| 507 |
+
|
| 508 |
+
<List>
|
| 509 |
+
<ListItem
|
| 510 |
+
title={Locale.Settings.Prompt.Disable.Title}
|
| 511 |
+
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
|
| 512 |
+
>
|
| 513 |
+
<input
|
| 514 |
+
type="checkbox"
|
| 515 |
+
checked={config.disablePromptHint}
|
| 516 |
+
onChange={(e) =>
|
| 517 |
+
updateConfig(
|
| 518 |
+
(config) =>
|
| 519 |
+
(config.disablePromptHint = e.currentTarget.checked),
|
| 520 |
+
)
|
| 521 |
+
}
|
| 522 |
+
></input>
|
| 523 |
+
</ListItem>
|
| 524 |
+
|
| 525 |
+
<ListItem
|
| 526 |
+
title={Locale.Settings.Prompt.List}
|
| 527 |
+
subTitle={Locale.Settings.Prompt.ListCount(
|
| 528 |
+
builtinCount,
|
| 529 |
+
customCount,
|
| 530 |
+
)}
|
| 531 |
+
>
|
| 532 |
+
<IconButton
|
| 533 |
+
icon={<EditIcon />}
|
| 534 |
+
text={Locale.Settings.Prompt.Edit}
|
| 535 |
+
onClick={() => setShowPromptModal(true)}
|
| 536 |
+
/>
|
| 537 |
+
</ListItem>
|
| 538 |
+
</List>
|
| 539 |
+
|
| 540 |
+
<List>
|
| 541 |
+
<ModelConfigList
|
| 542 |
+
modelConfig={config.modelConfig}
|
| 543 |
+
updateConfig={(upater) => {
|
| 544 |
+
const modelConfig = { ...config.modelConfig };
|
| 545 |
+
upater(modelConfig);
|
| 546 |
+
config.update((config) => (config.modelConfig = modelConfig));
|
| 547 |
+
}}
|
| 548 |
+
/>
|
| 549 |
+
</List>
|
| 550 |
+
|
| 551 |
+
{shouldShowPromptModal && (
|
| 552 |
+
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
| 553 |
+
)}
|
| 554 |
+
</div>
|
| 555 |
+
</ErrorBoundary>
|
| 556 |
+
);
|
| 557 |
+
}
|
app/components/sidebar.tsx
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from "react";
|
| 2 |
+
|
| 3 |
+
import styles from "./home.module.scss";
|
| 4 |
+
|
| 5 |
+
import { IconButton } from "./button";
|
| 6 |
+
import SettingsIcon from "../icons/settings.svg";
|
| 7 |
+
import GithubIcon from "../icons/github.svg";
|
| 8 |
+
import ChatGptIcon from "../icons/chatgpt.svg";
|
| 9 |
+
import AddIcon from "../icons/add.svg";
|
| 10 |
+
import CloseIcon from "../icons/close.svg";
|
| 11 |
+
import MaskIcon from "../icons/mask.svg";
|
| 12 |
+
import PluginIcon from "../icons/plugin.svg";
|
| 13 |
+
|
| 14 |
+
import Locale from "../locales";
|
| 15 |
+
|
| 16 |
+
import { useAppConfig, useChatStore } from "../store";
|
| 17 |
+
|
| 18 |
+
import {
|
| 19 |
+
MAX_SIDEBAR_WIDTH,
|
| 20 |
+
MIN_SIDEBAR_WIDTH,
|
| 21 |
+
NARROW_SIDEBAR_WIDTH,
|
| 22 |
+
Path,
|
| 23 |
+
REPO_URL,
|
| 24 |
+
} from "../constant";
|
| 25 |
+
|
| 26 |
+
import { Link, useNavigate } from "react-router-dom";
|
| 27 |
+
import { useMobileScreen } from "../utils";
|
| 28 |
+
import dynamic from "next/dynamic";
|
| 29 |
+
import { showToast } from "./ui-lib";
|
| 30 |
+
|
| 31 |
+
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
| 32 |
+
loading: () => null,
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
function useDragSideBar() {
|
| 36 |
+
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
| 37 |
+
|
| 38 |
+
const config = useAppConfig();
|
| 39 |
+
const startX = useRef(0);
|
| 40 |
+
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
| 41 |
+
const lastUpdateTime = useRef(Date.now());
|
| 42 |
+
|
| 43 |
+
const handleMouseMove = useRef((e: MouseEvent) => {
|
| 44 |
+
if (Date.now() < lastUpdateTime.current + 50) {
|
| 45 |
+
return;
|
| 46 |
+
}
|
| 47 |
+
lastUpdateTime.current = Date.now();
|
| 48 |
+
const d = e.clientX - startX.current;
|
| 49 |
+
const nextWidth = limit(startDragWidth.current + d);
|
| 50 |
+
config.update((config) => (config.sidebarWidth = nextWidth));
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
const handleMouseUp = useRef(() => {
|
| 54 |
+
startDragWidth.current = config.sidebarWidth ?? 300;
|
| 55 |
+
window.removeEventListener("mousemove", handleMouseMove.current);
|
| 56 |
+
window.removeEventListener("mouseup", handleMouseUp.current);
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
const onDragMouseDown = (e: MouseEvent) => {
|
| 60 |
+
startX.current = e.clientX;
|
| 61 |
+
|
| 62 |
+
window.addEventListener("mousemove", handleMouseMove.current);
|
| 63 |
+
window.addEventListener("mouseup", handleMouseUp.current);
|
| 64 |
+
};
|
| 65 |
+
const isMobileScreen = useMobileScreen();
|
| 66 |
+
const shouldNarrow =
|
| 67 |
+
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
| 68 |
+
|
| 69 |
+
useEffect(() => {
|
| 70 |
+
const barWidth = shouldNarrow
|
| 71 |
+
? NARROW_SIDEBAR_WIDTH
|
| 72 |
+
: limit(config.sidebarWidth ?? 300);
|
| 73 |
+
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
| 74 |
+
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
| 75 |
+
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
| 76 |
+
|
| 77 |
+
return {
|
| 78 |
+
onDragMouseDown,
|
| 79 |
+
shouldNarrow,
|
| 80 |
+
};
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
export function SideBar(props: { className?: string }) {
|
| 84 |
+
const chatStore = useChatStore();
|
| 85 |
+
|
| 86 |
+
// drag side bar
|
| 87 |
+
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
| 88 |
+
const navigate = useNavigate();
|
| 89 |
+
|
| 90 |
+
const config = useAppConfig();
|
| 91 |
+
|
| 92 |
+
return (
|
| 93 |
+
<div
|
| 94 |
+
className={`${styles.sidebar} ${props.className} ${
|
| 95 |
+
shouldNarrow && styles["narrow-sidebar"]
|
| 96 |
+
}`}
|
| 97 |
+
>
|
| 98 |
+
<div className={styles["sidebar-header"]}>
|
| 99 |
+
<div className={styles["sidebar-title"]}>ChatGPT Next</div>
|
| 100 |
+
<div className={styles["sidebar-sub-title"]}>
|
| 101 |
+
Build your own AI assistant.
|
| 102 |
+
</div>
|
| 103 |
+
<div className={styles["sidebar-logo"] + " no-dark"}>
|
| 104 |
+
<ChatGptIcon />
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<div className={styles["sidebar-header-bar"]}>
|
| 109 |
+
<IconButton
|
| 110 |
+
icon={<MaskIcon />}
|
| 111 |
+
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
| 112 |
+
className={styles["sidebar-bar-button"]}
|
| 113 |
+
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
|
| 114 |
+
shadow
|
| 115 |
+
/>
|
| 116 |
+
<IconButton
|
| 117 |
+
icon={<PluginIcon />}
|
| 118 |
+
text={shouldNarrow ? undefined : Locale.Plugin.Name}
|
| 119 |
+
className={styles["sidebar-bar-button"]}
|
| 120 |
+
onClick={() => showToast(Locale.WIP)}
|
| 121 |
+
shadow
|
| 122 |
+
/>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div
|
| 126 |
+
className={styles["sidebar-body"]}
|
| 127 |
+
onClick={(e) => {
|
| 128 |
+
if (e.target === e.currentTarget) {
|
| 129 |
+
navigate(Path.Home);
|
| 130 |
+
}
|
| 131 |
+
}}
|
| 132 |
+
>
|
| 133 |
+
<ChatList narrow={shouldNarrow} />
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div className={styles["sidebar-tail"]}>
|
| 137 |
+
<div className={styles["sidebar-actions"]}>
|
| 138 |
+
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
| 139 |
+
<IconButton
|
| 140 |
+
icon={<CloseIcon />}
|
| 141 |
+
onClick={() => {
|
| 142 |
+
if (confirm(Locale.Home.DeleteChat)) {
|
| 143 |
+
chatStore.deleteSession(chatStore.currentSessionIndex);
|
| 144 |
+
}
|
| 145 |
+
}}
|
| 146 |
+
/>
|
| 147 |
+
</div>
|
| 148 |
+
<div className={styles["sidebar-action"]}>
|
| 149 |
+
<Link to={Path.Settings}>
|
| 150 |
+
<IconButton icon={<SettingsIcon />} shadow />
|
| 151 |
+
</Link>
|
| 152 |
+
</div>
|
| 153 |
+
<div className={styles["sidebar-action"]}>
|
| 154 |
+
<a href={REPO_URL} target="_blank">
|
| 155 |
+
<IconButton icon={<GithubIcon />} shadow />
|
| 156 |
+
</a>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
<div>
|
| 160 |
+
<IconButton
|
| 161 |
+
icon={<AddIcon />}
|
| 162 |
+
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
| 163 |
+
onClick={() => {
|
| 164 |
+
if (config.dontShowMaskSplashScreen) {
|
| 165 |
+
chatStore.newSession();
|
| 166 |
+
} else {
|
| 167 |
+
navigate(Path.NewChat);
|
| 168 |
+
}
|
| 169 |
+
}}
|
| 170 |
+
shadow
|
| 171 |
+
/>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<div
|
| 176 |
+
className={styles["sidebar-drag"]}
|
| 177 |
+
onMouseDown={(e) => onDragMouseDown(e as any)}
|
| 178 |
+
></div>
|
| 179 |
+
</div>
|
| 180 |
+
);
|
| 181 |
+
}
|
app/components/ui-lib.module.scss
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "../styles/animation.scss";
|
| 2 |
+
|
| 3 |
+
.card {
|
| 4 |
+
background-color: var(--white);
|
| 5 |
+
border-radius: 10px;
|
| 6 |
+
box-shadow: var(--card-shadow);
|
| 7 |
+
padding: 10px;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.popover {
|
| 11 |
+
position: relative;
|
| 12 |
+
z-index: 2;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.popover-content {
|
| 16 |
+
position: absolute;
|
| 17 |
+
animation: slide-in 0.3s ease;
|
| 18 |
+
right: 0;
|
| 19 |
+
top: calc(100% + 10px);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.popover-mask {
|
| 23 |
+
position: fixed;
|
| 24 |
+
top: 0;
|
| 25 |
+
left: 0;
|
| 26 |
+
width: 100vw;
|
| 27 |
+
height: 100vh;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.list-item {
|
| 31 |
+
display: flex;
|
| 32 |
+
justify-content: space-between;
|
| 33 |
+
align-items: center;
|
| 34 |
+
min-height: 40px;
|
| 35 |
+
border-bottom: var(--border-in-light);
|
| 36 |
+
padding: 10px 20px;
|
| 37 |
+
animation: slide-in ease 0.6s;
|
| 38 |
+
|
| 39 |
+
.list-header {
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
|
| 43 |
+
.list-icon {
|
| 44 |
+
margin-right: 10px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.list-item-title {
|
| 48 |
+
font-size: 14px;
|
| 49 |
+
font-weight: bolder;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.list-item-sub-title {
|
| 53 |
+
font-size: 12px;
|
| 54 |
+
font-weight: normal;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.list {
|
| 60 |
+
border: var(--border-in-light);
|
| 61 |
+
border-radius: 10px;
|
| 62 |
+
box-shadow: var(--card-shadow);
|
| 63 |
+
margin-bottom: 20px;
|
| 64 |
+
animation: slide-in ease 0.3s;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.list .list-item:last-child {
|
| 68 |
+
border: 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.modal-container {
|
| 72 |
+
box-shadow: var(--card-shadow);
|
| 73 |
+
background-color: var(--white);
|
| 74 |
+
border-radius: 12px;
|
| 75 |
+
width: 60vw;
|
| 76 |
+
animation: slide-in ease 0.3s;
|
| 77 |
+
|
| 78 |
+
--modal-padding: 20px;
|
| 79 |
+
|
| 80 |
+
.modal-header {
|
| 81 |
+
padding: var(--modal-padding);
|
| 82 |
+
display: flex;
|
| 83 |
+
align-items: center;
|
| 84 |
+
justify-content: space-between;
|
| 85 |
+
border-bottom: var(--border-in-light);
|
| 86 |
+
|
| 87 |
+
.modal-title {
|
| 88 |
+
font-weight: bolder;
|
| 89 |
+
font-size: 16px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.modal-close-btn {
|
| 93 |
+
cursor: pointer;
|
| 94 |
+
|
| 95 |
+
&:hover {
|
| 96 |
+
filter: brightness(1.2);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.modal-content {
|
| 102 |
+
max-height: 40vh;
|
| 103 |
+
padding: var(--modal-padding);
|
| 104 |
+
overflow: auto;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.modal-footer {
|
| 108 |
+
padding: var(--modal-padding);
|
| 109 |
+
display: flex;
|
| 110 |
+
justify-content: flex-end;
|
| 111 |
+
border-top: var(--border-in-light);
|
| 112 |
+
box-shadow: var(--shadow);
|
| 113 |
+
|
| 114 |
+
.modal-actions {
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
|
| 118 |
+
.modal-action {
|
| 119 |
+
&:not(:last-child) {
|
| 120 |
+
margin-right: 20px;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.show {
|
| 128 |
+
opacity: 1;
|
| 129 |
+
transition: all ease 0.3s;
|
| 130 |
+
transform: translateY(0);
|
| 131 |
+
position: fixed;
|
| 132 |
+
left: 0;
|
| 133 |
+
bottom: 0;
|
| 134 |
+
animation: slide-in ease 0.6s;
|
| 135 |
+
z-index: 99999;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.hide {
|
| 139 |
+
opacity: 0;
|
| 140 |
+
transition: all ease 0.3s;
|
| 141 |
+
transform: translateY(20px);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.toast-container {
|
| 145 |
+
position: fixed;
|
| 146 |
+
bottom: 5vh;
|
| 147 |
+
left: 0;
|
| 148 |
+
width: 100vw;
|
| 149 |
+
display: flex;
|
| 150 |
+
justify-content: center;
|
| 151 |
+
pointer-events: none;
|
| 152 |
+
|
| 153 |
+
.toast-content {
|
| 154 |
+
max-width: 80vw;
|
| 155 |
+
word-break: break-all;
|
| 156 |
+
font-size: 14px;
|
| 157 |
+
background-color: var(--white);
|
| 158 |
+
box-shadow: var(--card-shadow);
|
| 159 |
+
border: var(--border-in-light);
|
| 160 |
+
color: var(--black);
|
| 161 |
+
padding: 10px 20px;
|
| 162 |
+
border-radius: 50px;
|
| 163 |
+
margin-bottom: 20px;
|
| 164 |
+
display: flex;
|
| 165 |
+
align-items: center;
|
| 166 |
+
pointer-events: all;
|
| 167 |
+
|
| 168 |
+
.toast-action {
|
| 169 |
+
padding-left: 20px;
|
| 170 |
+
color: var(--primary);
|
| 171 |
+
opacity: 0.8;
|
| 172 |
+
border: 0;
|
| 173 |
+
background: none;
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
font-family: inherit;
|
| 176 |
+
|
| 177 |
+
&:hover {
|
| 178 |
+
opacity: 1;
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.input {
|
| 185 |
+
border: var(--border-in-light);
|
| 186 |
+
border-radius: 10px;
|
| 187 |
+
padding: 10px;
|
| 188 |
+
font-family: inherit;
|
| 189 |
+
background-color: var(--white);
|
| 190 |
+
color: var(--black);
|
| 191 |
+
resize: none;
|
| 192 |
+
min-width: 50px;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
@media only screen and (max-width: 600px) {
|
| 196 |
+
.modal-container {
|
| 197 |
+
width: 90vw;
|
| 198 |
+
|
| 199 |
+
.modal-content {
|
| 200 |
+
max-height: 50vh;
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
}
|
app/components/ui-lib.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import styles from "./ui-lib.module.scss";
|
| 2 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
| 3 |
+
import CloseIcon from "../icons/close.svg";
|
| 4 |
+
import EyeIcon from "../icons/eye.svg";
|
| 5 |
+
import EyeOffIcon from "../icons/eye-off.svg";
|
| 6 |
+
|
| 7 |
+
import { createRoot } from "react-dom/client";
|
| 8 |
+
import React, { HTMLProps, useEffect, useState } from "react";
|
| 9 |
+
import { IconButton } from "./button";
|
| 10 |
+
|
| 11 |
+
export function Popover(props: {
|
| 12 |
+
children: JSX.Element;
|
| 13 |
+
content: JSX.Element;
|
| 14 |
+
open?: boolean;
|
| 15 |
+
onClose?: () => void;
|
| 16 |
+
}) {
|
| 17 |
+
return (
|
| 18 |
+
<div className={styles.popover}>
|
| 19 |
+
{props.children}
|
| 20 |
+
{props.open && (
|
| 21 |
+
<div className={styles["popover-content"]}>
|
| 22 |
+
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
|
| 23 |
+
{props.content}
|
| 24 |
+
</div>
|
| 25 |
+
)}
|
| 26 |
+
</div>
|
| 27 |
+
);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function Card(props: { children: JSX.Element[]; className?: string }) {
|
| 31 |
+
return (
|
| 32 |
+
<div className={styles.card + " " + props.className}>{props.children}</div>
|
| 33 |
+
);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export function ListItem(props: {
|
| 37 |
+
title: string;
|
| 38 |
+
subTitle?: string;
|
| 39 |
+
children?: JSX.Element | JSX.Element[];
|
| 40 |
+
icon?: JSX.Element;
|
| 41 |
+
className?: string;
|
| 42 |
+
}) {
|
| 43 |
+
return (
|
| 44 |
+
<div className={styles["list-item"] + ` ${props.className}`}>
|
| 45 |
+
<div className={styles["list-header"]}>
|
| 46 |
+
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
|
| 47 |
+
<div className={styles["list-item-title"]}>
|
| 48 |
+
<div>{props.title}</div>
|
| 49 |
+
{props.subTitle && (
|
| 50 |
+
<div className={styles["list-item-sub-title"]}>
|
| 51 |
+
{props.subTitle}
|
| 52 |
+
</div>
|
| 53 |
+
)}
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
{props.children}
|
| 57 |
+
</div>
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export function List(props: {
|
| 62 |
+
children:
|
| 63 |
+
| Array<JSX.Element | null | undefined>
|
| 64 |
+
| JSX.Element
|
| 65 |
+
| null
|
| 66 |
+
| undefined;
|
| 67 |
+
}) {
|
| 68 |
+
return <div className={styles.list}>{props.children}</div>;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export function Loading() {
|
| 72 |
+
return (
|
| 73 |
+
<div
|
| 74 |
+
style={{
|
| 75 |
+
height: "100vh",
|
| 76 |
+
width: "100vw",
|
| 77 |
+
display: "flex",
|
| 78 |
+
alignItems: "center",
|
| 79 |
+
justifyContent: "center",
|
| 80 |
+
}}
|
| 81 |
+
>
|
| 82 |
+
<LoadingIcon />
|
| 83 |
+
</div>
|
| 84 |
+
);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
interface ModalProps {
|
| 88 |
+
title: string;
|
| 89 |
+
children?: JSX.Element | JSX.Element[];
|
| 90 |
+
actions?: JSX.Element[];
|
| 91 |
+
onClose?: () => void;
|
| 92 |
+
}
|
| 93 |
+
export function Modal(props: ModalProps) {
|
| 94 |
+
useEffect(() => {
|
| 95 |
+
const onKeyDown = (e: KeyboardEvent) => {
|
| 96 |
+
if (e.key === "Escape") {
|
| 97 |
+
props.onClose?.();
|
| 98 |
+
}
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
window.addEventListener("keydown", onKeyDown);
|
| 102 |
+
|
| 103 |
+
return () => {
|
| 104 |
+
window.removeEventListener("keydown", onKeyDown);
|
| 105 |
+
};
|
| 106 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 107 |
+
}, []);
|
| 108 |
+
|
| 109 |
+
return (
|
| 110 |
+
<div className={styles["modal-container"]}>
|
| 111 |
+
<div className={styles["modal-header"]}>
|
| 112 |
+
<div className={styles["modal-title"]}>{props.title}</div>
|
| 113 |
+
|
| 114 |
+
<div className={styles["modal-close-btn"]} onClick={props.onClose}>
|
| 115 |
+
<CloseIcon />
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<div className={styles["modal-content"]}>{props.children}</div>
|
| 120 |
+
|
| 121 |
+
<div className={styles["modal-footer"]}>
|
| 122 |
+
<div className={styles["modal-actions"]}>
|
| 123 |
+
{props.actions?.map((action, i) => (
|
| 124 |
+
<div key={i} className={styles["modal-action"]}>
|
| 125 |
+
{action}
|
| 126 |
+
</div>
|
| 127 |
+
))}
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
export function showModal(props: ModalProps) {
|
| 135 |
+
const div = document.createElement("div");
|
| 136 |
+
div.className = "modal-mask";
|
| 137 |
+
document.body.appendChild(div);
|
| 138 |
+
|
| 139 |
+
const root = createRoot(div);
|
| 140 |
+
const closeModal = () => {
|
| 141 |
+
props.onClose?.();
|
| 142 |
+
root.unmount();
|
| 143 |
+
div.remove();
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
div.onclick = (e) => {
|
| 147 |
+
if (e.target === div) {
|
| 148 |
+
closeModal();
|
| 149 |
+
}
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
root.render(<Modal {...props} onClose={closeModal}></Modal>);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
export type ToastProps = {
|
| 156 |
+
content: string;
|
| 157 |
+
action?: {
|
| 158 |
+
text: string;
|
| 159 |
+
onClick: () => void;
|
| 160 |
+
};
|
| 161 |
+
onClose?: () => void;
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
export function Toast(props: ToastProps) {
|
| 165 |
+
return (
|
| 166 |
+
<div className={styles["toast-container"]}>
|
| 167 |
+
<div className={styles["toast-content"]}>
|
| 168 |
+
<span>{props.content}</span>
|
| 169 |
+
{props.action && (
|
| 170 |
+
<button
|
| 171 |
+
onClick={() => {
|
| 172 |
+
props.action?.onClick?.();
|
| 173 |
+
props.onClose?.();
|
| 174 |
+
}}
|
| 175 |
+
className={styles["toast-action"]}
|
| 176 |
+
>
|
| 177 |
+
{props.action.text}
|
| 178 |
+
</button>
|
| 179 |
+
)}
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
export function showToast(
|
| 186 |
+
content: string,
|
| 187 |
+
action?: ToastProps["action"],
|
| 188 |
+
delay = 3000,
|
| 189 |
+
) {
|
| 190 |
+
const div = document.createElement("div");
|
| 191 |
+
div.className = styles.show;
|
| 192 |
+
document.body.appendChild(div);
|
| 193 |
+
|
| 194 |
+
const root = createRoot(div);
|
| 195 |
+
const close = () => {
|
| 196 |
+
div.classList.add(styles.hide);
|
| 197 |
+
|
| 198 |
+
setTimeout(() => {
|
| 199 |
+
root.unmount();
|
| 200 |
+
div.remove();
|
| 201 |
+
}, 300);
|
| 202 |
+
};
|
| 203 |
+
|
| 204 |
+
setTimeout(() => {
|
| 205 |
+
close();
|
| 206 |
+
}, delay);
|
| 207 |
+
|
| 208 |
+
root.render(<Toast content={content} action={action} onClose={close} />);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {
|
| 212 |
+
autoHeight?: boolean;
|
| 213 |
+
rows?: number;
|
| 214 |
+
};
|
| 215 |
+
|
| 216 |
+
export function Input(props: InputProps) {
|
| 217 |
+
return (
|
| 218 |
+
<textarea
|
| 219 |
+
{...props}
|
| 220 |
+
className={`${styles["input"]} ${props.className}`}
|
| 221 |
+
></textarea>
|
| 222 |
+
);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
| 226 |
+
const [visible, setVisible] = useState(false);
|
| 227 |
+
|
| 228 |
+
function changeVisibility() {
|
| 229 |
+
setVisible(!visible);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
return (
|
| 233 |
+
<div className={"password-input-container"}>
|
| 234 |
+
<IconButton
|
| 235 |
+
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
|
| 236 |
+
onClick={changeVisibility}
|
| 237 |
+
className={"password-eye"}
|
| 238 |
+
/>
|
| 239 |
+
<input
|
| 240 |
+
{...props}
|
| 241 |
+
type={visible ? "text" : "password"}
|
| 242 |
+
className={"password-input"}
|
| 243 |
+
/>
|
| 244 |
+
</div>
|
| 245 |
+
);
|
| 246 |
+
}
|
app/config/build.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const COMMIT_ID: string = (() => {
|
| 2 |
+
try {
|
| 3 |
+
const childProcess = require("child_process");
|
| 4 |
+
return (
|
| 5 |
+
childProcess
|
| 6 |
+
// .execSync("git describe --tags --abbrev=0")
|
| 7 |
+
.execSync("git rev-parse --short HEAD")
|
| 8 |
+
.toString()
|
| 9 |
+
.trim()
|
| 10 |
+
);
|
| 11 |
+
} catch (e) {
|
| 12 |
+
console.error("[Build Config] No git or not from git repo.");
|
| 13 |
+
return "unknown";
|
| 14 |
+
}
|
| 15 |
+
})();
|
| 16 |
+
|
| 17 |
+
export const getBuildConfig = () => {
|
| 18 |
+
if (typeof process === "undefined") {
|
| 19 |
+
throw Error(
|
| 20 |
+
"[Server Config] you are importing a nodejs-only module outside of nodejs",
|
| 21 |
+
);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
return {
|
| 25 |
+
commitId: COMMIT_ID,
|
| 26 |
+
};
|
| 27 |
+
};
|
app/config/server.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import md5 from "spark-md5";
|
| 2 |
+
|
| 3 |
+
declare global {
|
| 4 |
+
namespace NodeJS {
|
| 5 |
+
interface ProcessEnv {
|
| 6 |
+
OPENAI_API_KEY?: string;
|
| 7 |
+
CODE?: string;
|
| 8 |
+
PROXY_URL?: string;
|
| 9 |
+
VERCEL?: string;
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const ACCESS_CODES = (function getAccessCodes(): Set<string> {
|
| 15 |
+
const code = process.env.CODE;
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
const codes = (code?.split(",") ?? [])
|
| 19 |
+
.filter((v) => !!v)
|
| 20 |
+
.map((v) => md5.hash(v.trim()));
|
| 21 |
+
return new Set(codes);
|
| 22 |
+
} catch (e) {
|
| 23 |
+
return new Set();
|
| 24 |
+
}
|
| 25 |
+
})();
|
| 26 |
+
|
| 27 |
+
export const getServerSideConfig = () => {
|
| 28 |
+
if (typeof process === "undefined") {
|
| 29 |
+
throw Error(
|
| 30 |
+
"[Server Config] you are importing a nodejs-only module outside of nodejs",
|
| 31 |
+
);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return {
|
| 35 |
+
apiKey: process.env.OPENAI_API_KEY,
|
| 36 |
+
code: process.env.CODE,
|
| 37 |
+
codes: ACCESS_CODES,
|
| 38 |
+
needCode: ACCESS_CODES.size > 0,
|
| 39 |
+
proxyUrl: process.env.PROXY_URL,
|
| 40 |
+
isVercel: !!process.env.VERCEL,
|
| 41 |
+
};
|
| 42 |
+
};
|
app/constant.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const OWNER = "Yidadaa";
|
| 2 |
+
export const REPO = "ChatGPT-Next-Web";
|
| 3 |
+
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
| 4 |
+
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
|
| 5 |
+
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
|
| 6 |
+
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
| 7 |
+
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
| 8 |
+
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
| 9 |
+
|
| 10 |
+
export enum Path {
|
| 11 |
+
Home = "/",
|
| 12 |
+
Chat = "/chat",
|
| 13 |
+
Settings = "/settings",
|
| 14 |
+
NewChat = "/new-chat",
|
| 15 |
+
Masks = "/masks",
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export enum SlotID {
|
| 19 |
+
AppBody = "app-body",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export enum FileName {
|
| 23 |
+
Masks = "masks.json",
|
| 24 |
+
Prompts = "prompts.json",
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export enum StoreKey {
|
| 28 |
+
Chat = "chat-next-web-store",
|
| 29 |
+
Access = "access-control",
|
| 30 |
+
Config = "app-config",
|
| 31 |
+
Mask = "mask-store",
|
| 32 |
+
Prompt = "prompt-store",
|
| 33 |
+
Update = "chat-update",
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export const MAX_SIDEBAR_WIDTH = 500;
|
| 37 |
+
export const MIN_SIDEBAR_WIDTH = 230;
|
| 38 |
+
export const NARROW_SIDEBAR_WIDTH = 100;
|
app/global.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
declare module "*.jpg";
|
| 2 |
+
declare module "*.png";
|
| 3 |
+
declare module "*.woff2";
|
| 4 |
+
declare module "*.woff";
|
| 5 |
+
declare module "*.ttf";
|
| 6 |
+
declare module "*.scss" {
|
| 7 |
+
const content: Record<string, string>;
|
| 8 |
+
export default content;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
declare module "*.svg";
|
app/icons/add.svg
ADDED
|
|
app/icons/auto.svg
ADDED
|
|
app/icons/black-bot.svg
ADDED
|
|