Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ff5db7b41 | ||
|
|
56a0b48b97 | ||
|
|
ff24042df5 | ||
|
|
c31d247f07 | ||
|
|
e903eb5b94 | ||
|
|
c605964fa8 | ||
|
|
1fe5cd751a | ||
|
|
488e2f476e | ||
|
|
915b104b8a | ||
|
|
aaa350a13e | ||
|
|
6a2b34cb92 | ||
|
|
7f26b31f53 | ||
|
|
2a597964a2 | ||
|
|
c1d3a46dc7 | ||
|
|
0c55beb72d | ||
|
|
9b1c0e1a3c | ||
|
|
a7988c164e | ||
|
|
99e5fbd0f5 | ||
|
|
5e4c4dd79b | ||
|
|
70584783a5 | ||
|
|
705ac1c27e | ||
|
|
52d00d0562 | ||
|
|
9a145f223f | ||
|
|
b7cd4dec89 | ||
|
|
33154a9c19 | ||
|
|
e1c7503611 | ||
|
|
d04c298132 | ||
|
|
eceda01c19 | ||
|
|
ea1681e1eb | ||
|
|
f6c4b4c96d | ||
|
|
22cc9c85be | ||
|
|
43f8d6008f | ||
|
|
29c5554f9e | ||
|
|
9b18a46456 | ||
|
|
d5923bc64f | ||
|
|
f19c2d2ca1 | ||
|
|
84d91f3f76 | ||
|
|
7811f7482b | ||
|
|
9c8ca7dd25 | ||
|
|
1409916bd0 | ||
|
|
fc7edcb54f | ||
|
|
87d35042de | ||
|
|
77dc961a07 | ||
|
|
9a45fb64c2 | ||
|
|
881c36542c | ||
|
|
f88c6031f5 | ||
|
|
8a02b3b04a | ||
|
|
d460305871 | ||
|
|
144bed5a77 | ||
|
|
96fc917bad | ||
|
|
794a3698ad | ||
|
|
fbbc32361b | ||
|
|
dc329041f3 | ||
|
|
5feb2e19bf | ||
|
|
ec22cd8320 | ||
|
|
8c7efcbd1a | ||
|
|
afc5947bfb | ||
|
|
40189a6899 | ||
|
|
b73829a25c | ||
|
|
a7c5d3cc05 | ||
|
|
cc36a13f17 | ||
|
|
943abbe0fb | ||
|
|
b13c3c4da5 | ||
|
|
c12aa7fdf7 | ||
|
|
e08e8aa00b | ||
|
|
85e11abc0a | ||
|
|
becee69d6a | ||
|
|
042b0c535a | ||
|
|
f97c29b41e | ||
|
|
4d6616cbfa | ||
|
|
cf37992b5c | ||
|
|
6c4026ccef | ||
|
|
caf31faf31 | ||
|
|
a0832af14b | ||
|
|
677e61416d | ||
|
|
56ba6fa5f7 | ||
|
|
16a31de1c7 | ||
|
|
05b2e9e99c | ||
|
|
ae4243b522 | ||
|
|
5759cbeae0 | ||
|
|
a3d74ec4a6 | ||
|
|
df9ac99ef2 | ||
|
|
ef1e8aef5c | ||
|
|
56dab7abba | ||
|
|
ed1f93d836 | ||
|
|
5ec8aac3ac | ||
|
|
837c132d24 | ||
|
|
8239c58494 | ||
|
|
456686f3d0 | ||
|
|
2099a87908 | ||
|
|
2b2c70e53d | ||
|
|
f32c557bdd | ||
|
|
713332522f | ||
|
|
586607a9ce | ||
|
|
8c70205940 | ||
|
|
c3ccbcb7f6 | ||
|
|
7a6d0ea650 | ||
|
|
7fb6f62cf6 | ||
|
|
af385b1b42 | ||
|
|
5249297cb1 | ||
|
|
9280a21d12 | ||
|
|
650a9dd651 | ||
|
|
42e12d7db1 | ||
|
|
249ed18d15 | ||
|
|
3e4487ad9a | ||
|
|
888642f154 | ||
|
|
963e590dfd | ||
|
|
e547893ac1 | ||
|
|
9125910cfe | ||
|
|
98c458dcf8 | ||
|
|
41b6401c13 | ||
|
|
936e36205e | ||
|
|
e289c4ec53 | ||
|
|
1cbcc62494 | ||
|
|
da31ef286b | ||
|
|
60f62507bd | ||
|
|
75cf3d1e9f | ||
|
|
02cee35a45 | ||
|
|
0cee404c7f | ||
|
|
274ece1d91 | ||
|
|
6bba859060 | ||
|
|
4eaf3a1be0 | ||
|
|
3db690773f | ||
|
|
8a9f1ed29b | ||
|
|
c0dc5a74c9 | ||
|
|
a35cda6873 | ||
|
|
30678d8ebf | ||
|
|
6ce727f9ea | ||
|
|
81e6821174 | ||
|
|
d8290f0809 | ||
|
|
8b72dca533 | ||
|
|
4d64068591 | ||
|
|
af35e17fdb | ||
|
|
5ec303610c | ||
|
|
984baf60f0 | ||
|
|
d065539707 | ||
|
|
129f3a2a30 | ||
|
|
42c26bd155 | ||
|
|
dc467c26b5 | ||
|
|
3aeb510f43 | ||
|
|
405a75e23b | ||
|
|
be47169fa8 | ||
|
|
58a010c12c | ||
|
|
7ba14d2c14 | ||
|
|
00b90f071d | ||
|
|
1c364eca35 | ||
|
|
9384419c9d | ||
|
|
38c093d9ae | ||
|
|
e6c9ca540a | ||
|
|
d85b4c0945 | ||
|
|
1e770088d0 | ||
|
|
7529f51e72 | ||
|
|
be69cfb966 | ||
|
|
6244f6c1fb | ||
|
|
e12f97a73b | ||
|
|
7f96c4ff9b | ||
|
|
138b607ac7 | ||
|
|
b204c55bd1 | ||
|
|
17cbfa05d3 | ||
|
|
a7b9940d7a | ||
|
|
fd8135f50c | ||
|
|
9f96593136 | ||
|
|
ed9e72ec9a | ||
|
|
7fb76cde0b | ||
|
|
38d49ea05f | ||
|
|
7db87c2d09 | ||
|
|
453f3be8ce | ||
|
|
f023f63103 | ||
|
|
65da4653bc | ||
|
|
26888e855b | ||
|
|
e132c622a6 | ||
|
|
17364e9da3 | ||
|
|
e5fe670a6e | ||
|
|
7807b26707 | ||
|
|
16775430ea | ||
|
|
dd5217d8a5 | ||
|
|
9f8d696bbe | ||
|
|
bf81d23de4 | ||
|
|
52a752dab5 | ||
|
|
78903baefa | ||
|
|
45ad3ba22a | ||
|
|
c03a7db633 | ||
|
|
2cc32d1806 |
@@ -8,3 +8,4 @@ README.md
|
|||||||
|
|
||||||
.yalc/
|
.yalc/
|
||||||
yalc.lock
|
yalc.lock
|
||||||
|
testApi/
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
AXIOS_PROXY_HOST=127.0.0.1
|
AXIOS_PROXY_HOST=127.0.0.1
|
||||||
AXIOS_PROXY_PORT=33210
|
AXIOS_PROXY_PORT_FAST=7890
|
||||||
MONGODB_UR=
|
AXIOS_PROXY_PORT_NORMAL=7890
|
||||||
MY_MAIL=
|
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false
|
||||||
MAILE_CODE=
|
MY_MAIL=11111111@qq.com
|
||||||
TOKEN_KEY=
|
MAILE_CODE=sdasadasfasfad
|
||||||
|
TOKEN_KEY=sssssssss
|
||||||
|
OPENAIKEY=sk-afadfadfadfsd
|
||||||
|
REDIS_URL=redis://default:password@0.0.0.0:8100
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "next/core-web-vitals"
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"react-hooks/rules-of-hooks": 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -34,6 +34,7 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
public/trainData/
|
/public/trainData/
|
||||||
.vscode/
|
/.vscode/
|
||||||
platform.json
|
platform.json
|
||||||
|
testApi/
|
||||||
0
.husky/pre-commit
Normal file → Executable file
@@ -18,4 +18,3 @@ module.exports = {
|
|||||||
htmlWhitespaceSensitivity: 'css',
|
htmlWhitespaceSensitivity: 'css',
|
||||||
endOfLine: 'lf'
|
endOfLine: 'lf'
|
||||||
};
|
};
|
||||||
|
|
||||||
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.formatOnType": true,
|
|
||||||
"editor.formatOnSave": true ,
|
|
||||||
"prettier.tabWidth": 2
|
|
||||||
}
|
|
||||||
@@ -54,13 +54,4 @@ USER nextjs
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENV PORT 3000
|
|
||||||
ENV MAX_USER ''
|
|
||||||
ENV AXIOS_PROXY_HOST ''
|
|
||||||
ENV AXIOS_PROXY_PORT ''
|
|
||||||
ENV MONGODB_UR ''
|
|
||||||
ENV MY_MAIL ''
|
|
||||||
ENV MAILE_CODE ''
|
|
||||||
ENV TOKEN_KEY ''
|
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
6
Makefile
@@ -1,4 +1,4 @@
|
|||||||
SERVICE_NAME=doc-gpt
|
SERVICE_NAME=fast-gpt
|
||||||
# Image URL to use all building/pushing image targets
|
# Image URL to use all building/pushing image targets
|
||||||
IMG ?= $(SERVICE_NAME):latest
|
IMG ?= $(SERVICE_NAME):latest
|
||||||
|
|
||||||
@@ -34,13 +34,13 @@ run: ## Run a dev service from host.
|
|||||||
|
|
||||||
.PHONY: docker-build
|
.PHONY: docker-build
|
||||||
docker-build: ## Build docker image with the desktop-frontend.
|
docker-build: ## Build docker image with the desktop-frontend.
|
||||||
docker build -t c121914yu/doc-gpt:latest .
|
docker build -t c121914yu/fast-gpt:latest . --network host --build-arg HTTP_PROXY=http://127.0.0.1:7890 --build-arg HTTPS_PROXY=http://127.0.0.1:7890
|
||||||
|
|
||||||
##@ Deployment
|
##@ Deployment
|
||||||
|
|
||||||
.PHONY: docker-run
|
.PHONY: docker-run
|
||||||
docker-run: ## Push docker image.
|
docker-run: ## Push docker image.
|
||||||
docker run -d -p 8008:3000 --name doc-gpt -v /web_project/yjl/doc-gpt/logs:/app/.next/logs c121914yu/doc-gpt:latest
|
docker run -d -p 8008:3000 --name fast-gpt -v /web_project/yjl/fast-gpt/logs:/app/.next/logs c121914yu/fast-gpt:latest
|
||||||
|
|
||||||
#TODO: add support of docker push
|
#TODO: add support of docker push
|
||||||
|
|
||||||
|
|||||||
217
README.md
@@ -1,73 +1,202 @@
|
|||||||
# Doc GPT
|
# Fast GPT
|
||||||
|
|
||||||
## 初始化
|
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
|
||||||
|
|
||||||
|
## 开发
|
||||||
复制 .env.template 成 .env.local ,填写核心参数
|
复制 .env.template 成 .env.local ,填写核心参数
|
||||||
|
|
||||||
```
|
```
|
||||||
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
||||||
AXIOS_PROXY_PORT=代理端口
|
AXIOS_PROXY_PORT_FAST=代理端口1,clash默认为7890
|
||||||
MONGODB_UR=mongo数据库地址
|
AXIOS_PROXY_PORT_NORMAL=代理端口2
|
||||||
|
MONGODB_URI=mongo数据库地址
|
||||||
MY_MAIL=发送验证码邮箱
|
MY_MAIL=发送验证码邮箱
|
||||||
MAILE_CODE=邮箱秘钥
|
MAILE_CODE=邮箱秘钥(代理里设置的是QQ邮箱,不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件")
|
||||||
TOKEN_KEY=随便填一个,用于生成和校验 token
|
TOKEN_KEY=随便填一个,用于生成和校验 token
|
||||||
|
OPENAIKEY=openai的key
|
||||||
|
REDIS_URL=redis的地址
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
```bash
|
|
||||||
# 本地 docker 打包
|
|
||||||
docker build -t imageName .
|
|
||||||
docker push imageName
|
|
||||||
|
|
||||||
# 服务器拉取部署
|
### docker 模式
|
||||||
docker pull imageName
|
请准备好 docker, mongo,代理, 和 nginx。 镜像走本机的代理,所以用 network=host,port 改成代理的端口,clash 一般都是 7890。
|
||||||
docker stop doc-gpt || true
|
|
||||||
docker rm doc-gpt || true
|
#### docker 打包
|
||||||
# 运行时才把参数写入
|
```bash
|
||||||
docker run -d --network=host --name doc-gpt -e AXIOS_PROXY_HOST= -e AXIOS_PROXY_PORT= -e MAILE_CODE= -e TOKEN_KEY= -e MONGODB_UR= imageName
|
docker build -t imageName:tag .
|
||||||
|
docker push imageName:tag
|
||||||
|
# 或者直接拉镜像,见下方
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
#### 软件教程:docker 安装
|
||||||
|
```bash
|
||||||
|
# 安装docker
|
||||||
|
curl -sSL https://get.daocloud.io/docker | sh
|
||||||
|
sudo systemctl start docker
|
||||||
|
```
|
||||||
|
|
||||||
# 介绍页
|
#### 软件教程: clash 代理
|
||||||
|
```bash
|
||||||
|
# 下载包
|
||||||
|
curl https://glados.rocks/tools/clash-linux.zip -o clash.zip
|
||||||
|
# 解压
|
||||||
|
unzip clash.zip
|
||||||
|
# 下载终端配置⽂件(改成自己配置文件路径)
|
||||||
|
curl https://update.glados-config.com/clash/98980/8f30944/70870/glados-terminal.yaml > config.yaml
|
||||||
|
# 赋予运行权限
|
||||||
|
chmod +x ./clash-linux-amd64-v1.10.0
|
||||||
|
# 记得配置端口变量:
|
||||||
|
export ALL_PROXY=socks5://127.0.0.1:7891
|
||||||
|
export http_proxy=http://127.0.0.1:7890
|
||||||
|
export https_proxy=http://127.0.0.1:7890
|
||||||
|
export HTTP_PROXY=http://127.0.0.1:7890
|
||||||
|
export HTTPS_PROXY=http://127.0.0.1:7890
|
||||||
|
|
||||||
## 欢迎使用 Doc GPT
|
# 运行脚本: 删除clash - 到 clash 目录 - 删除缓存 - 执行运行. 会生成一个 nohup.out 文件,可以看到 clash 的 logs
|
||||||
|
OLD_PROCESS=$(pgrep clash)
|
||||||
|
if [ ! -z "$OLD_PROCESS" ]; then
|
||||||
|
echo "Killing old process: $OLD_PROCESS"
|
||||||
|
kill $OLD_PROCESS
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
cd **/clash
|
||||||
|
rm -f ./nohup.out || true
|
||||||
|
rm -f ./cache.db || true
|
||||||
|
nohup ./clash-linux-amd64-v1.10.0 -d ./ &
|
||||||
|
echo "Restart clash"
|
||||||
|
```
|
||||||
|
|
||||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
#### 文件创建
|
||||||
|
**yml文件**
|
||||||
|
```yml
|
||||||
|
version: "3.3"
|
||||||
|
services:
|
||||||
|
fast-gpt:
|
||||||
|
image: c121914yu/fast-gpt:latest
|
||||||
|
environment:
|
||||||
|
AXIOS_PROXY_HOST: 127.0.0.1
|
||||||
|
AXIOS_PROXY_PORT: 7890
|
||||||
|
MY_MAIL: 11111111@qq.com
|
||||||
|
MAILE_CODE: sdasadasfasfad
|
||||||
|
TOKEN_KEY: sssssssss
|
||||||
|
MONGODB_URI: mongodb://username:password@0.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false
|
||||||
|
OPENAIKEY: sk-afadfadfadfsd
|
||||||
|
REDIS_URL: redis://default:password@0.0.0.0:8100
|
||||||
|
network_mode: host
|
||||||
|
restart: always
|
||||||
|
container_name: fast-gpt
|
||||||
|
mongodb:
|
||||||
|
image: mongo:6.0.4
|
||||||
|
container_name: mongo
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=root
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=ROOT_1234
|
||||||
|
- MONGO_DATA_DIR=/data/db
|
||||||
|
- MONGO_LOG_DIR=/data/logs
|
||||||
|
volumes:
|
||||||
|
- /root/fastgpt/mongo/data:/data/db
|
||||||
|
- /root/fastgpt/mongo/logs:/data/logs
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine3.17
|
||||||
|
container_name: nginx
|
||||||
|
restart: always
|
||||||
|
network_mode: host
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- /root/fastgpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
redis-stack:
|
||||||
|
image: redis/redis-stack:6.2.6-v6
|
||||||
|
container_name: redis-stack
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8100:6379"
|
||||||
|
- "8101:8001"
|
||||||
|
environment:
|
||||||
|
- REDIS_ARGS=--requirepass psw1234
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- /root/fastgpt/redis/redis.conf:/redis.conf
|
||||||
|
- /root/fastgpt/redis/data:/data
|
||||||
|
```
|
||||||
|
**redis.conf**
|
||||||
|
```
|
||||||
|
## 开启aop持久化
|
||||||
|
appendonly yes
|
||||||
|
#default: 持久化文件
|
||||||
|
appendfilename "appendonly.aof"
|
||||||
|
#default: 每秒同步一次
|
||||||
|
appendfsync everysec
|
||||||
|
```
|
||||||
|
**nginx.conf**
|
||||||
|
```
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
pid /run/nginx.pid;
|
||||||
|
|
||||||
1. 使用邮箱注册账号。
|
events {
|
||||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
worker_connections 1024;
|
||||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
}
|
||||||
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
|
||||||
|
|
||||||
### 模型配置
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name test.com;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1k;
|
||||||
|
gzip_buffers 4 8k;
|
||||||
|
gzip_http_version 1.1;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types text/plain application/x-javascript text/css application/javascript application/json application/xml;
|
||||||
|
gzip_disable "MSIE [1-6]\.";
|
||||||
|
|
||||||
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 运行脚本
|
||||||
|
**redis创建索引**
|
||||||
|
```bash
|
||||||
|
FT.CREATE idx:model:data:hash ON HASH PREFIX 1 model:data: SCHEMA modelId TAG userId TAG status TAG q TEXT text TEXT vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
|
||||||
|
```
|
||||||
|
**run.sh 运行文件**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
|
echo "Docker Compose 重新拉取镜像完成!"
|
||||||
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
|
|
||||||
|
|
||||||
4. **过期时间**:生成对话框后,这个对话框多久过期。
|
# 删除本地旧镜像
|
||||||
|
images=$(docker images --format "{{.ID}} {{.Repository}}" | grep fast-gpt)
|
||||||
|
|
||||||
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
|
# 将镜像 ID 和名称放入数组中
|
||||||
|
IFS=$'\n' read -rd '' -a image_array <<<"$images"
|
||||||
|
|
||||||
### 对话框介绍
|
# 遍历数组并删除所有旧的镜像
|
||||||
|
for ((i=1; i<${#image_array[@]}; i++))
|
||||||
1. 每个对话框以 windowId 作为标识。
|
do
|
||||||
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
|
image=${image_array[$i]}
|
||||||
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
|
image_id=${image%% *}
|
||||||
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如:
|
docker rmi $image_id
|
||||||
|
done
|
||||||
* 当前网页链接:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
|
```
|
||||||
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
|
||||||
|
|
||||||
### 其他问题
|
|
||||||
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
|
|
||||||

|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{"prompt": "sealos的介绍", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
|
|
||||||
{"prompt": "sealos是什么", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
|
|
||||||
{"prompt": "sealos安装的先决条件", "completion": "sealos 是一个简单的 go 二进制文件,可以安装在大多数 Linux 操作系统中。"}
|
|
||||||
{"prompt": "sealos的CPU架构", "completion": "目前支持 amd64 和 arm64 架构。"}
|
|
||||||
@@ -6,7 +6,18 @@ const isDev = process.env.NODE_ENV === 'development';
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
compress: true
|
compress: true,
|
||||||
|
webpack(config) {
|
||||||
|
config.module.rules = config.module.rules.concat([
|
||||||
|
{
|
||||||
|
test: /\.svg$/i,
|
||||||
|
issuer: /\.[jt]sx?$/,
|
||||||
|
use: ['@svgr/webpack']
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
28
package.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "docgpt",
|
"name": "fastgpt",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -13,52 +13,64 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.17",
|
"@chakra-ui/icons": "^2.0.17",
|
||||||
"@chakra-ui/react": "^2.5.1",
|
"@chakra-ui/react": "^2.5.1",
|
||||||
"@chakra-ui/system": "^2.5.1",
|
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@next/font": "13.1.6",
|
"@next/font": "13.1.6",
|
||||||
"@reduxjs/toolkit": "^1.9.3",
|
|
||||||
"@tanstack/react-query": "^4.24.10",
|
"@tanstack/react-query": "^4.24.10",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
"axios": "^1.3.3",
|
"axios": "^1.3.3",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"eslint": "8.34.0",
|
"eventsource-parser": "^0.1.0",
|
||||||
"eslint-config-next": "13.1.6",
|
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^9.0.6",
|
"framer-motion": "^9.0.6",
|
||||||
|
"gpt-token-utils": "^1.2.0",
|
||||||
"hyperdown": "^2.4.29",
|
"hyperdown": "^2.4.29",
|
||||||
"immer": "^9.0.19",
|
"immer": "^9.0.19",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mammoth": "^1.5.1",
|
||||||
"mongoose": "^6.10.0",
|
"mongoose": "^6.10.0",
|
||||||
|
"nanoid": "^4.0.1",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.9.1",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
"openai": "^3.2.1",
|
"openai": "^3.2.1",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.43.1",
|
"react-hook-form": "^7.43.1",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"redis": "^4.6.5",
|
||||||
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
"sass": "^1.58.3",
|
"sass": "^1.58.3",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"typescript": "4.9.5",
|
"wxpay-v3": "^3.0.2",
|
||||||
"uuid": "^9.0.0",
|
|
||||||
"zustand": "^4.3.5"
|
"zustand": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@types/formidable": "^2.0.5",
|
"@types/formidable": "^2.0.5",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/node": "18.14.0",
|
"@types/node": "18.14.0",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "^6.4.7",
|
||||||
|
"@types/papaparse": "^5.3.7",
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-syntax-highlighter": "^15.5.6",
|
"@types/react-syntax-highlighter": "^15.5.6",
|
||||||
"@types/tunnel": "^0.0.3",
|
"@types/tunnel": "^0.0.3",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
|
"eslint": "8.34.0",
|
||||||
|
"eslint-config-next": "13.1.6",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.1.2",
|
"lint-staged": "^13.1.2",
|
||||||
"prettier": "^2.8.4"
|
"prettier": "^2.8.4",
|
||||||
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"./src/**/*.{ts,tsx,scss}": "npm run format"
|
"./src/**/*.{ts,tsx,scss}": "npm run format"
|
||||||
|
|||||||
3987
pnpm-lock.yaml
generated
@@ -1,21 +0,0 @@
|
|||||||
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"}
|
|
||||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"}
|
|
||||||
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"}
|
|
||||||
{"prompt":"什么是lru算法?\n\n###\n\n","completion":" lru算法是一种页面置换算法,它的全称是least recently used,即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录,当需要替换一页时,选择最近最少被访问的页面进行替换。###"}
|
|
||||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"}
|
|
||||||
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"}
|
|
||||||
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"}
|
|
||||||
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"}
|
|
||||||
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数,它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"}
|
|
||||||
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"}
|
|
||||||
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"}
|
|
||||||
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"}
|
|
||||||
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"}
|
|
||||||
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"}
|
|
||||||
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"}
|
|
||||||
{"prompt":"什么是线程?\n\n###\n\n","completion":" 线程是操作系统能够进行运算调度的最小单位,它是进程的一个执行流程,共享进程的地址空间。###"}
|
|
||||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存,而实际上它通常是被分成多个物理内存碎片。###"}
|
|
||||||
{"prompt":"什么是死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有进程都无法继续执行。###"}
|
|
||||||
{"prompt":"什么是缓存?\n\n###\n\n","completion":" 缓存是指在计算机中暂存数据的高速存储器,它可以提高计算机对数据的访问速度,避免频繁地访问较慢的主存储器。###"}
|
|
||||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是计算机中用来管理和组织文件的一种机制,它通过一系列的数据结构来描述文件和目录的组织方式,以及文件如何存储和访问。###"}
|
|
||||||
{"prompt":"什么是调度算法?\n\n###\n\n","completion":" 调度算法是指操作系统中用来决定进程或线程在cpu上执行顺序的一种算法,它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}
|
|
||||||
9
public/docs/chatProblem.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 常见问题
|
||||||
|
**内容长度**
|
||||||
|
单次最长 4000 tokens, 上下文最长 8000 tokens, 上下文超长时会被截断。
|
||||||
|
|
||||||
|
**删除和复制**
|
||||||
|
点击对话头像,可以选择复制或删除该条内容。
|
||||||
|
|
||||||
|
**代理出错**
|
||||||
|
服务器代理不稳定,可以过一会儿再尝试。
|
||||||
6
public/docs/csvSelect.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
接受一个csv文件,表格头包含 question 和 answer。question 代表问题,answer 代表答案。
|
||||||
|
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
|
||||||
|
| question | answer |
|
||||||
|
| --- | --- |
|
||||||
|
| 什么是 laf | laf 是一个云函数开发平台…… |
|
||||||
|
| 什么是 sealos | Sealos 是以 kubernetes 为内核的云操作系统发行版,可以…… |
|
||||||
40
public/docs/intro.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
## 欢迎使用 Fast GPT
|
||||||
|
|
||||||
|
[Git 仓库](https://github.com/c121914yu/FastGPT)
|
||||||
|
|
||||||
|
### 交流群/问题反馈
|
||||||
|
扫码满了,加个小号,定时拉
|
||||||
|
wx号: fastgpt123
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
1. 使用邮箱注册账号。
|
||||||
|
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
||||||
|
3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。
|
||||||
|
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||||
|
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
||||||
|
|
||||||
|
### 定制 prompt
|
||||||
|
|
||||||
|
1. 进入模型编辑页
|
||||||
|
2. 调整温度和提示词
|
||||||
|
3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。
|
||||||
|
|
||||||
|
### 知识库
|
||||||
|
|
||||||
|
1. 创建模型时选择【知识库】
|
||||||
|
2. 进入模型编辑页
|
||||||
|
3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。
|
||||||
|
4. 使用该模型对话。
|
||||||
|
|
||||||
|
注意:使用知识库模型对话时,tokens 消耗会加快。
|
||||||
|
|
||||||
|
### 价格表
|
||||||
|
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
|
||||||
|
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||||
|
| --- | --- |
|
||||||
|
| chatgpt - 对话 | 0.03 |
|
||||||
|
| 知识库 - 对话 | 0.03 |
|
||||||
|
| 知识库 - 索引 | 0.004 |
|
||||||
|
| 文件拆分 | 0.03 |
|
||||||
3
public/docs/shareHint.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
你正准备分享对话,请确保分享链接不会滥用,因为它是使用的是你的 API key。
|
||||||
|
* 分享空白对话:为该模型创建一个空白的聊天分享出去。
|
||||||
|
* 分享当前对话:会把当前聊天的内容也分享出去,但是要注意不要多个人同时用一个聊天内容。
|
||||||
3
public/docs/versionIntro.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Fast GPT V2.7
|
||||||
|
* FastGpt Api 允许你将 Fast Gpt 的部分功能通过 api 的形式,将知识库接入到自己的应用中,例如:飞书、企业微信、客服助手.
|
||||||
|
* 通过 csv 文件导入和导出你的问答对。你可以将你的 csv 文件放置在飞书文档上,以便团队共享。
|
||||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
BIN
public/icon/logo.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17" height="12" viewBox="0 0 17 12" fill="none"><g opacity="1" transform="translate(0.70001220703125 0.2001953125) rotate(0 7.5 5.5)"><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0 5) rotate(0 7.5 0.5)" d="M0,0.5L15,0.5 " /><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0 0) rotate(0 7.5 0.5)" d="M0,0.5L15,0.5 " /><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7 10) rotate(0 4 0.5)" d="M0,0.5L8,0.5 " /></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 728 B |
|
Before Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 205 KiB |
BIN
public/imgs/wx300.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/imgs/wxerweima300.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
15792
public/js/pdf.js
Normal file
64521
public/js/pdf.worker.js
vendored
Normal file
1
public/js/qrcode.min.js
vendored
Normal file
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,21 +0,0 @@
|
|||||||
{"prompt": "什么是高潜公司\n\n###\n\n", "completion": " 高潜公司是指高潜力的公司,其所处的赛道有发展潜力和前景。###"}
|
|
||||||
{"prompt": "高潜公司的优势是什么\n\n###\n\n", "completion": " 高潜公司所处的整个行业和赛道有一定的增量空间,头部vc通过严格的筛选和加持为高潜公司背书,高潜公司的创始者一般都特别优秀。加入高潜公司可以获得很多机会来锻炼自己、强化自己的认知,快速成长、展现自己,并有可能成为公司扩张后的管理层###"}
|
|
||||||
{"prompt": "高潜公司的风险是什么\n\n###\n\n", "completion": " 高潜公司的高风险问题也很明显。一旦公司失败,很多边缘人就会被淘汰掉。但宏观来讲,只要赛道是有潜力的,即便这家公司失败了,也可以跳槽到成功的公司去。###"}
|
|
||||||
{"prompt": "高潜公司的例子有哪些\n\n###\n\n", "completion": " 很多高潜公司在做最近比较火的AIGC,包括去年的元宇宙,前两年比较火的新消费、企业服务和出海之类的。###"}
|
|
||||||
{"prompt": "为什么现在加入大厂的前景不广阔?\n\n###\n\n", "completion": " 今年的大形势之下,互联网大厂纷纷开始裁员、取消普调、晋升比例严重缩水,HC也急剧减少。###"}
|
|
||||||
{"prompt": "加入高潜公司可以获得什么样的机会?\n\n###\n\n", "completion": " 加入高潜公司可以获得很多机会来锻炼自己、强化自己的认知,快速成长、展现自己,并有可能成为公司扩张后的管理层。###"}
|
|
||||||
{"prompt": "什么是高潜公司?\n\n###\n\n", "completion": " 高潜公司的定义是“高潜力的公司”,所处的行业和赛道有发展潜力和前景,头部vc为其背书,创始者一般都特别优秀。###"}
|
|
||||||
{"prompt": "加入高潜公司有哪些优点\n\n###\n\n", "completion": " 加入高潜公司能够获得锻炼自己、强化认知的机会,快速成长、展现自己,在公司扩张后成为团队的管理层。同时,所处的行业还处于上升期,发展前景广阔。###"}
|
|
||||||
{"prompt": "高潜公司存在哪些风险?\n\n###\n\n", "completion": " 高潜公司存在高风险问题,一旦公司失败,很多边缘人就会被淘汰掉。但宏观来讲,只要赛道是有潜力的,即便这家公司失败了,也可以跳槽到成功的公司去。###"}
|
|
||||||
{"prompt": "高潜公司和互联网有什么相似之处?\n\n###\n\n", "completion": " 目前来说,高潜公司的风格大部分都比较像互联网大厂,有风格,但又没有那么内卷,因为大家每天都在开拓边疆,突破和探索市场的边界。同时,互联网产业的蓬勃发展也给其他行业,包括新能源汽车、新消费等,带来了用户增长和运营的经验###"}
|
|
||||||
{"prompt": "互联网大厂的制度和分工如何?\n\n###\n\n", "completion": " 目前的互联网大厂的制度和分工非常明确,基本是按照一些标准SOP做事。同时,由于已经没有用户增量了,内卷和向上管理的问题也比较严重。###"}
|
|
||||||
{"prompt": "高潜公司对于哪些毕业生是很好的选择\n\n###\n\n", "completion": " 对于追求长期创造价值而非想找份安稳的工作的毕业生来说,高潜公司是很好的选择。###"}
|
|
||||||
{"prompt": "互联网大厂的晋升方式有哪些?\n\n###\n\n", "completion": " 互联网大厂的晋升方式有两种:老板特别喜欢你或者你的战功支撑你的竞争###"}
|
|
||||||
{"prompt": "为什么目前更多优秀的人都会加入创业公司?\n\n###\n\n", "completion": " 目前社会的大趋势是更多优秀的人都会加入创业公司,因为真正在创造价值的其实永远是创业公司###"}
|
|
||||||
{"prompt": "为什么选择高潜公司主要是因为我们迎来了什么三个繁荣?\n\n###\n\n", "completion": " 选择高潜公司主要是因为我们迎来了创新生态的三个繁荣:人才繁荣、资本繁荣和环境繁荣###"}
|
|
||||||
{"prompt": "资本繁荣是如何推动创业生态的崛起的?\n\n###\n\n", "completion": " 资本繁荣,无论中国还是美国,创业生态的崛起都是伴随着移动互联网的发展。中国这一代VC的崛起,主要是通过投资移动互联网项目所积累的战绩扩大了资金池,通过这些成本低、增长快、回报率高的项目,才有了底气去推动更多行业发展,尝试着投资toB和硬科技这类成本高、增长慢、回报率低、风险大的项目。###"}
|
|
||||||
{"prompt": "环境繁荣是指什么?\n\n###\n\n", "completion": " 环境繁荣是指互联网带动起经济发展后,各地政府也开始了对于创业进行培育,免费场地、巨额无偿补贴、各地送钱竞赛,大幅度降低了创业成本。###"}
|
|
||||||
{"prompt": "高潜公司对于追求长期创造价值的毕业生来说为什么是好的选择?\n\n###\n\n", "completion": " 因为在高潜公司,毕业生可以通过参与创业公司的发展,获得更多的机会和成长空间,而不是被固定在某个职位上。此外,高潜公司通常会注重团队建设和文化建设,能够提供更好的工作环境和发展机会。###"}
|
|
||||||
{"prompt": "如果想要寻找高潜公司,有哪些渠道可以利用?\n\n###\n\n", "completion": " 可以利用领英职场app和知潜微信小程序。领英职场app中入驻了很多高潜公司及高管,而知潜小程序现在也已经有100+高潜力创业公司入驻,都是经过筛选的、top VC投资过且赛道有前景的公司。此外,红杉和真格等VC机构自己的公众号也会经常披露投资的企业。###"}
|
|
||||||
{"prompt": "为什么现在更多优秀的人才会加入创业公司?\n\n###\n\n", "completion": " 因为在当前社会的大趋势下,创业公司成为了创新生态的重要组成部分。创业公司在创造价值和推动行业发展方面具有更大的空间和潜力,而且随着移动互联网的发展,创业公司的崛起也得到了资本和政策的支持。因此,更多优秀的人才选择加入创业公司,以实现自己的创业梦想和获得更好的发展机会。###"}
|
|
||||||
{"prompt": "高潜公司的优势在哪些方面?\n\n###\n\n", "completion": " 高潜公司在团队建设、文化建设和发展机会等方面具有优势。高潜公司通常会注重团队建设和文化建设,能够提供更好的工作环境和发展机会。同时,高潜公司的发展潜力和创新能力也很强,可以为毕业生提供更多的机会和成长空间。###"}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"}
|
|
||||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"}
|
|
||||||
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"}
|
|
||||||
{"prompt":"什么是lru算法?\n\n###\n\n","completion":" lru算法是一种页面置换算法,它的全称是least recently used,即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录,当需要替换一页时,选择最近最少被访问的页面进行替换。###"}
|
|
||||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"}
|
|
||||||
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"}
|
|
||||||
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"}
|
|
||||||
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"}
|
|
||||||
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数,它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"}
|
|
||||||
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"}
|
|
||||||
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"}
|
|
||||||
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"}
|
|
||||||
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"}
|
|
||||||
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"}
|
|
||||||
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"}
|
|
||||||
{"prompt":"什么是线程?\n\n###\n\n","completion":" 线程是操作系统能够进行运算调度的最小单位,它是进程的一个执行流程,共享进程的地址空间。###"}
|
|
||||||
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存,而实际上它通常是被分成多个物理内存碎片。###"}
|
|
||||||
{"prompt":"什么是死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有进程都无法继续执行。###"}
|
|
||||||
{"prompt":"什么是缓存?\n\n###\n\n","completion":" 缓存是指在计算机中暂存数据的高速存储器,它可以提高计算机对数据的访问速度,避免频繁地访问较慢的主存储器。###"}
|
|
||||||
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是计算机中用来管理和组织文件的一种机制,它通过一系列的数据结构来描述文件和目录的组织方式,以及文件如何存储和访问。###"}
|
|
||||||
{"prompt":"什么是调度算法?\n\n###\n\n","completion":" 调度算法是指操作系统中用来决定进程或线程在cpu上执行顺序的一种算法,它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GET, POST, DELETE } from './request';
|
import { GET, POST, DELETE } from './request';
|
||||||
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
|
import type { ChatItemType, ChatSiteItemType } from '@/types/chat';
|
||||||
|
import type { InitChatResponse } from './response/chat';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一个聊天框的ID
|
* 获取一个聊天框的ID
|
||||||
@@ -9,12 +10,8 @@ export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?mo
|
|||||||
/**
|
/**
|
||||||
* 获取初始化聊天内容
|
* 获取初始化聊天内容
|
||||||
*/
|
*/
|
||||||
export const getInitChatSiteInfo = (chatId: string, windowId: string = '') =>
|
export const getInitChatSiteInfo = (chatId: string) =>
|
||||||
GET<{
|
GET<InitChatResponse>(`/chat/init?chatId=${chatId}`);
|
||||||
windowId: string;
|
|
||||||
chatSite: ChatSiteType;
|
|
||||||
history: ChatItemType[];
|
|
||||||
}>(`/chat/init?chatId=${chatId}&windowId=${windowId}`);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送 GPT3 prompt
|
* 发送 GPT3 prompt
|
||||||
@@ -35,31 +32,13 @@ export const postGPT3SendPrompt = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预发 prompt 进行存储
|
* 存储一轮对话
|
||||||
*/
|
*/
|
||||||
export const postChatGptPrompt = ({
|
export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] }) =>
|
||||||
prompt,
|
POST('/chat/saveChat', data);
|
||||||
windowId,
|
|
||||||
chatId
|
|
||||||
}: {
|
|
||||||
prompt: ChatSiteItemType;
|
|
||||||
windowId: string;
|
|
||||||
chatId: string;
|
|
||||||
}) =>
|
|
||||||
POST<string>(`/chat/preChat`, {
|
|
||||||
windowId,
|
|
||||||
prompt: {
|
|
||||||
obj: prompt.obj,
|
|
||||||
value: prompt.value
|
|
||||||
},
|
|
||||||
chatId
|
|
||||||
});
|
|
||||||
/* 获取 Chat 的 Event 对象,进行持续通信 */
|
|
||||||
export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
|
|
||||||
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}`);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除最后一句
|
* 删除一句对话
|
||||||
*/
|
*/
|
||||||
export const delLastMessage = (windowId?: string) =>
|
export const delChatRecordByIndex = (chatId: string, index: number) =>
|
||||||
windowId ? DELETE(`/chat/delLastMessage?windowId=${windowId}`) : null;
|
DELETE(`/chat/delChatRecordByIndex?chatId=${chatId}&index=${index}`);
|
||||||
|
|||||||
1
src/api/common.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import { GET, POST, DELETE } from './request';
|
||||||
25
src/api/data.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { GET, POST, DELETE, PUT } from './request';
|
||||||
|
import { RequestPaging } from '../types/index';
|
||||||
|
import { Obj2Query } from '@/utils/tools';
|
||||||
|
import type { DataListItem } from '@/types/data';
|
||||||
|
import type { PagingData } from '../types/index';
|
||||||
|
import type { DataItemSchema } from '@/types/mongoSchema';
|
||||||
|
import type { CreateDataProps } from '@/pages/data/components/CreateDataModal';
|
||||||
|
|
||||||
|
export const getDataList = () => GET<DataListItem[]>(`/data/getDataList`);
|
||||||
|
|
||||||
|
export const postData = (data: CreateDataProps) => POST<string>(`/data/postData`, data);
|
||||||
|
|
||||||
|
export const postSplitData = (dataId: string, text: string) =>
|
||||||
|
POST(`/data/splitData`, { dataId, text });
|
||||||
|
|
||||||
|
export const updateDataName = (dataId: string, name: string) =>
|
||||||
|
PUT(`/data/putDataName?dataId=${dataId}&name=${name}`);
|
||||||
|
|
||||||
|
export const delData = (dataId: string) => DELETE(`/data/delData?dataId=${dataId}`);
|
||||||
|
|
||||||
|
type GetDataItemsProps = RequestPaging & {
|
||||||
|
dataId: string;
|
||||||
|
};
|
||||||
|
export const getDataItems = (data: GetDataItemsProps) =>
|
||||||
|
GET<PagingData<DataItemSchema>>(`/data/getDataItems?${Obj2Query(data)}`);
|
||||||
51
src/api/fetch.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { getToken } from '../utils/user';
|
||||||
|
interface StreamFetchProps {
|
||||||
|
url: string;
|
||||||
|
data: any;
|
||||||
|
onMessage: (text: string) => void;
|
||||||
|
abortSignal: AbortController;
|
||||||
|
}
|
||||||
|
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
|
||||||
|
new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: getToken() || ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
signal: abortSignal.signal
|
||||||
|
});
|
||||||
|
const reader = res.body?.getReader();
|
||||||
|
if (!reader) return;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let responseText = '';
|
||||||
|
|
||||||
|
const read = async () => {
|
||||||
|
const { done, value } = await reader?.read();
|
||||||
|
if (done) {
|
||||||
|
if (res.status === 200) {
|
||||||
|
resolve(responseText);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const parseError = JSON.parse(responseText);
|
||||||
|
reject(parseError?.message || '请求异常');
|
||||||
|
} catch (err) {
|
||||||
|
reject('请求异常');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
|
||||||
|
res.status === 200 && onMessage(text);
|
||||||
|
responseText += text;
|
||||||
|
read();
|
||||||
|
};
|
||||||
|
read();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log(err, '====');
|
||||||
|
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,28 +1,106 @@
|
|||||||
import { GET, POST, DELETE, PUT } from './request';
|
import { GET, POST, DELETE, PUT } from './request';
|
||||||
import type { ModelType } from '@/types/model';
|
import type { ModelSchema, ModelDataSchema, ModelSplitDataSchema } from '@/types/mongoSchema';
|
||||||
import { ModelUpdateParams } from '@/types/model';
|
import { ModelUpdateParams } from '@/types/model';
|
||||||
import { TrainingItemType } from '../types/training';
|
import { TrainingItemType } from '../types/training';
|
||||||
|
import { RequestPaging } from '../types/index';
|
||||||
|
import { Obj2Query } from '@/utils/tools';
|
||||||
|
|
||||||
export const getMyModels = () => GET<ModelType[]>('/model/list');
|
/**
|
||||||
|
* 获取模型列表
|
||||||
|
*/
|
||||||
|
export const getMyModels = () => GET<ModelSchema[]>('/model/list');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建一个模型
|
||||||
|
*/
|
||||||
export const postCreateModel = (data: { name: string; serviceModelName: string }) =>
|
export const postCreateModel = (data: { name: string; serviceModelName: string }) =>
|
||||||
POST<ModelType>('/model/create', data);
|
POST<ModelSchema>('/model/create', data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 删除模型
|
||||||
|
*/
|
||||||
export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`);
|
export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`);
|
||||||
|
|
||||||
export const getModelById = (id: string) => GET<ModelType>(`/model/detail?modelId=${id}`);
|
/**
|
||||||
|
* 根据 ID 获取模型
|
||||||
|
*/
|
||||||
|
export const getModelById = (id: string) => GET<ModelSchema>(`/model/detail?modelId=${id}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 更新模型
|
||||||
|
*/
|
||||||
export const putModelById = (id: string, data: ModelUpdateParams) =>
|
export const putModelById = (id: string, data: ModelUpdateParams) =>
|
||||||
PUT(`/model/update?modelId=${id}`, data);
|
PUT(`/model/update?modelId=${id}`, data);
|
||||||
|
|
||||||
export const postTrainModel = (id: string, form: FormData) =>
|
export const postTrainModel = (id: string, form: FormData) =>
|
||||||
POST(`/model/train?modelId=${id}`, form, {
|
POST(`/model/train/train?modelId=${id}`, form, {
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'multipart/form-data'
|
'content-type': 'multipart/form-data'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const putModelTrainingStatus = (id: string) => PUT(`/model/putTrainStatus?modelId=${id}`);
|
export const putModelTrainingStatus = (id: string) =>
|
||||||
|
PUT(`/model/train/putTrainStatus?modelId=${id}`);
|
||||||
|
|
||||||
export const getModelTrainings = (id: string) =>
|
export const getModelTrainings = (id: string) =>
|
||||||
GET<TrainingItemType[]>(`/model/getTrainings?modelId=${id}`);
|
GET<TrainingItemType[]>(`/model/train/getTrainings?modelId=${id}`);
|
||||||
|
|
||||||
|
/* 模型 data */
|
||||||
|
|
||||||
|
type GetModelDataListProps = RequestPaging & {
|
||||||
|
modelId: string;
|
||||||
|
searchText: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 获取模型的知识库数据
|
||||||
|
*/
|
||||||
|
export const getModelDataList = (props: GetModelDataListProps) =>
|
||||||
|
GET(`/model/data/getModelData?${Obj2Query(props)}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出数据(不分页)
|
||||||
|
*/
|
||||||
|
export const getExportDataList = (modelId: string) =>
|
||||||
|
GET<[string, string][]>(`/model/data/exportModelData?modelId=${modelId}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型正在拆分数据的数量
|
||||||
|
*/
|
||||||
|
export const getModelSplitDataListLen = (modelId: string) =>
|
||||||
|
GET<number>(`/model/data/getSplitData?modelId=${modelId}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 web 页面内容
|
||||||
|
*/
|
||||||
|
export const getWebContent = (url: string) => POST<string>(`/model/data/fetchingUrlData`, { url });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动输入数据
|
||||||
|
*/
|
||||||
|
export const postModelDataInput = (data: {
|
||||||
|
modelId: string;
|
||||||
|
data: { text: ModelDataSchema['text']; q: ModelDataSchema['q'] }[];
|
||||||
|
}) => POST<number>(`/model/data/pushModelDataInput`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拆分数据
|
||||||
|
*/
|
||||||
|
export const postModelDataSplitData = (data: { modelId: string; text: string; prompt: string }) =>
|
||||||
|
POST(`/model/data/splitData`, data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* json导入数据
|
||||||
|
*/
|
||||||
|
export const postModelDataCsvData = (modelId: string, data: string[][]) =>
|
||||||
|
POST<number>(`/model/data/pushModelDataCsv`, { modelId, data: data });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新模型数据
|
||||||
|
*/
|
||||||
|
export const putModelDataById = (data: { dataId: string; text: string; q?: string }) =>
|
||||||
|
PUT('/model/data/putModelData', data);
|
||||||
|
/**
|
||||||
|
* 删除一条模型数据
|
||||||
|
*/
|
||||||
|
export const delOneModelData = (dataId: string) =>
|
||||||
|
DELETE(`/model/data/delModelDataById?dataId=${dataId}`);
|
||||||
|
|||||||
16
src/api/openapi.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { GET, POST, DELETE } from './request';
|
||||||
|
import { UserOpenApiKey } from '@/types/openapi';
|
||||||
|
/**
|
||||||
|
* crete a api key
|
||||||
|
*/
|
||||||
|
export const createAOpenApiKey = () => POST<string>('/openapi/postKey');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get api keys
|
||||||
|
*/
|
||||||
|
export const getOpenApiKeys = () => GET<UserOpenApiKey[]>('/openapi/getKeys');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete api by id
|
||||||
|
*/
|
||||||
|
export const delOpenApiById = (id: string) => DELETE(`/openapi/delKey?id=${id}`);
|
||||||
@@ -34,10 +34,10 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) {
|
|||||||
*/
|
*/
|
||||||
function checkRes(data: ResponseDataType) {
|
function checkRes(data: ResponseDataType) {
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
console.log(data, 'data is empty');
|
console.log('error->', data, 'data is empty');
|
||||||
return Promise.reject('服务器异常');
|
return Promise.reject('服务器异常');
|
||||||
} else if (data.code < 200 || data.code >= 400) {
|
} else if (data.code < 200 || data.code >= 400) {
|
||||||
return Promise.reject(data.message);
|
return Promise.reject(data);
|
||||||
}
|
}
|
||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
@@ -46,24 +46,23 @@ function checkRes(data: ResponseDataType) {
|
|||||||
* 响应错误
|
* 响应错误
|
||||||
*/
|
*/
|
||||||
function responseError(err: any) {
|
function responseError(err: any) {
|
||||||
console.error('请求错误', err);
|
console.log('error->', '请求错误', err);
|
||||||
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
return Promise.reject('未知错误');
|
return Promise.reject({ message: '未知错误' });
|
||||||
}
|
}
|
||||||
if (typeof err === 'string') {
|
if (typeof err === 'string') {
|
||||||
return Promise.reject(err);
|
return Promise.reject({ message: err });
|
||||||
}
|
}
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
// 有报错响应
|
// 有报错响应
|
||||||
const res = err.response;
|
const res = err.response;
|
||||||
/* token过期,判断请求token与本地是否相同,若不同需要重发 */
|
|
||||||
if (res.data.code in TOKEN_ERROR_CODE) {
|
if (res.data.code in TOKEN_ERROR_CODE) {
|
||||||
clearToken();
|
clearToken();
|
||||||
return Promise.reject('token过期,重新登录');
|
return Promise.reject({ message: 'token过期,重新登录' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject('未知错误');
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 创建请求实例 */
|
/* 创建请求实例 */
|
||||||
|
|||||||
13
src/api/response/chat.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { ChatPopulate, ModelSchema } from '@/types/mongoSchema';
|
||||||
|
import type { ChatItemType } from '@/types/chat';
|
||||||
|
|
||||||
|
export type InitChatResponse = {
|
||||||
|
chatId: string;
|
||||||
|
modelId: string;
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
intro: string;
|
||||||
|
chatModel: ModelSchema.service.chatModel; // 对话模型名
|
||||||
|
modelName: ModelSchema.service.modelName; // 底层模型
|
||||||
|
history: ChatItemType[];
|
||||||
|
};
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { GET, POST, PUT } from './request';
|
import { GET, POST, PUT } from './request';
|
||||||
import { createHashPassword } from '@/utils/tools';
|
import { createHashPassword, Obj2Query } from '@/utils/tools';
|
||||||
import { ResLogin } from './response/user';
|
import { ResLogin } from './response/user';
|
||||||
import { EmailTypeEnum } from '@/constants/common';
|
import { EmailTypeEnum } from '@/constants/common';
|
||||||
import { UserType, UserUpdateParams } from '@/types/user';
|
import { UserType, UserUpdateParams } from '@/types/user';
|
||||||
|
import type { PagingData, RequestPaging } from '@/types';
|
||||||
|
import { BillSchema, PaySchema } from '@/types/mongoSchema';
|
||||||
|
import { adaptBill } from '@/utils/adapt';
|
||||||
|
|
||||||
export const sendCodeToEmail = ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) =>
|
export const sendCodeToEmail = ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) =>
|
||||||
GET('/user/sendEmail', { email, type });
|
GET('/user/sendEmail', { email, type });
|
||||||
@@ -46,3 +49,19 @@ export const postLogin = ({ email, password }: { email: string; password: string
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
|
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
|
||||||
|
|
||||||
|
export const getUserBills = (data: RequestPaging) =>
|
||||||
|
GET<PagingData<BillSchema>>(`/user/getBill?${Obj2Query(data)}`).then((res) => ({
|
||||||
|
...res,
|
||||||
|
data: res.data.map((bill) => adaptBill(bill))
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const getPayOrders = () => GET<PaySchema[]>(`/user/getPayOrders`);
|
||||||
|
|
||||||
|
export const getPayCode = (amount: number) =>
|
||||||
|
GET<{
|
||||||
|
codeUrl: string;
|
||||||
|
payId: string;
|
||||||
|
}>(`/user/getPayCode?amount=${amount}`);
|
||||||
|
|
||||||
|
export const checkPayResult = (payId: string) => GET<number>(`/user/checkPayResult?payId=${payId}`);
|
||||||
|
|||||||
1
src/components/Icon/icons/board.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1680878351566" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M896 771.413333h-768c-51.2 0-93.866667-42.666667-93.866667-93.866666V209.92c0-51.2 42.666667-93.866667 93.866667-93.866667h768c51.2 0 93.866667 42.666667 93.866667 93.866667v465.92c0 52.906667-42.666667 95.573333-93.866667 95.573333zM128 167.253333C104.106667 167.253333 85.333333 186.026667 85.333333 209.92v465.92c0 23.893333 18.773333 42.666667 42.666667 42.666667h768c23.893333 0 42.666667-18.773333 42.666667-42.666667V209.92c0-23.893333-18.773333-42.666667-42.666667-42.666667h-768z" p-id="1174"></path><path d="M512 907.946667c-13.653333 0-25.6-11.946667-25.6-25.6v-136.533334c0-13.653333 11.946667-25.6 25.6-25.6s25.6 11.946667 25.6 25.6v136.533334c0 13.653333-11.946667 25.6-25.6 25.6z" p-id="1175"></path><path d="M680.96 907.946667H343.04c-13.653333 0-25.6-11.946667-25.6-25.6s11.946667-25.6 25.6-25.6h337.92c13.653333 0 25.6 11.946667 25.6 25.6s-11.946667 25.6-25.6 25.6zM776.533333 648.533333h-529.066666c-13.653333 0-25.6-11.946667-25.6-25.6s11.946667-25.6 25.6-25.6h530.773333c13.653333 0 25.6 11.946667 25.6 25.6s-11.946667 25.6-27.306667 25.6z" p-id="1176"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/components/Icon/icons/chatSend.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805359001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1328" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M416.583186 1022.194004c-5.417989 0-10.835979-1.203998-16.253968-3.611993-15.049971-6.621987-24.681952-21.069959-24.681952-37.323927l0-299.795414c0-12.641975 5.417989-24.079953 15.651969-31.905938 9.631981-7.825985 22.273956-10.23398 34.915932-7.825985l417.787184 99.931805 84.279835-599.590829c1.203998-9.631981-8.427984-16.253968-16.855967-11.437978L147.489712 573.102881l139.061728 35.517931c19.865961 4.815991 34.313933 22.875955 32.507937 43.343915-2.407995 25.885949-26.487948 42.139918-50.567901 36.119929L30.70194 627.282775c-16.253968-4.213992-27.691946-17.457966-30.099941-33.711934-2.407995-16.253968 5.417989-32.507937 19.865961-40.93592L962.59612 6.621987c13.243974-7.825985 30.099941-7.223986 43.343915 1.203998 12.641975 8.427984 19.865961 24.079953 17.457966 39.129924l-105.349794 750.090535c-1.805996 11.437978-7.825985 21.671958-17.457966 28.293945-9.631981 6.621987-21.069959 8.427984-32.507937 6.019988l-411.165197-98.125808 0 154.111699 81.87184-76.453851c15.049971-13.845973 37.925926-16.855967 54.179894-4.213992 20.46796 15.651969 21.069959 45.149912 3.009994 62.005879L444.275132 1011.358025C436.449148 1018.582011 426.817166 1022.194004 416.583186 1022.194004L416.583186 1022.194004z" p-id="1329"></path><path d="M416.583186 722.398589c-9.631981 0-19.263962-3.611993-27.089947-10.23398-16.855967-15.049971-18.059965-40.93592-3.009994-57.791887l216.117578-242.003527c15.049971-16.855967 40.93592-18.059965 57.791887-3.009994 16.855967 15.049971 18.059965 40.93592 3.009994 57.791887l-216.117578 242.003527C438.857143 718.184597 427.419165 722.398589 416.583186 722.398589L416.583186 722.398589z" p-id="1330"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
1
src/components/Icon/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805221456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M267.3 834.6h-96.5c-27.4 0-49.7-22.3-49.7-49.7V115.2c0-27.4 22.3-49.7 49.7-49.7H727c27.4 0 49.7 22.3 49.7 49.7v96.5h-42.6v-96.5c0-3.9-3.2-7.1-7.1-7.1H170.8c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h96.5v42.6z" p-id="1174"></path><path d="M851.9 959.5H295.7c-27.4 0-49.7-22.3-49.7-49.7V240.1c0-27.4 22.3-49.7 49.7-49.7h556.2c27.4 0 49.7 22.3 49.7 49.7v669.7c-0.1 27.4-22.3 49.7-49.7 49.7zM295.7 233c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h556.2c3.9 0 7.1-3.2 7.1-7.1V240.1c0-3.9-3.2-7.1-7.1-7.1H295.7z" p-id="1175"></path></svg>
|
||||||
|
After Width: | Height: | Size: 878 B |
1
src/components/Icon/icons/develop.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1680878410563" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2745" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M256 512l81.6 108.8a32 32 0 0 1-51.2 38.4l-96-128a31.968 31.968 0 0 1 0-38.4l96-128a32 32 0 0 1 51.2 38.4L256 512zM670.4 620.8a32 32 0 0 0 51.2 38.4l96-128a31.968 31.968 0 0 0 0-38.4l-96-128a32 32 0 0 0-51.2 38.4L752 512l-81.6 108.8zM503.232 646.944a32 32 0 1 1-62.464-13.888l64-288a32 32 0 1 1 62.464 13.888l-64 288z" p-id="2746"></path><path d="M160 144a32 32 0 0 0-32 32V864a32 32 0 0 0 32 32h688a32 32 0 0 0 32-32V176a32 32 0 0 0-32-32H160z m0-64h688a96 96 0 0 1 96 96V864a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V176a96 96 0 0 1 96-96z" p-id="2747"></path></svg>
|
||||||
|
After Width: | Height: | Size: 897 B |
1
src/components/Icon/icons/home.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679114254212" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2776" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M923.733333 394.666667c-85.333333-70.4-206.933333-174.933333-362.666666-309.333334C533.333333 61.866667 490.666667 61.866667 462.933333 85.333333c-155.733333 134.4-277.333333 238.933333-362.666666 309.333334-14.933333 14.933333-25.6 34.133333-25.6 53.333333 0 38.4 32 70.4 70.4 70.4H192v358.4c0 29.866667 23.466667 53.333333 53.333333 53.333333H405.333333c29.866667 0 53.333333-23.466667 53.333334-53.333333v-206.933333h106.666666v206.933333c0 29.866667 23.466667 53.333333 53.333334 53.333333h160c29.866667 0 53.333333-23.466667 53.333333-53.333333V518.4h46.933333c38.4 0 70.4-32 70.4-70.4 0-21.333333-10.666667-40.533333-25.6-53.333333z m-44.8 59.733333h-57.6c-29.866667 0-53.333333 23.466667-53.333333 53.333333v358.4h-138.666667V661.333333c0-29.866667-23.466667-53.333333-53.333333-53.333333h-128c-29.866667 0-53.333333 23.466667-53.333333 53.333333v206.933334H256V507.733333c0-29.866667-23.466667-53.333333-53.333333-53.333333H145.066667c-4.266667 0-6.4-2.133333-6.4-6.4 0-2.133333 2.133333-4.266667 2.133333-6.4 85.333333-70.4 206.933333-174.933333 362.666667-309.333333 4.266667-4.266667 10.666667-4.266667 14.933333 0 155.733333 134.4 277.333333 238.933333 362.666667 309.333333 2.133333 2.133333 2.133333 2.133333 2.133333 4.266667 2.133333 6.4-2.133333 8.533333-4.266667 8.533333z" p-id="2777"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/components/Icon/icons/menu.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679316084227" class="icon" viewBox="0 0 1305 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="61.171875" height="48"><path d="M0.837818 75.218317c0 19.642164 8.098902 39.191237 21.969435 53.06177 13.963624 13.963624 33.512697 22.062525 53.247951 22.062525a76.055204 76.055204 0 0 0 53.06177-21.969434c13.963624-13.963624 22.062525-33.512697 22.062526-53.154861A76.055204 76.055204 0 0 0 129.303156 21.970365 76.055204 76.055204 0 0 0 76.055204 0.000931a76.055204 76.055204 0 0 0-53.247951 21.969434A76.706839 76.706839 0 0 0 0.837818 75.218317M0.837818 476.160498c0 19.642164 8.005811 39.377419 21.969435 53.247952 13.963624 13.963624 33.419606 21.969435 53.247951 21.969434a76.241385 76.241385 0 0 0 53.154861-21.969434 75.962113 75.962113 0 0 0 21.969435-53.247952 76.241385 76.241385 0 0 0-21.969435-53.154861 75.962113 75.962113 0 0 0-53.154861-21.969434 76.241385 76.241385 0 0 0-53.247951 21.969434 75.962113 75.962113 0 0 0-21.969435 53.154861M0.837818 877.19577c0 19.642164 8.005811 39.284328 21.969435 53.247951 13.963624 13.963624 33.419606 21.969435 53.247951 21.969435a76.241385 76.241385 0 0 0 53.154861-21.969435 75.962113 75.962113 0 0 0 21.969435-53.247951 76.241385 76.241385 0 0 0-21.969435-53.247952 75.962113 75.962113 0 0 0-53.154861-21.969434 76.241385 76.241385 0 0 0-53.247951 21.969434 76.520658 76.520658 0 0 0-21.969435 53.247952M1304.109361 75.218317c0 41.518508-32.395607 75.124295-72.331571 75.124295H373.945843c-40.029055 0-72.331571-33.512697-72.331571-75.124295C301.521181 33.513628 333.916788 0.000931 373.945843 0.000931h857.831947c40.029055 0 72.331571 33.605788 72.331571 75.217386M1231.77779 551.377884H373.945843c-40.029055 0-72.331571-33.605788-72.331571-75.217386 0-41.518508 32.302516-75.124295 72.331571-75.124295h857.831947c40.029055-0.186182 72.331571 33.512697 72.331571 75.124295 0 41.425417-32.395607 75.217386-72.331571 75.217386zM1304.109361 877.102679c0 41.611599-32.395607 75.310477-72.331571 75.310477H373.945843c-40.029055 0-72.331571-33.698878-72.331571-75.310477 0-41.425417 32.302516-75.124295 72.331571-75.124295h857.831947c40.029055-0.093091 72.331571 33.698878 72.331571 75.124295" p-id="1174"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
1
src/components/Icon/icons/model.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679070302676" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M910.13 243.22L545.97 32.97c-19.82-11.46-44.41-11.4-64.16 0.13L115.54 246.51c-19.5 11.36-31.68 32.43-31.76 54.99L82.1 725.44c-0.08 22.87 12.16 44.16 31.97 55.6l364.16 210.25c9.86 5.7 20.92 8.55 31.97 8.55 11.13 0 22.27-2.89 32.19-8.67l366.27-213.41c19.5-11.36 31.66-32.43 31.75-54.99l1.69-423.93c0.08-22.88-12.16-44.18-31.97-55.62zM513.68 88.9l335.28 193.58-332.93 192.2c-1.38 0.8-2.63 1.76-3.94 2.64-1.32-0.88-2.56-1.85-3.94-2.64L178.66 284.46 513.68 88.9zM146.69 725.68l1.24-384.39 327.91 189.32c1.59 0.92 2.74 2.31 3.54 3.89-0.09 1.49-0.29 2.95-0.28 4.45l0.7 175.55-0.8 202.69-332.31-191.51z m398.5 189.44l-0.8-200.61 0.7-175.54c0.01-1.5-0.2-2.97-0.28-4.46 0.8-1.59 1.95-2.98 3.53-3.9l329.03-189.96-1.23 381.29-330.95 193.18z" p-id="1174"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/components/Icon/icons/pay.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679410564438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2824" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M693.095316 281.760857l-131.632817 223.935003 103.718481 0 0 49.478312-120.846571 0 0 68.193688 120.846571 0 0 50.115659-120.846571 0 0 99.276514-62.164435 0L482.169975 673.483519 356.88022 673.483519l0-50.115659 125.289755 0 0-68.193688L356.88022 555.174172l0-49.478312 106.893053 0-130.364204-223.935003 70.099647 0c60.895822 111.230417 97.898433 181.748475 111.012698 211.562689l1.268612 0c4.441967-12.262847 16.596562-37.002611 36.474732-74.219292l74.536749-137.343396L693.095316 281.760857 693.095316 281.760857zM693.095316 281.760857" p-id="2825"></path><path d="M784.470674 621.448522c-15.061578 0-27.247797 12.187435-27.247797 27.247797s12.187435 27.247797 27.247797 27.247797l71.98128 0c-61.204765 128.843816-192.338895 217.986027-344.464118 217.986027-210.6687 0-381.478892-170.782216-381.478892-381.475243 0-210.696675 170.810191-381.465512 381.478892-381.465512 192.121175 0 350.635679 142.189179 377.137878 326.968701l55.08064 0C917.333181 242.953241 734.255278 76.493794 511.987837 76.493794 271.197197 76.493794 76.012135 271.688586 76.012135 512.456117c0 240.762665 195.185062 435.972053 435.975702 435.972053 164.236031 0 307.128238-90.894915 381.475243-225.064956l0 61.57574c0 15.061578 12.187435 27.247797 27.276989 27.247797 15.004412 0 27.247797-12.187435 27.247797-27.247797L947.987865 648.697535c0-3.297419 0-27.247797-27.247797-27.247797L784.470674 621.449738 784.470674 621.448522zM784.470674 621.448522" p-id="2826"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/components/Icon/icons/share.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679070718083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5975" id="mx_n_1679070718084" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M1023.82 694.91v146.26c0 102.38-80.44 182.82-182.82 182.82H182.83C80.45 1024 0 943.56 0 841.18V183.01C0 80.63 80.45 0.19 182.83 0.19h146.26c21.94 0 36.57 14.62 36.57 36.57 0 21.94-14.62 36.56-36.57 36.56H182.83c-58.5 0-109.7 51.19-109.7 109.7v658.17c0 58.5 51.19 109.7 109.7 109.7h658.17c58.5 0 109.7-51.19 109.7-109.7V694.91c0-21.94 14.62-36.56 36.56-36.56 21.93 0 36.56 14.63 36.56 36.56z" p-id="5976"></path><path d="M1012.6 292.61L684.73 5.86c-6.56-5.7-15.02-6.32-21.96-1.49-6.94 4.83-11.31 14.24-11.31 24.65v132.66h-80.9c-84.89 0-164.74 41.49-224.82 116.92-29.27 36.79-52.28 79.65-68.44 127.34C260.57 455.5 252.11 508.02 252.11 562.27c0 40.13 4.65 85.72 12.17 118.79 2.47 11.02 9.89 18.95 18.72 19.94h1.81c8.08 0 15.59-6.07 19.21-15.61 50.29-134.27 154.86-220.98 266.46-220.98h80.9v138.12c0 10.28 4.37 19.69 11.31 24.65 6.94 4.83 15.4 4.33 21.96-1.49l327.96-286.75c5.89-5.21 9.51-13.87 9.51-23.16-0.01-9.3-3.53-17.97-9.52-23.17z m-88.21 16.04L717.4 477.58v-81.92c0-11.07-7.41-20.08-16.52-20.08h-78.98c-49.84 0-95.79 2.5-146.79 32.25-30.31 17.68-130.92 89.06-150 121.09-0.51-7.94 20.14-85.47 23-92.41C390.11 334.54 504.6 237.8 621.9 237.8h78.98c9.1 0 16.52-9.02 16.52-20.08v-78l206.99 168.93z" p-id="5977"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/components/Icon/icons/user.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1680878383832" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1637" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M511.333 63.333c-247.424 0-448 200.576-448 448s200.576 448 448 448 448-200.576 448-448-200.576-448-448-448z m0 832c-51.868 0-102.15-10.144-149.451-30.15-36.011-15.231-69.123-35.67-98.812-60.897 12.177-31.985 42.226-63.875 84.223-88.903C396.189 686.243 456.222 669.53 512 669.53c55.631 0 115.416 16.658 164.026 45.703 41.762 24.953 71.689 56.812 83.863 88.804-29.764 25.342-62.976 45.865-99.106 61.146-47.299 20.006-97.582 30.15-149.45 30.15z m296.268-139.658c-20.493-35.937-54.353-68.855-98.747-95.381C649.75 624.979 579.839 605.53 512 605.53c-67.964 0-138.094 19.488-197.471 54.875-44.644 26.606-78.656 59.594-99.195 95.586-23.835-28.755-43.234-60.652-57.85-95.208-20.006-47.3-30.15-97.583-30.15-149.451s10.144-102.15 30.15-149.451c19.337-45.719 47.034-86.792 82.321-122.078 35.286-35.287 76.359-62.983 122.078-82.321 47.3-20.006 97.583-30.15 149.451-30.15 51.868 0 102.15 10.144 149.451 30.15 45.719 19.337 86.792 47.034 122.078 82.321 35.287 35.286 62.983 76.359 82.321 122.078 20.006 47.3 30.15 97.583 30.15 149.451s-10.144 102.15-30.15 149.451c-14.563 34.429-33.869 66.22-57.583 94.892z" p-id="1638"></path><path d="M512 220.223c-88.224 0-160 71.776-160 160s71.776 160 160 160c88.225 0 160-71.775 160-160s-71.775-160-160-160z m0 256c-52.935 0-96-43.065-96-96s43.065-96 96-96 96 43.065 96 96-43.065 96-96 96z" p-id="1639"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,23 +1,27 @@
|
|||||||
type TIconfont = {
|
import React from 'react';
|
||||||
name: string;
|
import type { IconProps } from '@chakra-ui/react';
|
||||||
color?: string;
|
import { Icon } from '@chakra-ui/react';
|
||||||
width?: number | string;
|
|
||||||
height?: number | string;
|
const map = {
|
||||||
className?: string;
|
model: require('./icons/model.svg').default,
|
||||||
|
share: require('./icons/share.svg').default,
|
||||||
|
home: require('./icons/home.svg').default,
|
||||||
|
menu: require('./icons/menu.svg').default,
|
||||||
|
pay: require('./icons/pay.svg').default,
|
||||||
|
copy: require('./icons/copy.svg').default,
|
||||||
|
chatSend: require('./icons/chatSend.svg').default,
|
||||||
|
board: require('./icons/board.svg').default,
|
||||||
|
develop: require('./icons/develop.svg').default,
|
||||||
|
user: require('./icons/user.svg').default,
|
||||||
|
chatting: require('./icons/chatting.svg').default
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) {
|
export type IconName = keyof typeof map;
|
||||||
const style = {
|
|
||||||
fill: color,
|
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
|
||||||
width,
|
return map[name] ? (
|
||||||
height
|
<Icon as={map[name]} w={w} h={h} boxSizing={'content-box'} verticalAlign={'top'} {...props} />
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
export default MyIcon;
|
||||||
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
|
|
||||||
<use xlinkHref={`#${name}`}></use>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Icon;
|
|
||||||
|
|||||||
23
src/components/Iconfont/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
type TIconfont = {
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
width?: number | string;
|
||||||
|
height?: number | string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Iconfont({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) {
|
||||||
|
const style = {
|
||||||
|
fill: color,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
|
||||||
|
<use xlinkHref={`#${name}`}></use>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Iconfont;
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { getTokenLogin } from '@/api/user';
|
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
const unAuthPage: { [key: string]: boolean } = {
|
const unAuthPage: { [key: string]: boolean } = {
|
||||||
'/login': true,
|
'/': true,
|
||||||
'/chat': true
|
'/login': true
|
||||||
};
|
};
|
||||||
|
|
||||||
const Auth = ({ children }: { children: JSX.Element }) => {
|
const Auth = ({ children }: { children: JSX.Element }) => {
|
||||||
@@ -18,27 +17,22 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
position: 'top',
|
position: 'top',
|
||||||
status: 'warning'
|
status: 'warning'
|
||||||
});
|
});
|
||||||
const { userInfo, setUserInfo } = useUserStore();
|
const { userInfo, initUserInfo } = useUserStore();
|
||||||
const { setLoading } = useGlobalStore();
|
const { setLoading } = useGlobalStore();
|
||||||
|
|
||||||
useQuery(
|
useQuery(
|
||||||
[router.pathname, userInfo],
|
[router.pathname, userInfo],
|
||||||
() => {
|
() => {
|
||||||
setLoading(true);
|
|
||||||
if (unAuthPage[router.pathname] === true || userInfo) {
|
if (unAuthPage[router.pathname] === true || userInfo) {
|
||||||
return setLoading(false);
|
return setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
return getTokenLogin();
|
setLoading(true);
|
||||||
|
return initUserInfo();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(user) {
|
|
||||||
if (user) {
|
|
||||||
setUserInfo(user);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.log(error);
|
console.log('error->', error);
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
toast();
|
toast();
|
||||||
},
|
},
|
||||||
@@ -48,7 +42,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return userInfo || unAuthPage[router.pathname] === true ? <>{children}</> : null;
|
return userInfo || unAuthPage[router.pathname] === true ? children : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Auth;
|
export default Auth;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Box } from '@chakra-ui/react';
|
import { Box, useColorMode, Flex } from '@chakra-ui/react';
|
||||||
import Navbar from './navbar';
|
import Navbar from './navbar';
|
||||||
import NavbarPhone from './navbarPhone';
|
import NavbarPhone from './navbarPhone';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -16,70 +16,67 @@ const unShowLayoutRoute: { [key: string]: boolean } = {
|
|||||||
const navbarList = [
|
const navbarList = [
|
||||||
{
|
{
|
||||||
label: '介绍',
|
label: '介绍',
|
||||||
icon: 'icon-gongzuotai-01',
|
icon: 'board',
|
||||||
link: '/',
|
link: '/',
|
||||||
activeLink: ['/']
|
activeLink: ['/']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '模型',
|
label: '模型',
|
||||||
icon: 'icon-moxing',
|
icon: 'model',
|
||||||
link: '/model/list',
|
link: '/model/list',
|
||||||
activeLink: ['/model/list', '/model/detail']
|
activeLink: ['/model/list', '/model/detail']
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// label: '数据',
|
|
||||||
// icon: 'icon-datafull',
|
|
||||||
// link: '/training/dataList',
|
|
||||||
// activeLink: ['/training/dataList']
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
label: '账号',
|
label: '账号',
|
||||||
icon: 'icon-yonghu-yuan',
|
icon: 'user',
|
||||||
link: '/number/setting',
|
link: '/number/setting',
|
||||||
activeLink: ['/number/setting']
|
activeLink: ['/number/setting']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '开发',
|
||||||
|
icon: 'develop',
|
||||||
|
link: '/openapi',
|
||||||
|
activeLink: ['/openapi']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const Layout = ({ children }: { children: JSX.Element }) => {
|
const Layout = ({ children }: { children: JSX.Element }) => {
|
||||||
const { isPc } = useScreen();
|
const { isPc } = useScreen();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { Loading } = useLoading({
|
const { colorMode, setColorMode } = useColorMode();
|
||||||
defaultLoading: true
|
const { Loading } = useLoading({ defaultLoading: true });
|
||||||
});
|
|
||||||
const { loading } = useGlobalStore();
|
const { loading } = useGlobalStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (colorMode === 'dark' && router.pathname !== '/chat') {
|
||||||
|
setColorMode('light');
|
||||||
|
}
|
||||||
|
}, [colorMode, router.pathname, setColorMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!unShowLayoutRoute[router.pathname] ? (
|
{!unShowLayoutRoute[router.pathname] ? (
|
||||||
<Box minHeight={'100vh'} backgroundColor={'gray.100'}>
|
<Box h={'100%'} backgroundColor={'gray.100'} overflow={'auto'}>
|
||||||
{isPc ? (
|
{isPc ? (
|
||||||
<>
|
<>
|
||||||
<Box h={'100vh'} position={'fixed'} left={0} top={0} w={'80px'}>
|
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'80px'}>
|
||||||
<Navbar navbarList={navbarList} />
|
<Navbar navbarList={navbarList} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box ml={'80px'} p={7}>
|
<Box h={'100%'} ml={'80px'}>
|
||||||
<Box maxW={'1100px'} m={'auto'}>
|
<Box h={'100%'} py={7} px={'5vw'} m={'auto'} overflowY={'auto'}>
|
||||||
<Auth>{children}</Auth>
|
<Auth>{children}</Auth>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Box pt={'60px'}>
|
<Flex h={'100%'} flexDirection={'column'}>
|
||||||
<Box
|
<Box h={'60px'} borderBottom={'1px solid rgba(0,0,0,0.1)'}>
|
||||||
h={'60px'}
|
|
||||||
position={'fixed'}
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
right={0}
|
|
||||||
zIndex={100}
|
|
||||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
|
||||||
>
|
|
||||||
<NavbarPhone navbarList={navbarList} />
|
<NavbarPhone navbarList={navbarList} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box py={3} px={4}>
|
<Box flex={'1 0 0'} h={0} py={3} px={4} overflowY={'auto'}>
|
||||||
<Auth>{children}</Auth>
|
<Auth>{children}</Auth>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import React from 'react';
|
|||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Icon from '../Icon';
|
import MyIcon from '../Icon';
|
||||||
import styles from './style.module.scss';
|
|
||||||
|
|
||||||
export enum NavbarTypeEnum {
|
export enum NavbarTypeEnum {
|
||||||
normal = 'normal',
|
normal = 'normal',
|
||||||
small = 'small'
|
small = 'small'
|
||||||
@@ -35,7 +33,7 @@ const Navbar = ({
|
|||||||
>
|
>
|
||||||
{/* logo */}
|
{/* logo */}
|
||||||
<Box pb={4}>
|
<Box pb={4}>
|
||||||
<Image src={'/logo.svg'} width={50} height={100} alt=""></Image>
|
<Image src={'/icon/logo.png'} width={'35'} height={'35'} alt=""></Image>
|
||||||
</Box>
|
</Box>
|
||||||
{/* 导航列表 */}
|
{/* 导航列表 */}
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
@@ -46,11 +44,12 @@ const Navbar = ({
|
|||||||
flexDirection={'column'}
|
flexDirection={'column'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
justifyContent={'center'}
|
justifyContent={'center'}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
|
if (item.link === router.pathname) return;
|
||||||
router.push(item.link, undefined, {
|
router.push(item.link, undefined, {
|
||||||
shallow: true
|
shallow: true
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
fontSize={'sm'}
|
fontSize={'sm'}
|
||||||
w={'60px'}
|
w={'60px'}
|
||||||
@@ -66,20 +65,16 @@ const Navbar = ({
|
|||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Icon
|
<MyIcon
|
||||||
name={item.icon}
|
name={item.icon as any}
|
||||||
width={24}
|
width={'24px'}
|
||||||
height={24}
|
height={'24px'}
|
||||||
color={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
fill={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
||||||
/>
|
/>
|
||||||
<Box mt={1}>{item.label}</Box>
|
<Box mt={1}>{item.label}</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
{/* 通知 icon */}
|
|
||||||
{/* <Flex className={styles.informIcon} mb={5} justifyContent={'center'}>
|
|
||||||
<Icon name={'icon-tongzhi'} width={28} height={28} color={'#718096'}></Icon>
|
|
||||||
</Flex> */}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Icon from '../Icon';
|
import MyIcon from '../Icon';
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -39,30 +39,29 @@ const NavbarPhone = ({
|
|||||||
px={7}
|
px={7}
|
||||||
>
|
>
|
||||||
<Box onClick={onOpen}>
|
<Box onClick={onOpen}>
|
||||||
<Icon name="icon-caidan" width={20} height={20}></Icon>
|
<MyIcon name="menu" width={'20px'} height={'20px'} color={'blackAlpha.600'}></MyIcon>
|
||||||
</Box>
|
</Box>
|
||||||
{/* <Icon name="icon-tongzhi" width={20} height={20}></Icon> */}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
|
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent maxWidth={'60vw'}>
|
<DrawerContent maxWidth={'50vw'}>
|
||||||
<DrawerBody p={4}>
|
<DrawerBody p={4}>
|
||||||
<Box pb={4}>
|
<Box py={4}>
|
||||||
<Image src={'/logo.svg'} w={'100%'} h={'70px'} pt={2} alt=""></Image>
|
<Image src={'/icon/logo.png'} margin={'auto'} w={'35'} h={'35'} alt=""></Image>
|
||||||
</Box>
|
</Box>
|
||||||
{navbarList.map((item) => (
|
{navbarList.map((item) => (
|
||||||
<Flex
|
<Flex
|
||||||
key={item.label}
|
key={item.label}
|
||||||
mb={4}
|
mb={5}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
justifyContent={'center'}
|
justifyContent={'center'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (item.link === router.pathname) return;
|
||||||
router.push(item.link);
|
router.push(item.link);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
fontSize={'sm'}
|
h={'60px'}
|
||||||
h={'65px'}
|
|
||||||
borderRadius={'md'}
|
borderRadius={'md'}
|
||||||
{...(item.activeLink.includes(router.pathname)
|
{...(item.activeLink.includes(router.pathname)
|
||||||
? {
|
? {
|
||||||
@@ -74,11 +73,11 @@ const NavbarPhone = ({
|
|||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Icon
|
<MyIcon
|
||||||
name={item.icon}
|
name={item.icon as any}
|
||||||
width={24}
|
width={'24px'}
|
||||||
height={24}
|
height={'24px'}
|
||||||
color={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
fill={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
|
||||||
/>
|
/>
|
||||||
<Box ml={5}>{item.label}</Box>
|
<Box ml={5}>{item.label}</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import React from 'react';
|
|||||||
export const codeLight: { [key: string]: React.CSSProperties } = {
|
export const codeLight: { [key: string]: React.CSSProperties } = {
|
||||||
'code[class*=language-]': {
|
'code[class*=language-]': {
|
||||||
color: '#d4d4d4',
|
color: '#d4d4d4',
|
||||||
fontSize: '13px',
|
|
||||||
textShadow: 'none',
|
textShadow: 'none',
|
||||||
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
|
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
whiteSpace: 'pre',
|
whiteSpace: 'pre',
|
||||||
@@ -21,9 +19,7 @@ export const codeLight: { [key: string]: React.CSSProperties } = {
|
|||||||
},
|
},
|
||||||
'pre[class*=language-]': {
|
'pre[class*=language-]': {
|
||||||
color: '#d4d4d4',
|
color: '#d4d4d4',
|
||||||
fontSize: '13px',
|
|
||||||
textShadow: 'none',
|
textShadow: 'none',
|
||||||
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
|
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
whiteSpace: 'pre',
|
whiteSpace: 'pre',
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
width: 4px;
|
width: 4px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
transform: translate(4px, 2px) scaleY(1.3);
|
transform: translate(4px, 2px) scaleY(1.3);
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background-color: var(--chakra-colors-chakra-body-text);
|
||||||
animation: blink 0.6s infinite;
|
animation: blink 0.6s infinite;
|
||||||
}
|
}
|
||||||
.animation {
|
.animation {
|
||||||
:last-child::after {
|
> :last-child::after {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
content: '';
|
content: '';
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
transform: translate(4px, 2px) scaleY(1.3);
|
transform: translate(4px, 2px) scaleY(1.3);
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background-color: var(--chakra-colors-chakra-body-text);
|
||||||
animation: blink 0.6s infinite;
|
animation: blink 0.6s infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,96 +27,389 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.markdown {
|
|
||||||
/* 标题样式 */
|
|
||||||
h1 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
.markdown > *:first-child {
|
||||||
font-size: 1.6rem;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
.markdown > *:last-child {
|
||||||
h3 {
|
margin-bottom: 0 !important;
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
}
|
||||||
|
.markdown a.absent {
|
||||||
h4 {
|
color: #cc0000;
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
}
|
||||||
|
.markdown a.anchor {
|
||||||
h5 {
|
bottom: 0;
|
||||||
font-size: 1rem;
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
left: 0;
|
||||||
|
margin-left: -30px;
|
||||||
|
padding-left: 30px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
.markdown h1,
|
||||||
h6 {
|
.markdown h2,
|
||||||
font-size: 0.83rem;
|
.markdown h3,
|
||||||
|
.markdown h4,
|
||||||
|
.markdown h5,
|
||||||
|
.markdown h6 {
|
||||||
|
cursor: text;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.markdown h1 .mini-icon-link,
|
||||||
/* 列表样式 */
|
.markdown h2 .mini-icon-link,
|
||||||
ol,
|
.markdown h3 .mini-icon-link,
|
||||||
ul {
|
.markdown h4 .mini-icon-link,
|
||||||
padding-left: 1.5rem;
|
.markdown h5 .mini-icon-link,
|
||||||
margin-left: 1rem;
|
.markdown h6 .mini-icon-link {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
ul {
|
.markdown h1:hover a.anchor,
|
||||||
list-style: inside;
|
.markdown h2:hover a.anchor,
|
||||||
}
|
.markdown h3:hover a.anchor,
|
||||||
ol {
|
.markdown h4:hover a.anchor,
|
||||||
list-style: decimal;
|
.markdown h5:hover a.anchor,
|
||||||
}
|
.markdown h6:hover a.anchor {
|
||||||
|
line-height: 1;
|
||||||
/* 链接样式 */
|
margin-left: -22px;
|
||||||
a {
|
padding-left: 0;
|
||||||
color: #0077cc;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 1px solid #0077cc;
|
top: 15%;
|
||||||
}
|
}
|
||||||
|
.markdown h1:hover a.anchor .mini-icon-link,
|
||||||
a:hover {
|
.markdown h2:hover a.anchor .mini-icon-link,
|
||||||
color: #005580;
|
.markdown h3:hover a.anchor .mini-icon-link,
|
||||||
border-bottom-color: #005580;
|
.markdown h4:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h5:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h6:hover a.anchor .mini-icon-link {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.markdown h1 tt,
|
||||||
/* 图片样式 */
|
.markdown h1 code,
|
||||||
img {
|
.markdown h2 tt,
|
||||||
max-width: 100%;
|
.markdown h2 code,
|
||||||
max-height: 200px;
|
.markdown h3 tt,
|
||||||
margin: auto;
|
.markdown h3 code,
|
||||||
|
.markdown h4 tt,
|
||||||
|
.markdown h4 code,
|
||||||
|
.markdown h5 tt,
|
||||||
|
.markdown h5 code,
|
||||||
|
.markdown h6 tt,
|
||||||
|
.markdown h6 code {
|
||||||
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
.markdown h1 {
|
||||||
/* 强调样式 */
|
font-size: 28px;
|
||||||
em,
|
}
|
||||||
i {
|
.markdown h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.markdown h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.markdown h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.markdown h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.markdown h6 {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.markdown p,
|
||||||
|
.markdown blockquote,
|
||||||
|
.markdown ul,
|
||||||
|
.markdown ol,
|
||||||
|
.markdown dl,
|
||||||
|
.markdown table,
|
||||||
|
.markdown pre {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.markdown > h2:first-child,
|
||||||
|
.markdown > h1:first-child,
|
||||||
|
.markdown > h1:first-child + h2,
|
||||||
|
.markdown > h3:first-child,
|
||||||
|
.markdown > h4:first-child,
|
||||||
|
.markdown > h5:first-child,
|
||||||
|
.markdown > h6:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.markdown a:first-child h1,
|
||||||
|
.markdown a:first-child h2,
|
||||||
|
.markdown a:first-child h3,
|
||||||
|
.markdown a:first-child h4,
|
||||||
|
.markdown a:first-child h5,
|
||||||
|
.markdown a:first-child h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.markdown h1 + p,
|
||||||
|
.markdown h2 + p,
|
||||||
|
.markdown h3 + p,
|
||||||
|
.markdown h4 + p,
|
||||||
|
.markdown h5 + p,
|
||||||
|
.markdown h6 + p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown li p.first {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.markdown ul,
|
||||||
|
.markdown ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
.markdown ul.no-list,
|
||||||
|
.markdown ol.no-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown ul li > *:first-child,
|
||||||
|
.markdown ol li > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown ul ul,
|
||||||
|
.markdown ul ol,
|
||||||
|
.markdown ol ol,
|
||||||
|
.markdown ol ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown dl {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dt {
|
||||||
|
font-size: 14px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 15px 0 5px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
.markdown dl dt:first-child {
|
||||||
strong,
|
padding: 0;
|
||||||
b {
|
}
|
||||||
|
.markdown dl dt > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dt > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dd {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
.markdown dl dd > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dd > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown blockquote {
|
||||||
|
border-left: 4px solid #dddddd;
|
||||||
|
color: #777777;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
.markdown blockquote > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown blockquote > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown table th {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.markdown table th,
|
||||||
|
.markdown table td {
|
||||||
|
padding: 6px 13px;
|
||||||
|
}
|
||||||
|
.markdown table tr {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.markdown table tr:nth-child(2n) {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.markdown img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.markdown span.frame {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.frame > span {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 7px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.markdown span.frame span img {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.markdown span.frame span span {
|
||||||
|
clear: both;
|
||||||
|
color: #333333;
|
||||||
|
display: block;
|
||||||
|
padding: 5px 0 0;
|
||||||
|
}
|
||||||
|
.markdown span.align-center {
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.align-center > span {
|
||||||
|
display: block;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.markdown span.align-center span img {
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.markdown span.align-right {
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.align-right > span {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.markdown span.align-right span img {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.markdown span.float-left {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin-right: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.float-left span {
|
||||||
|
margin: 13px 0 0;
|
||||||
|
}
|
||||||
|
.markdown span.float-right {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
margin-left: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.float-right > span {
|
||||||
|
display: block;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.markdown code,
|
||||||
|
.markdown tt {
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
border-radius: 3px 3px 3px 3px;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.markdown pre > code {
|
||||||
|
background: none repeat scroll 0 0 transparent;
|
||||||
|
border: medium none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.markdown .highlight pre,
|
||||||
|
.markdown pre {
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-radius: 3px 3px 3px 3px;
|
||||||
|
font-size: max(0.9em, 14px);
|
||||||
|
line-height: 19px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
.markdown pre code,
|
||||||
|
.markdown pre tt {
|
||||||
|
background-color: transparent;
|
||||||
|
border: medium none;
|
||||||
|
}
|
||||||
|
.markdown {
|
||||||
|
text-align: justify;
|
||||||
|
overflow-y: hidden;
|
||||||
|
tab-size: 4;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
/* 代码样式 */
|
p {
|
||||||
code {
|
white-space: pre-line;
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 10px 15px;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
background-color: #222 !important;
|
background-color: #222 !important;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
|
||||||
|
|
||||||
pre code {
|
|
||||||
display: block;
|
|
||||||
border: none;
|
|
||||||
background-color: #222;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
pre code {
|
||||||
line-height: 1.7;
|
background-color: #222 !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--chakra-colors-blue-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0px;
|
||||||
|
color: var(--chakra-colors-gray-700);
|
||||||
|
|
||||||
|
thead tr:first-child th {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-color: #ccc;
|
||||||
|
background-color: rgba(236, 236, 241, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top-right-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-color: #ccc;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:last-child {
|
||||||
|
overflow: hidden;
|
||||||
|
td {
|
||||||
|
&:first-child {
|
||||||
|
border-bottom-left-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,69 @@
|
|||||||
import React, { useMemo, memo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import styles from './index.module.scss';
|
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { codeLight } from './codeLight';
|
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
|
||||||
import { useCopyData } from '@/utils/tools';
|
import { useCopyData } from '@/utils/tools';
|
||||||
import Icon from '@/components/Icon';
|
import Icon from '@/components/Icon';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkMath from 'remark-math';
|
||||||
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
|
||||||
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
|
import 'katex/dist/katex.min.css';
|
||||||
// const formatSource = useMemo(() => source.replace(/\n/g, '\n'), [source]);
|
import styles from './index.module.scss';
|
||||||
|
import { codeLight } from './codeLight';
|
||||||
|
|
||||||
|
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
|
||||||
|
const formatSource = useMemo(() => source, [source]);
|
||||||
const { copyData } = useCopyData();
|
const { copyData } = useCopyData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
className={`${styles.markdown} ${
|
className={`${styles.markdown} ${
|
||||||
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
|
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
|
||||||
}`}
|
}`}
|
||||||
rehypePlugins={[remarkGfm]}
|
remarkPlugins={[remarkMath]}
|
||||||
skipHtml={true}
|
rehypePlugins={[remarkGfm, rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
p: 'div',
|
|
||||||
pre: 'div',
|
pre: 'div',
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
const code = String(children).replace(/\n$/, '');
|
const code = String(children);
|
||||||
|
|
||||||
return (
|
return !inline || match ? (
|
||||||
<Box my={3} borderRadius={'md'} overflow={'hidden'}>
|
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
|
||||||
<Flex py={2} px={5} backgroundColor={'#323641'} color={'#fff'} fontSize={'sm'}>
|
<Flex
|
||||||
|
py={2}
|
||||||
|
px={5}
|
||||||
|
backgroundColor={useColorModeValue('#323641', 'gray.600')}
|
||||||
|
color={'#fff'}
|
||||||
|
fontSize={'sm'}
|
||||||
|
userSelect={'none'}
|
||||||
|
>
|
||||||
<Box flex={1}>{match?.[1]}</Box>
|
<Box flex={1}>{match?.[1]}</Box>
|
||||||
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
||||||
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
|
<Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
|
||||||
<Box ml={1}>复制代码</Box>
|
<Box ml={1}>复制代码</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
style={codeLight as any}
|
style={codeLight as any}
|
||||||
showLineNumbers
|
|
||||||
language={match?.[1]}
|
language={match?.[1]}
|
||||||
|
PreTag="pre"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{code}
|
{code}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</Box>
|
</Box>
|
||||||
|
) : (
|
||||||
|
<code className={className} {...props}>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
linkTarget="_blank"
|
||||||
>
|
>
|
||||||
{source}
|
{formatSource}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
76
src/components/ScrollData/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React, { useRef, useEffect, useMemo } from 'react';
|
||||||
|
import type { BoxProps } from '@chakra-ui/react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
|
|
||||||
|
interface Props extends BoxProps {
|
||||||
|
nextPage: () => void;
|
||||||
|
isLoadAll: boolean;
|
||||||
|
requesting: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
initRequesting?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScrollData = ({
|
||||||
|
children,
|
||||||
|
nextPage,
|
||||||
|
isLoadAll,
|
||||||
|
requesting,
|
||||||
|
initRequesting,
|
||||||
|
...props
|
||||||
|
}: Props) => {
|
||||||
|
const { Loading } = useLoading({ defaultLoading: true });
|
||||||
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
const loadText = useMemo(() => {
|
||||||
|
if (requesting) return '请求中……';
|
||||||
|
if (isLoadAll) return '已加载全部';
|
||||||
|
return '点击加载更多';
|
||||||
|
}, [isLoadAll, requesting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!elementRef.current) return;
|
||||||
|
|
||||||
|
const scrolling = throttle((e: Event) => {
|
||||||
|
const element = e.target as HTMLDivElement;
|
||||||
|
if (!element) return;
|
||||||
|
// 当前滚动位置
|
||||||
|
const scrollTop = element.scrollTop;
|
||||||
|
// 可视高度
|
||||||
|
const clientHeight = element.clientHeight;
|
||||||
|
// 内容总高度
|
||||||
|
const scrollHeight = element.scrollHeight;
|
||||||
|
// 判断是否滚动到底部
|
||||||
|
if (scrollTop + clientHeight + 100 >= scrollHeight) {
|
||||||
|
nextPage();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
elementRef.current.addEventListener('scroll', scrolling);
|
||||||
|
return () => {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
elementRef.current?.removeEventListener('scroll', scrolling);
|
||||||
|
};
|
||||||
|
}, [elementRef, nextPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box {...props} ref={elementRef} overflowY={'auto'} position={'relative'}>
|
||||||
|
{children}
|
||||||
|
<Box
|
||||||
|
mt={2}
|
||||||
|
fontSize={'xs'}
|
||||||
|
color={'blackAlpha.500'}
|
||||||
|
textAlign={'center'}
|
||||||
|
cursor={loadText === '点击加载更多' ? 'pointer' : 'default'}
|
||||||
|
onClick={() => {
|
||||||
|
if (loadText !== '点击加载更多') return;
|
||||||
|
nextPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loadText}
|
||||||
|
</Box>
|
||||||
|
{initRequesting && <Loading fixed={false} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollData;
|
||||||
82
src/components/Slider/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Slider,
|
||||||
|
SliderTrack,
|
||||||
|
SliderFilledTrack,
|
||||||
|
SliderThumb,
|
||||||
|
SliderMark,
|
||||||
|
Box
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const MySlider = ({
|
||||||
|
markList,
|
||||||
|
setVal,
|
||||||
|
activeVal,
|
||||||
|
max = 100,
|
||||||
|
min = 0,
|
||||||
|
step = 1
|
||||||
|
}: {
|
||||||
|
markList: {
|
||||||
|
label: string | number;
|
||||||
|
value: number;
|
||||||
|
}[];
|
||||||
|
activeVal?: number;
|
||||||
|
setVal: (index: number) => void;
|
||||||
|
max?: number;
|
||||||
|
min?: number;
|
||||||
|
step?: number;
|
||||||
|
}) => {
|
||||||
|
const startEndPointStyle = {
|
||||||
|
content: '""',
|
||||||
|
borderRadius: '10px',
|
||||||
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
border: '2px solid #D7DBE2',
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 1,
|
||||||
|
top: 0,
|
||||||
|
transform: 'translateY(-3px)'
|
||||||
|
};
|
||||||
|
const value = useMemo(() => {
|
||||||
|
const index = markList.findIndex((item) => item.value === activeVal);
|
||||||
|
return index > -1 ? index : 0;
|
||||||
|
}, [activeVal, markList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slider max={max} min={min} step={step} size={'lg'} value={value} onChange={setVal}>
|
||||||
|
{markList.map((item, i) => (
|
||||||
|
<SliderMark
|
||||||
|
key={item.value}
|
||||||
|
value={i}
|
||||||
|
mt={3}
|
||||||
|
fontSize={'sm'}
|
||||||
|
transform={'translateX(-50%)'}
|
||||||
|
{...(activeVal === item.value ? { color: 'blue.500', fontWeight: 'bold' } : {})}
|
||||||
|
>
|
||||||
|
<Box px={3} cursor={'pointer'}>
|
||||||
|
{item.label}
|
||||||
|
</Box>
|
||||||
|
</SliderMark>
|
||||||
|
))}
|
||||||
|
<SliderTrack
|
||||||
|
bg={'#EAEDF3'}
|
||||||
|
overflow={'visible'}
|
||||||
|
h={'4px'}
|
||||||
|
_before={{
|
||||||
|
...startEndPointStyle,
|
||||||
|
left: '-5px'
|
||||||
|
}}
|
||||||
|
_after={{
|
||||||
|
...startEndPointStyle,
|
||||||
|
right: '-5px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SliderFilledTrack />
|
||||||
|
</SliderTrack>
|
||||||
|
<SliderThumb border={'2.5px solid'} borderColor={'blue.500'}></SliderThumb>
|
||||||
|
</Slider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MySlider;
|
||||||
49
src/components/WxConcat/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
useColorModeValue
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
const WxConcat = ({ onClose }: { onClose: () => void }) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||||
|
<ModalHeader>wx交流群</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody textAlign={'center'}>
|
||||||
|
<Image
|
||||||
|
style={{ margin: 'auto' }}
|
||||||
|
src={'/imgs/wx300.jpg'}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<Box mt={2}>
|
||||||
|
微信号:
|
||||||
|
<Box as={'span'} userSelect={'all'}>
|
||||||
|
fastgpt123
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant={'outline'} onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WxConcat;
|
||||||
@@ -3,42 +3,4 @@ export enum EmailTypeEnum {
|
|||||||
findPassword = 'findPassword'
|
findPassword = 'findPassword'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const introPage = `
|
export const PRICE_SCALE = 100000;
|
||||||
## 欢迎使用 Doc GPT
|
|
||||||
|
|
||||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
|
||||||
|
|
||||||
1. 使用邮箱注册账号。
|
|
||||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
|
||||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
|
||||||
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
|
||||||
|
|
||||||
### 模型配置
|
|
||||||
|
|
||||||
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
|
|
||||||
|
|
||||||
|
|
||||||
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
|
|
||||||
|
|
||||||
|
|
||||||
3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
|
|
||||||
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
|
|
||||||
|
|
||||||
4. **过期时间**:生成对话框后,这个对话框多久过期。
|
|
||||||
|
|
||||||
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
|
|
||||||
|
|
||||||
### 对话框介绍
|
|
||||||
|
|
||||||
1. 每个对话框以 windowId 作为标识。
|
|
||||||
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
|
|
||||||
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
|
|
||||||
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如:
|
|
||||||
|
|
||||||
* 当前网页链接:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
|
|
||||||
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
|
||||||
|
|
||||||
### 其他问题
|
|
||||||
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
|
|
||||||

|
|
||||||
`;
|
|
||||||
|
|||||||
6
src/constants/data.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { DataType } from '@/types/data';
|
||||||
|
|
||||||
|
export const DataTypeTextMap: Record<DataType, string> = {
|
||||||
|
QA: '问答拆分',
|
||||||
|
abstract: '摘要总结'
|
||||||
|
};
|
||||||
@@ -1,21 +1,49 @@
|
|||||||
export enum OpenAiModelEnum {
|
import type { ServiceName, ModelDataType, ModelSchema } from '@/types/mongoSchema';
|
||||||
|
import type { RedisModelDataItemType } from '@/types/redis';
|
||||||
|
|
||||||
|
export enum ChatModelNameEnum {
|
||||||
GPT35 = 'gpt-3.5-turbo',
|
GPT35 = 'gpt-3.5-turbo',
|
||||||
GPT3 = 'text-davinci-003'
|
VECTOR_GPT = 'VECTOR_GPT',
|
||||||
|
VECTOR = 'text-embedding-ada-002'
|
||||||
}
|
}
|
||||||
export const OpenAiList = [
|
|
||||||
|
export const ChatModelNameMap = {
|
||||||
|
[ChatModelNameEnum.GPT35]: 'gpt-3.5-turbo',
|
||||||
|
[ChatModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo',
|
||||||
|
[ChatModelNameEnum.VECTOR]: 'text-embedding-ada-002'
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModelConstantsData = {
|
||||||
|
serviceCompany: `${ServiceName}`;
|
||||||
|
name: string;
|
||||||
|
model: `${ChatModelNameEnum}`;
|
||||||
|
trainName: string; // 空字符串代表不能训练
|
||||||
|
maxToken: number;
|
||||||
|
contextMaxToken: number;
|
||||||
|
maxTemperature: number;
|
||||||
|
price: number; // 多少钱 / 1token,单位: 0.00001元
|
||||||
|
};
|
||||||
|
|
||||||
|
export const modelList: ModelConstantsData[] = [
|
||||||
{
|
{
|
||||||
|
serviceCompany: 'openai',
|
||||||
name: 'chatGPT',
|
name: 'chatGPT',
|
||||||
model: OpenAiModelEnum.GPT35,
|
model: ChatModelNameEnum.GPT35,
|
||||||
trainName: 'turbo',
|
trainName: '',
|
||||||
canTraining: false,
|
maxToken: 4000,
|
||||||
maxToken: 4060
|
contextMaxToken: 7500,
|
||||||
|
maxTemperature: 1.5,
|
||||||
|
price: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'GPT3',
|
serviceCompany: 'openai',
|
||||||
model: OpenAiModelEnum.GPT3,
|
name: '知识库',
|
||||||
trainName: 'davinci',
|
model: ChatModelNameEnum.VECTOR_GPT,
|
||||||
canTraining: true,
|
trainName: 'vector',
|
||||||
maxToken: 4060
|
maxToken: 4000,
|
||||||
|
contextMaxToken: 7000,
|
||||||
|
maxTemperature: 1,
|
||||||
|
price: 3
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -51,3 +79,65 @@ export const formatModelStatus = {
|
|||||||
text: '已关闭'
|
text: '已关闭'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ModelDataStatusMap: Record<RedisModelDataItemType['status'], string> = {
|
||||||
|
ready: '训练完成',
|
||||||
|
waiting: '训练中'
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 知识库搜索时的配置 */
|
||||||
|
// 搜索方式
|
||||||
|
export enum ModelVectorSearchModeEnum {
|
||||||
|
hightSimilarity = 'hightSimilarity', // 高相似度+禁止回复
|
||||||
|
lowSimilarity = 'lowSimilarity', // 低相似度
|
||||||
|
noContext = 'noContex' // 高相似度+无上下文回复
|
||||||
|
}
|
||||||
|
export const ModelVectorSearchModeMap: Record<
|
||||||
|
`${ModelVectorSearchModeEnum}`,
|
||||||
|
{
|
||||||
|
text: string;
|
||||||
|
similarity: number;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
[ModelVectorSearchModeEnum.hightSimilarity]: {
|
||||||
|
text: '高相似度, 无匹配时拒绝回复',
|
||||||
|
similarity: 0.2
|
||||||
|
},
|
||||||
|
[ModelVectorSearchModeEnum.noContext]: {
|
||||||
|
text: '高相似度,无匹配时直接回复',
|
||||||
|
similarity: 0.2
|
||||||
|
},
|
||||||
|
[ModelVectorSearchModeEnum.lowSimilarity]: {
|
||||||
|
text: '低相似度匹配',
|
||||||
|
similarity: 0.8
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultModel: ModelSchema = {
|
||||||
|
_id: '',
|
||||||
|
userId: '',
|
||||||
|
name: '',
|
||||||
|
avatar: '',
|
||||||
|
status: ModelStatusEnum.pending,
|
||||||
|
updateTime: Date.now(),
|
||||||
|
trainingTimes: 0,
|
||||||
|
systemPrompt: '',
|
||||||
|
intro: '',
|
||||||
|
temperature: 5,
|
||||||
|
search: {
|
||||||
|
mode: ModelVectorSearchModeEnum.hightSimilarity
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
company: 'openai',
|
||||||
|
trainId: '',
|
||||||
|
chatModel: ChatModelNameEnum.GPT35,
|
||||||
|
modelName: ChatModelNameEnum.GPT35
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
domain: ['*'],
|
||||||
|
contextMaxLen: 1,
|
||||||
|
contentMaxLen: 1,
|
||||||
|
expiredTime: 9999,
|
||||||
|
maxLoadAmount: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
6
src/constants/redis.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const VecModelDataPrefix = 'model:data';
|
||||||
|
export const VecModelDataIdx = `idx:${VecModelDataPrefix}:hash`;
|
||||||
|
export enum ModelDataStatusEnum {
|
||||||
|
ready = 'ready',
|
||||||
|
waiting = 'waiting'
|
||||||
|
}
|
||||||
@@ -20,30 +20,38 @@ const Button = defineStyleConfig({
|
|||||||
baseStyle: {},
|
baseStyle: {},
|
||||||
sizes: {
|
sizes: {
|
||||||
sm: {
|
sm: {
|
||||||
fontSize: 'sm',
|
fontSize: 'xs',
|
||||||
px: 3,
|
px: 3,
|
||||||
py: 0,
|
py: 0,
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
height: '26px'
|
height: '26px',
|
||||||
|
lineHeight: '26px'
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
fontSize: 'md',
|
fontSize: 'sm',
|
||||||
px: 6,
|
px: 6,
|
||||||
py: 0,
|
py: 0,
|
||||||
height: '34px',
|
height: '34px',
|
||||||
|
lineHeight: '34px',
|
||||||
fontWeight: 'normal'
|
fontWeight: 'normal'
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
fontSize: 'lg',
|
fontSize: 'md',
|
||||||
px: 8,
|
px: 8,
|
||||||
py: 0,
|
py: 0,
|
||||||
height: '42px',
|
height: '42px',
|
||||||
|
lineHeight: '42px',
|
||||||
fontWeight: 'normal'
|
fontWeight: 'normal'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
outline: {
|
white: {
|
||||||
borderWidth: '1.5px'
|
color: '#fff',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: '1px solid #ffffff',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
@@ -58,13 +66,12 @@ export const theme = extendTheme({
|
|||||||
global: {
|
global: {
|
||||||
'html, body': {
|
'html, body': {
|
||||||
color: 'blackAlpha.800',
|
color: 'blackAlpha.800',
|
||||||
fontSize: '14px'
|
height: '100%',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
overflowY: 'hidden'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fonts: {
|
|
||||||
body: 'system-ui, sans-serif'
|
|
||||||
},
|
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
xs: '0.8rem',
|
xs: '0.8rem',
|
||||||
sm: '0.9rem',
|
sm: '0.9rem',
|
||||||
@@ -80,6 +87,16 @@ export const theme = extendTheme({
|
|||||||
'8xl': '6rem',
|
'8xl': '6rem',
|
||||||
'9xl': '8rem'
|
'9xl': '8rem'
|
||||||
},
|
},
|
||||||
|
fonts: {
|
||||||
|
body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||||
|
},
|
||||||
|
breakpoints: {
|
||||||
|
sm: '900px',
|
||||||
|
md: '1200px',
|
||||||
|
lg: '1500px',
|
||||||
|
xl: '1800',
|
||||||
|
'2xl': '2100'
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
Modal: ModalTheme,
|
Modal: ModalTheme,
|
||||||
Button
|
Button
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
|
export enum BillTypeEnum {
|
||||||
|
chat = 'chat',
|
||||||
|
splitData = 'splitData',
|
||||||
|
QA = 'QA',
|
||||||
|
abstract = 'abstract',
|
||||||
|
vector = 'vector',
|
||||||
|
return = 'return'
|
||||||
|
}
|
||||||
export enum PageTypeEnum {
|
export enum PageTypeEnum {
|
||||||
login = 'login',
|
login = 'login',
|
||||||
register = 'register',
|
register = 'register',
|
||||||
forgetPassword = 'forgetPassword'
|
forgetPassword = 'forgetPassword'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BillTypeMap: Record<`${BillTypeEnum}`, string> = {
|
||||||
|
[BillTypeEnum.chat]: '对话',
|
||||||
|
[BillTypeEnum.splitData]: 'QA拆分',
|
||||||
|
[BillTypeEnum.QA]: 'QA拆分',
|
||||||
|
[BillTypeEnum.abstract]: '摘要总结',
|
||||||
|
[BillTypeEnum.vector]: '索引生成',
|
||||||
|
[BillTypeEnum.return]: '退款'
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogBody,
|
AlertDialogBody,
|
||||||
@@ -17,12 +17,17 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
|
|||||||
const cancelCb = useRef<any>();
|
const cancelCb = useRef<any>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openConfirm: (confirm?: any, cancel?: any) => {
|
openConfirm: useCallback(
|
||||||
onOpen();
|
(confirm?: any, cancel?: any) => {
|
||||||
confirmCb.current = confirm;
|
confirmCb.current = confirm;
|
||||||
cancelCb.current = cancel;
|
cancelCb.current = cancel;
|
||||||
|
|
||||||
|
return onOpen;
|
||||||
},
|
},
|
||||||
ConfirmChild: () => (
|
[onOpen]
|
||||||
|
),
|
||||||
|
ConfirmChild: useCallback(
|
||||||
|
() => (
|
||||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
<AlertDialogOverlay>
|
<AlertDialogOverlay>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
@@ -44,7 +49,7 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
ml={3}
|
ml={4}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
typeof confirmCb.current === 'function' && confirmCb.current();
|
typeof confirmCb.current === 'function' && confirmCb.current();
|
||||||
@@ -56,6 +61,8 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialogOverlay>
|
</AlertDialogOverlay>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
),
|
||||||
|
[content, isOpen, onClose, title]
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { Spinner, Flex } from '@chakra-ui/react';
|
import { Spinner, Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
export const useLoading = (props?: { defaultLoading: boolean }) => {
|
export const useLoading = (props?: { defaultLoading: boolean }) => {
|
||||||
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
|
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
|
||||||
|
|
||||||
const Loading = ({
|
const Loading = useCallback(
|
||||||
loading,
|
({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
|
||||||
fixed = true
|
|
||||||
}: {
|
|
||||||
loading?: boolean;
|
|
||||||
fixed?: boolean;
|
|
||||||
}): JSX.Element | null => {
|
|
||||||
return isLoading || loading ? (
|
return isLoading || loading ? (
|
||||||
<Flex
|
<Flex
|
||||||
position={fixed ? 'fixed' : 'absolute'}
|
position={fixed ? 'fixed' : 'absolute'}
|
||||||
@@ -26,7 +21,9 @@ export const useLoading = (props?: { defaultLoading: boolean }) => {
|
|||||||
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
|
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
|
||||||
</Flex>
|
</Flex>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
},
|
||||||
|
[isLoading]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
15
src/hooks/useMarkdown.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const getMd = async (url: string) => {
|
||||||
|
const response = await fetch(`/docs/${url}`);
|
||||||
|
const textContent = await response.text();
|
||||||
|
return textContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMarkdown = ({ url }: { url: string }) => {
|
||||||
|
const { data = '' } = useQuery([url], () => getMd(url));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data
|
||||||
|
};
|
||||||
|
};
|
||||||
105
src/hooks/usePagination.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
|
import type { PagingData } from '../types/index';
|
||||||
|
import { IconButton, Flex, Box, Input } from '@chakra-ui/react';
|
||||||
|
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { useToast } from './useToast';
|
||||||
|
|
||||||
|
export const usePagination = <T = any,>({
|
||||||
|
api,
|
||||||
|
pageSize = 10,
|
||||||
|
params = {}
|
||||||
|
}: {
|
||||||
|
api: (data: any) => any;
|
||||||
|
pageSize?: number;
|
||||||
|
params?: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [pageNum, setPageNum] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [data, setData] = useState<T[]>([]);
|
||||||
|
const maxPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
|
||||||
|
|
||||||
|
const { mutate, isLoading } = useMutation({
|
||||||
|
mutationFn: async (num: number = pageNum) => {
|
||||||
|
try {
|
||||||
|
const res: PagingData<T> = await api({
|
||||||
|
pageNum: num,
|
||||||
|
pageSize,
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
setPageNum(num);
|
||||||
|
setTotal(res.total);
|
||||||
|
setData(res.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: error?.message || '获取数据异常',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mutate(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const Pagination = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<Flex alignItems={'center'} justifyContent={'end'}>
|
||||||
|
<IconButton
|
||||||
|
isDisabled={pageNum === 1}
|
||||||
|
icon={<ArrowBackIcon />}
|
||||||
|
aria-label={'left'}
|
||||||
|
size={'sm'}
|
||||||
|
w={'28px'}
|
||||||
|
h={'28px'}
|
||||||
|
onClick={() => mutate(pageNum - 1)}
|
||||||
|
/>
|
||||||
|
<Flex mx={2} alignItems={'center'}>
|
||||||
|
<Input
|
||||||
|
defaultValue={pageNum}
|
||||||
|
w={'50px'}
|
||||||
|
size={'xs'}
|
||||||
|
type={'number'}
|
||||||
|
min={1}
|
||||||
|
max={maxPage}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const val = +e.target.value;
|
||||||
|
if (val === pageNum) return;
|
||||||
|
if (val >= maxPage) {
|
||||||
|
mutate(maxPage);
|
||||||
|
} else if (val < 1) {
|
||||||
|
mutate(1);
|
||||||
|
} else {
|
||||||
|
mutate(+e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box mx={2}>/</Box>
|
||||||
|
{maxPage}
|
||||||
|
</Flex>
|
||||||
|
<IconButton
|
||||||
|
isDisabled={pageNum === maxPage}
|
||||||
|
icon={<ArrowForwardIcon />}
|
||||||
|
aria-label={'left'}
|
||||||
|
size={'sm'}
|
||||||
|
w={'28px'}
|
||||||
|
h={'28px'}
|
||||||
|
onClick={() => mutate(pageNum + 1)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}, [maxPage, mutate, pageNum]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
Pagination,
|
||||||
|
getData: mutate
|
||||||
|
};
|
||||||
|
};
|
||||||
82
src/hooks/usePaging.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import type { PagingData } from '../types/index';
|
||||||
|
import { useToast } from './useToast';
|
||||||
|
|
||||||
|
export const usePaging = <T = any>({
|
||||||
|
api,
|
||||||
|
pageSize = 10,
|
||||||
|
params = {}
|
||||||
|
}: {
|
||||||
|
api: (data: any) => any;
|
||||||
|
pageSize?: number;
|
||||||
|
params?: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [data, setData] = useState<T[]>([]);
|
||||||
|
const [pageNum, setPageNum] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [isLoadAll, setIsLoadAll] = useState(false);
|
||||||
|
const [requesting, setRequesting] = useState(false);
|
||||||
|
const [initRequesting, setInitRequesting] = useState(false);
|
||||||
|
|
||||||
|
const getData = useCallback(
|
||||||
|
async (num: number, init = false) => {
|
||||||
|
if (requesting) return;
|
||||||
|
if (!init && isLoadAll) return;
|
||||||
|
if (init) {
|
||||||
|
setInitRequesting(true);
|
||||||
|
}
|
||||||
|
setRequesting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: PagingData<T> = await api({
|
||||||
|
pageNum: num,
|
||||||
|
pageSize,
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
setData((state) => {
|
||||||
|
const data = init ? res.data : state.concat(res.data);
|
||||||
|
if (data.length >= res.total) {
|
||||||
|
setIsLoadAll(true);
|
||||||
|
}
|
||||||
|
setTotal(res.total);
|
||||||
|
setPageNum(num);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: error?.message || '获取数据异常',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequesting(false);
|
||||||
|
setInitRequesting(false);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[api, isLoadAll, pageSize, params, requesting, toast]
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextPage = useCallback(() => {
|
||||||
|
if (requesting || isLoadAll) return;
|
||||||
|
getData(pageNum + 1);
|
||||||
|
}, [getData, isLoadAll, pageNum, requesting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getData(1, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
data,
|
||||||
|
getData,
|
||||||
|
requesting,
|
||||||
|
isLoadAll,
|
||||||
|
nextPage,
|
||||||
|
initRequesting,
|
||||||
|
setData
|
||||||
|
};
|
||||||
|
};
|
||||||
33
src/hooks/useRequest.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
interface Props extends UseMutationOptions<any, any, any, any> {
|
||||||
|
successToast?: string;
|
||||||
|
errorToast?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...props }: Props) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const mutation = useMutation<unknown, unknown, any, unknown>({
|
||||||
|
...props,
|
||||||
|
onSuccess(res, variables: void, context: unknown) {
|
||||||
|
onSuccess?.(res, variables, context);
|
||||||
|
successToast &&
|
||||||
|
toast({
|
||||||
|
title: successToast,
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError(err: any, variables: void, context: unknown) {
|
||||||
|
onError?.(err, variables, context);
|
||||||
|
errorToast &&
|
||||||
|
toast({
|
||||||
|
title: typeof err === 'string' ? err : err?.message || errorToast,
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
};
|
||||||
@@ -11,6 +11,6 @@ export function useScreen() {
|
|||||||
isPc,
|
isPc,
|
||||||
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [isPc]),
|
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [isPc]),
|
||||||
mediaMdSm: useMemo(() => (isPc ? 'md' : 'sm'), [isPc]),
|
mediaMdSm: useMemo(() => (isPc ? 'md' : 'sm'), [isPc]),
|
||||||
media: (pc: number | string, phone: number | string) => (isPc ? pc : phone)
|
media: (pc: any, phone: any) => (isPc ? pc : phone)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/hooks/useSelectFile.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { useRef, useCallback } from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => {
|
||||||
|
const { fileType = '*', multiple = false } = props || {};
|
||||||
|
const SelectFileDom = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const File = useCallback(
|
||||||
|
({ onSelect }: { onSelect: (e: File[]) => void }) => (
|
||||||
|
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||||
|
<input
|
||||||
|
ref={SelectFileDom}
|
||||||
|
type="file"
|
||||||
|
accept={fileType}
|
||||||
|
multiple={multiple}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.target.files || e.target.files?.length === 0) return;
|
||||||
|
onSelect(Array.from(e.target.files));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
[fileType, multiple]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
SelectFileDom.current && SelectFileDom.current.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
File,
|
||||||
|
onOpen
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { sendCodeToEmail } from '@/api/user';
|
import { sendCodeToEmail } from '@/api/user';
|
||||||
import { EmailTypeEnum } from '@/constants/common';
|
import { EmailTypeEnum } from '@/constants/common';
|
||||||
import { useToast } from '@chakra-ui/react';
|
|
||||||
let timer: any;
|
let timer: any;
|
||||||
|
import { useToast } from './useToast';
|
||||||
|
|
||||||
export const useSendCode = () => {
|
export const useSendCode = () => {
|
||||||
const toast = useToast({
|
const { toast } = useToast();
|
||||||
position: 'top',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
const [codeSending, setCodeSending] = useState(false);
|
const [codeSending, setCodeSending] = useState(false);
|
||||||
const [codeCountDown, setCodeCountDown] = useState(0);
|
const [codeCountDown, setCodeCountDown] = useState(0);
|
||||||
const sendCodeText = useMemo(() => {
|
const sendCodeText = useMemo(() => {
|
||||||
@@ -43,12 +40,10 @@ export const useSendCode = () => {
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
typeof error === 'string' &&
|
|
||||||
toast({
|
toast({
|
||||||
title: error,
|
title: error.message || '发送验证码异常',
|
||||||
status: 'error',
|
status: 'error'
|
||||||
position: 'top'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setCodeSending(false);
|
setCodeSending(false);
|
||||||
|
|||||||
18
src/hooks/useTabs.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React, { useState, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useTabs = ({
|
||||||
|
tabs = []
|
||||||
|
}: {
|
||||||
|
tabs: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
setActiveTab
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
import type { AppProps, NextWebVitalsMetric } from 'next/app';
|
import { useEffect } from 'react';
|
||||||
|
import type { AppProps } from 'next/app';
|
||||||
|
import Script from 'next/script';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
|
||||||
import Layout from '@/components/Layout';
|
import Layout from '@/components/Layout';
|
||||||
import { theme } from '@/constants/theme';
|
import { theme } from '@/constants/theme';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import NProgress from 'nprogress'; //nprogress module
|
||||||
|
import Router from 'next/router';
|
||||||
|
import 'nprogress/nprogress.css';
|
||||||
import '../styles/reset.scss';
|
import '../styles/reset.scss';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
|
//Binding events.
|
||||||
|
Router.events.on('routeChangeStart', () => NProgress.start());
|
||||||
|
Router.events.on('routeChangeComplete', () => NProgress.done());
|
||||||
|
Router.events.on('routeChangeError', () => NProgress.done());
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
|
||||||
// Create a client
|
// Create a client
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -18,20 +28,34 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
// 校验是否支持 click 事件
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof document.createElement('div').click !== 'function') {
|
||||||
|
toast({
|
||||||
|
title: '你的浏览器版本过低',
|
||||||
|
status: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Doc GPT</title>
|
<title>Fast GPT</title>
|
||||||
<meta name="description" content="Generated by Doc GPT" />
|
<meta name="description" content="Generated by Fast GPT" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<script src="/iconfont.js" async></script>
|
|
||||||
</Head>
|
</Head>
|
||||||
|
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
|
||||||
|
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
|
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||||
<Layout>
|
<Layout>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,110 +1,135 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import type { ModelType } from '@/types/model';
|
|
||||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent, openaiChatFilter } from '@/service/utils/tools';
|
||||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import type { ModelSchema } from '@/types/mongoSchema';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
|
import { modelList } from '@/constants/model';
|
||||||
|
import { pushChatBill } from '@/service/events/pushBill';
|
||||||
|
import { gpt35StreamResponse } from '@/service/utils/openai';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
res.writeHead(200, {
|
let step = 0; // step=1时,表示开始了流响应
|
||||||
Connection: 'keep-alive',
|
const stream = new PassThrough();
|
||||||
'Content-Encoding': 'none',
|
stream.on('error', () => {
|
||||||
'Cache-Control': 'no-cache',
|
console.log('error: ', 'stream error');
|
||||||
'Content-Type': 'text/event-stream'
|
stream.destroy();
|
||||||
|
});
|
||||||
|
res.on('close', () => {
|
||||||
|
stream.destroy();
|
||||||
|
});
|
||||||
|
res.on('error', () => {
|
||||||
|
console.log('error: ', 'request error');
|
||||||
|
stream.destroy();
|
||||||
});
|
});
|
||||||
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!windowId || !chatId) {
|
const { chatId, prompt } = req.body as {
|
||||||
|
prompt: ChatItemType;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
if (!chatId || !prompt) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
let startTime = Date.now();
|
||||||
|
|
||||||
const { chat, userApiKey } = await authChat(chatId);
|
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||||
|
|
||||||
const model: ModelType = chat.modelId;
|
const model: ModelSchema = chat.modelId;
|
||||||
|
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||||
|
if (!modelConstantsData) {
|
||||||
|
throw new Error('模型加载异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取对话内容
|
||||||
|
const prompts = [...chat.content, prompt];
|
||||||
|
|
||||||
|
// 如果有系统提示词,自动插入
|
||||||
|
if (model.systemPrompt) {
|
||||||
|
prompts.unshift({
|
||||||
|
obj: 'SYSTEM',
|
||||||
|
value: model.systemPrompt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制在 tokens 数量,防止超出
|
||||||
|
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||||
|
|
||||||
|
// 格式化文本内容成 chatgpt 格式
|
||||||
const map = {
|
const map = {
|
||||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||||
};
|
};
|
||||||
// 读取对话内容
|
|
||||||
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
|
||||||
|
|
||||||
// 长度过滤
|
|
||||||
const maxContext = model.security.contextMaxLen;
|
|
||||||
const filterPrompts =
|
|
||||||
prompts.length > maxContext + 2
|
|
||||||
? [prompts[0], ...prompts.slice(prompts.length - maxContext)]
|
|
||||||
: prompts.slice(0, prompts.length);
|
|
||||||
|
|
||||||
// 格式化文本内容
|
|
||||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||||
(item: ChatItemType) => ({
|
(item: ChatItemType) => ({
|
||||||
role: map[item.obj],
|
role: map[item.obj],
|
||||||
content: item.value
|
content: item.value
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
// console.log(formatPrompts);
|
||||||
|
// 计算温度
|
||||||
|
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||||
|
|
||||||
// 获取 chatAPI
|
// 获取 chatAPI
|
||||||
const chatAPI = getOpenAIApi(userApiKey);
|
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||||
|
// 发出请求
|
||||||
const chatResponse = await chatAPI.createChatCompletion(
|
const chatResponse = await chatAPI.createChatCompletion(
|
||||||
{
|
{
|
||||||
model: model.service.chatModel,
|
model: model.service.chatModel,
|
||||||
temperature: 1,
|
temperature: temperature,
|
||||||
// max_tokens: model.security.contentMaxLen,
|
// max_tokens: modelConstantsData.maxToken,
|
||||||
messages: formatPrompts,
|
messages: formatPrompts,
|
||||||
stream: true
|
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||||
|
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||||
|
stream: true,
|
||||||
|
stop: ['.!?。']
|
||||||
},
|
},
|
||||||
openaiProxy
|
{
|
||||||
|
timeout: 40000,
|
||||||
|
responseType: 'stream',
|
||||||
|
httpsAgent: httpsAgent(!userApiKey)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 截取字符串内容
|
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||||
const reg = /{"content"(.*)"}/g;
|
|
||||||
// @ts-ignore
|
|
||||||
const match = chatResponse.data.match(reg);
|
|
||||||
let AIResponse = '';
|
|
||||||
if (match) {
|
|
||||||
match.forEach((item: string, i: number) => {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(item);
|
|
||||||
// 开头的换行忽略
|
|
||||||
if (i === 0 && json.content?.startsWith('\n')) return;
|
|
||||||
AIResponse += json.content;
|
|
||||||
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
|
|
||||||
content && res.write(`data: ${content}\n\n`);
|
|
||||||
} catch (err) {
|
|
||||||
err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.write(`data: [DONE]\n\n`);
|
|
||||||
|
|
||||||
// 存入库
|
step = 1;
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
|
||||||
$push: {
|
|
||||||
content: {
|
|
||||||
obj: 'AI',
|
|
||||||
value: AIResponse
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end();
|
const { responseContent } = await gpt35StreamResponse({
|
||||||
|
res,
|
||||||
|
stream,
|
||||||
|
chatResponse
|
||||||
|
});
|
||||||
|
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||||
|
|
||||||
|
// 只有使用平台的 key 才计费
|
||||||
|
pushChatBill({
|
||||||
|
isPay: !userApiKey,
|
||||||
|
modelName: model.service.modelName,
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
text: promptsContent + responseContent
|
||||||
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log(err?.response?.data || err);
|
if (step === 1) {
|
||||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
// 直接结束流
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
console.log('error,结束');
|
||||||
$pop: { content: 1 },
|
stream.destroy();
|
||||||
updateTime: Date.now()
|
} else {
|
||||||
|
res.status(500);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
});
|
});
|
||||||
|
}
|
||||||
res.end();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/pages/api/chat/delChatRecordByIndex.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { chatId, index } = req.query as { chatId: string; index: string };
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
throw new Error('无权操作');
|
||||||
|
}
|
||||||
|
if (!chatId || !index) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
// 凭证校验
|
||||||
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
|
const chatRecord = await Chat.findById(chatId);
|
||||||
|
|
||||||
|
if (!chatRecord) {
|
||||||
|
throw new Error('找不到对话');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算 index,跳过已经被删除的内容
|
||||||
|
let unDeleteIndex = +index;
|
||||||
|
let deletedIndex = 0;
|
||||||
|
for (deletedIndex = 0; deletedIndex < chatRecord.content.length; deletedIndex++) {
|
||||||
|
if (!chatRecord.content[deletedIndex].deleted) {
|
||||||
|
unDeleteIndex--;
|
||||||
|
if (unDeleteIndex < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||||
|
await Chat.updateOne(
|
||||||
|
{
|
||||||
|
_id: chatId,
|
||||||
|
userId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
[`content.${deletedIndex}.deleted`]: true,
|
||||||
|
updateTime: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jsonRes(res);
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { jsonRes } from '@/service/response';
|
|
||||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
try {
|
|
||||||
const { windowId } = req.query as { windowId: string };
|
|
||||||
|
|
||||||
if (!windowId) {
|
|
||||||
throw new Error('缺少参数');
|
|
||||||
}
|
|
||||||
|
|
||||||
await connectToDatabase();
|
|
||||||
|
|
||||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
|
||||||
$pop: { content: 1 },
|
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
jsonRes(res);
|
|
||||||
} catch (err) {
|
|
||||||
jsonRes(res, {
|
|
||||||
code: 500,
|
|
||||||
error: err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,18 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase, Model, Chat } from '@/service/mongo';
|
import { connectToDatabase, Model, Chat } from '@/service/mongo';
|
||||||
import { authToken } from '@/service/utils/tools';
|
import { authToken } from '@/service/utils/tools';
|
||||||
import { ModelType } from '@/types/model';
|
import type { ModelSchema } from '@/types/mongoSchema';
|
||||||
|
|
||||||
/* 获取我的模型 */
|
/* 获取我的模型 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
const { modelId } = req.query;
|
const { modelId } = req.query as {
|
||||||
|
modelId: string;
|
||||||
|
};
|
||||||
const { authorization } = req.headers;
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
throw new Error('无权操作');
|
throw new Error('无权生成对话');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
@@ -23,26 +25,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// 获取模型配置
|
// 校验是否为用户的模型
|
||||||
const model: ModelType | null = await Model.findOne({
|
const model = await Model.findOne<ModelSchema>({
|
||||||
_id: modelId,
|
_id: modelId,
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
throw new Error('模型不存在');
|
throw new Error('无权使用该模型');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 chat 数据
|
// 创建 chat 数据
|
||||||
const response = await Chat.create({
|
const response = await Chat.create({
|
||||||
userId,
|
userId,
|
||||||
modelId,
|
modelId,
|
||||||
expiredTime: Date.now() + model.security.expiredTime,
|
content: []
|
||||||
loadAmount: model.security.maxLoadAmount
|
|
||||||
});
|
});
|
||||||
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
data: response._id
|
data: response._id // 即聊天框的 ID
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { jsonRes } from '@/service/response';
|
|
||||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
|
||||||
import type { ModelType } from '@/types/model';
|
|
||||||
import { getOpenAIApi } from '@/service/utils/chat';
|
|
||||||
import { ChatItemType } from '@/types/chat';
|
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
|
||||||
|
|
||||||
/* 发送提示词 */
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
try {
|
|
||||||
const { prompt, chatId } = req.body as { prompt: ChatItemType[]; chatId: string };
|
|
||||||
|
|
||||||
if (!prompt || !chatId) {
|
|
||||||
throw new Error('缺少参数');
|
|
||||||
}
|
|
||||||
|
|
||||||
await connectToDatabase();
|
|
||||||
|
|
||||||
// 获取 chat 数据
|
|
||||||
const chat = await Chat.findById(chatId)
|
|
||||||
.populate({
|
|
||||||
path: 'modelId',
|
|
||||||
options: {
|
|
||||||
strictPopulate: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.populate({
|
|
||||||
path: 'userId',
|
|
||||||
options: {
|
|
||||||
strictPopulate: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!chat || !chat.modelId || !chat.userId) {
|
|
||||||
throw new Error('聊天已过期');
|
|
||||||
}
|
|
||||||
|
|
||||||
const model: ModelType = chat.modelId;
|
|
||||||
|
|
||||||
// 获取 user 的 apiKey
|
|
||||||
const user = chat.userId;
|
|
||||||
|
|
||||||
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
|
|
||||||
|
|
||||||
if (!userApiKey) {
|
|
||||||
throw new Error('缺少ApiKey, 无法请求');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 chatAPI
|
|
||||||
const chatAPI = getOpenAIApi(userApiKey);
|
|
||||||
|
|
||||||
// prompt处理
|
|
||||||
const formatPrompt = prompt.map((item) => `${item.value}\n\n###\n\n`).join('');
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
const response = await chatAPI.createCompletion(
|
|
||||||
{
|
|
||||||
model: model.service.modelName,
|
|
||||||
prompt: formatPrompt,
|
|
||||||
temperature: 0.5,
|
|
||||||
max_tokens: model.security.contentMaxLen,
|
|
||||||
top_p: 1,
|
|
||||||
frequency_penalty: 0,
|
|
||||||
presence_penalty: 0.6,
|
|
||||||
stop: ['###']
|
|
||||||
},
|
|
||||||
openaiProxy
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseMessage = response.data.choices[0]?.text;
|
|
||||||
|
|
||||||
jsonRes(res, {
|
|
||||||
data: responseMessage
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
jsonRes(res, {
|
|
||||||
code: 500,
|
|
||||||
error: err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
|
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||||
import type { ModelType } from '@/types/model';
|
import type { ChatPopulate } from '@/types/mongoSchema';
|
||||||
|
import type { InitChatResponse } from '@/api/response/chat';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
|
||||||
/* 获取我的模型 */
|
/* 初始化我的聊天框,需要身份验证 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { chatId, windowId } = req.query as { chatId: string; windowId?: string };
|
const { authorization } = req.headers;
|
||||||
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
|
const { chatId } = req.query as { chatId: string };
|
||||||
|
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
@@ -15,74 +20,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// 获取 chat 数据
|
// 获取 chat 数据
|
||||||
const chat = await Chat.findById(chatId).populate({
|
const chat = await Chat.findOne<ChatPopulate>({
|
||||||
|
_id: chatId,
|
||||||
|
userId
|
||||||
|
}).populate({
|
||||||
path: 'modelId',
|
path: 'modelId',
|
||||||
options: {
|
options: {
|
||||||
strictPopulate: false
|
strictPopulate: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 安全校验
|
if (!chat) {
|
||||||
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
|
throw new Error('聊天框不存在');
|
||||||
throw new Error('聊天框已过期');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chat.loadAmount > 0) {
|
// filter 掉被 deleted 的内容
|
||||||
await Chat.updateOne(
|
chat.content = chat.content.filter((item) => item.deleted !== true);
|
||||||
{
|
|
||||||
_id: chat._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$inc: { loadAmount: -1 }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const model: ModelType = chat.modelId;
|
const model = chat.modelId;
|
||||||
|
jsonRes<InitChatResponse>(res, {
|
||||||
/* 查找是否有记录 */
|
|
||||||
let history = null;
|
|
||||||
let responseId = windowId;
|
|
||||||
try {
|
|
||||||
history = await ChatWindow.findById(windowId);
|
|
||||||
} catch (error) {
|
|
||||||
error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultContent = model.systemPrompt
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
obj: 'SYSTEM',
|
|
||||||
value: model.systemPrompt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!history) {
|
|
||||||
// 没有记录,创建一个
|
|
||||||
const response = await ChatWindow.create({
|
|
||||||
chatId,
|
|
||||||
updateTime: Date.now(),
|
|
||||||
content: defaultContent
|
|
||||||
});
|
|
||||||
responseId = response._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonRes(res, {
|
|
||||||
data: {
|
data: {
|
||||||
windowId: responseId,
|
chatId: chat._id,
|
||||||
chatSite: {
|
|
||||||
modelId: model._id,
|
modelId: model._id,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
avatar: model.avatar,
|
avatar: model.avatar,
|
||||||
secret: model.security,
|
intro: model.intro,
|
||||||
chatModel: model.service.chatModel
|
modelName: model.service.modelName,
|
||||||
},
|
chatModel: model.service.chatModel,
|
||||||
history: history ? history.content : defaultContent
|
history: chat.content
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
code: 500,
|
code: 500,
|
||||||
error: err
|
error: err
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { jsonRes } from '@/service/response';
|
|
||||||
import { ChatItemType } from '@/types/chat';
|
|
||||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
|
||||||
import type { ModelType } from '@/types/model';
|
|
||||||
import { authChat } from '@/service/utils/chat';
|
|
||||||
|
|
||||||
/* 聊天预请求,存储聊天内容 */
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
try {
|
|
||||||
const { windowId, prompt, chatId } = req.body as {
|
|
||||||
windowId: string;
|
|
||||||
prompt: ChatItemType;
|
|
||||||
chatId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!windowId || !prompt || !chatId) {
|
|
||||||
throw new Error('缺少参数');
|
|
||||||
}
|
|
||||||
|
|
||||||
await connectToDatabase();
|
|
||||||
|
|
||||||
const { chat } = await authChat(chatId);
|
|
||||||
|
|
||||||
// 长度校验
|
|
||||||
const model: ModelType = chat.modelId;
|
|
||||||
if (prompt.value.length > model.security.contentMaxLen) {
|
|
||||||
throw new Error('输入内容超长');
|
|
||||||
}
|
|
||||||
|
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
|
||||||
$push: { content: prompt },
|
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
jsonRes(res);
|
|
||||||
} catch (err) {
|
|
||||||
jsonRes(res, {
|
|
||||||
code: 500,
|
|
||||||
error: err
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
40
src/pages/api/chat/saveChat.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { ChatItemType } from '@/types/chat';
|
||||||
|
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||||
|
|
||||||
|
/* 聊天内容存存储 */
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { chatId, prompts } = req.body as {
|
||||||
|
chatId: string;
|
||||||
|
prompts: ChatItemType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!chatId || !prompts) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
// 存入库
|
||||||
|
await Chat.findByIdAndUpdate(chatId, {
|
||||||
|
$push: {
|
||||||
|
content: {
|
||||||
|
$each: prompts.map((item) => ({
|
||||||
|
obj: item.obj,
|
||||||
|
value: item.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateTime: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
jsonRes(res);
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
200
src/pages/api/chat/vectorGpt.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
|
import { authChat } from '@/service/utils/chat';
|
||||||
|
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
|
||||||
|
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||||
|
import { ChatItemType } from '@/types/chat';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import type { ModelSchema } from '@/types/mongoSchema';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
|
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
|
||||||
|
import { pushChatBill } from '@/service/events/pushBill';
|
||||||
|
import { connectRedis } from '@/service/redis';
|
||||||
|
import { VecModelDataPrefix } from '@/constants/redis';
|
||||||
|
import { vectorToBuffer } from '@/utils/tools';
|
||||||
|
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
/* 发送提示词 */
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
let step = 0; // step=1时,表示开始了流响应
|
||||||
|
const stream = new PassThrough();
|
||||||
|
stream.on('error', () => {
|
||||||
|
console.log('error: ', 'stream error');
|
||||||
|
stream.destroy();
|
||||||
|
});
|
||||||
|
res.on('close', () => {
|
||||||
|
stream.destroy();
|
||||||
|
});
|
||||||
|
res.on('error', () => {
|
||||||
|
console.log('error: ', 'request error');
|
||||||
|
stream.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { chatId, prompt } = req.body as {
|
||||||
|
prompt: ChatItemType;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
if (!chatId || !prompt) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase();
|
||||||
|
const redis = await connectRedis();
|
||||||
|
let startTime = Date.now();
|
||||||
|
|
||||||
|
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||||
|
|
||||||
|
const model: ModelSchema = chat.modelId;
|
||||||
|
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
|
||||||
|
if (!modelConstantsData) {
|
||||||
|
throw new Error('模型加载异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取对话内容
|
||||||
|
const prompts = [...chat.content, prompt];
|
||||||
|
|
||||||
|
// 获取提示词的向量
|
||||||
|
const { vector: promptVector, chatAPI } = await openaiCreateEmbedding({
|
||||||
|
isPay: !userApiKey,
|
||||||
|
apiKey: userApiKey || systemKey,
|
||||||
|
userId,
|
||||||
|
text: prompt.value
|
||||||
|
});
|
||||||
|
|
||||||
|
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
|
||||||
|
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
|
||||||
|
const redisData: any[] = await redis.sendCommand([
|
||||||
|
'FT.SEARCH',
|
||||||
|
`idx:${VecModelDataPrefix}:hash`,
|
||||||
|
`@modelId:{${String(
|
||||||
|
chat.modelId._id
|
||||||
|
)}} @vector:[VECTOR_RANGE ${similarity} $blob]=>{$YIELD_DISTANCE_AS: score}`,
|
||||||
|
'RETURN',
|
||||||
|
'1',
|
||||||
|
'text',
|
||||||
|
'SORTBY',
|
||||||
|
'score',
|
||||||
|
'PARAMS',
|
||||||
|
'2',
|
||||||
|
'blob',
|
||||||
|
vectorToBuffer(promptVector),
|
||||||
|
'LIMIT',
|
||||||
|
'0',
|
||||||
|
'30',
|
||||||
|
'DIALECT',
|
||||||
|
'2'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const formatRedisPrompt: string[] = [];
|
||||||
|
// 格式化响应值,获取 qa
|
||||||
|
for (let i = 2; i < 61; i += 2) {
|
||||||
|
const text = redisData[i]?.[1];
|
||||||
|
if (text) {
|
||||||
|
formatRedisPrompt.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高相似度+退出,无法匹配时直接退出 */
|
||||||
|
if (
|
||||||
|
formatRedisPrompt.length === 0 &&
|
||||||
|
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
|
||||||
|
) {
|
||||||
|
return res.send('对不起,你的问题不在知识库中。');
|
||||||
|
}
|
||||||
|
/* 高相似度+无上下文,不添加额外知识 */
|
||||||
|
if (
|
||||||
|
formatRedisPrompt.length === 0 &&
|
||||||
|
model.search.mode === ModelVectorSearchModeEnum.noContext
|
||||||
|
) {
|
||||||
|
prompts.unshift({
|
||||||
|
obj: 'SYSTEM',
|
||||||
|
value: model.systemPrompt
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 有匹配情况下,添加知识库内容。
|
||||||
|
// 系统提示词过滤,最多 2800 tokens
|
||||||
|
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2800);
|
||||||
|
|
||||||
|
prompts.unshift({
|
||||||
|
obj: 'SYSTEM',
|
||||||
|
value: `${model.systemPrompt} 用知识库内容回答,知识库内容为: "当前时间:${dayjs().format(
|
||||||
|
'YYYY/MM/DD HH:mm:ss'
|
||||||
|
)} ${systemPrompt}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制在 tokens 数量,防止超出
|
||||||
|
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||||
|
|
||||||
|
// 格式化文本内容成 chatgpt 格式
|
||||||
|
const map = {
|
||||||
|
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||||
|
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||||
|
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||||
|
};
|
||||||
|
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||||
|
(item: ChatItemType) => ({
|
||||||
|
role: map[item.obj],
|
||||||
|
content: item.value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// console.log(formatPrompts);
|
||||||
|
// 计算温度
|
||||||
|
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||||
|
|
||||||
|
// 发出请求
|
||||||
|
const chatResponse = await chatAPI.createChatCompletion(
|
||||||
|
{
|
||||||
|
model: model.service.chatModel,
|
||||||
|
temperature: temperature,
|
||||||
|
// max_tokens: modelConstantsData.maxToken,
|
||||||
|
messages: formatPrompts,
|
||||||
|
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||||
|
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||||
|
stream: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 40000,
|
||||||
|
responseType: 'stream',
|
||||||
|
httpsAgent: httpsAgent(!userApiKey)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
|
||||||
|
|
||||||
|
step = 1;
|
||||||
|
|
||||||
|
const { responseContent } = await gpt35StreamResponse({
|
||||||
|
res,
|
||||||
|
stream,
|
||||||
|
chatResponse
|
||||||
|
});
|
||||||
|
|
||||||
|
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||||
|
// 只有使用平台的 key 才计费
|
||||||
|
pushChatBill({
|
||||||
|
isPay: !userApiKey,
|
||||||
|
modelName: model.service.modelName,
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
text: promptsContent + responseContent
|
||||||
|
});
|
||||||
|
// jsonRes(res);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (step === 1) {
|
||||||
|
// 直接结束流
|
||||||
|
console.log('error,结束');
|
||||||
|
stream.destroy();
|
||||||
|
} else {
|
||||||
|
res.status(500);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/pages/api/data/delData.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, Data, DataItem } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
import type { DataListItem } from '@/types/data';
|
||||||
|
import type { PagingData } from '@/types';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
throw new Error('缺少登录凭证');
|
||||||
|
}
|
||||||
|
|
||||||
|
await authToken(authorization);
|
||||||
|
|
||||||
|
const { dataId } = req.query as { dataId: string };
|
||||||
|
if (!dataId) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
await Data.findByIdAndUpdate(dataId, {
|
||||||
|
isDeleted: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 改变 dataItem 状态为 0
|
||||||
|
await DataItem.updateMany(
|
||||||
|
{
|
||||||
|
dataId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jsonRes<PagingData<DataListItem>>(res);
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/pages/api/data/getDataItems.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, DataItem } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
let {
|
||||||
|
dataId,
|
||||||
|
pageNum = 1,
|
||||||
|
pageSize = 10
|
||||||
|
} = req.query as { dataId: string; pageNum: string; pageSize: string };
|
||||||
|
pageNum = +pageNum;
|
||||||
|
pageSize = +pageSize;
|
||||||
|
|
||||||
|
if (!dataId) {
|
||||||
|
throw new Error('参数错误');
|
||||||
|
}
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
await authToken(authorization);
|
||||||
|
|
||||||
|
const dataItems = await DataItem.find({
|
||||||
|
dataId
|
||||||
|
})
|
||||||
|
.sort({ _id: -1 }) // 按照创建时间倒序排列
|
||||||
|
.skip((pageNum - 1) * pageSize)
|
||||||
|
.limit(pageSize);
|
||||||
|
|
||||||
|
jsonRes(res, {
|
||||||
|
data: {
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
data: dataItems,
|
||||||
|
total: await DataItem.countDocuments({
|
||||||
|
dataId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/pages/api/data/getDataList.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, Data, DataItem } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
import type { DataListItem } from '@/types/data';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
throw new Error('缺少登录凭证');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
const datalist = await Data.aggregate<DataListItem>([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
userId: new mongoose.Types.ObjectId(userId),
|
||||||
|
isDeleted: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { createTime: -1 } // 按照创建时间倒序排列
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'dataitems',
|
||||||
|
localField: '_id',
|
||||||
|
foreignField: 'dataId',
|
||||||
|
as: 'items'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$addFields: {
|
||||||
|
totalData: {
|
||||||
|
$size: '$items' // 统计dataItem的总数
|
||||||
|
},
|
||||||
|
trainingData: {
|
||||||
|
$size: {
|
||||||
|
$filter: {
|
||||||
|
input: '$items',
|
||||||
|
as: 'item',
|
||||||
|
cond: { $ne: ['$$item.status', 0] } // 统计 status 不为0的数量
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
items: 0 // 不返回 items 字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonRes(res, {
|
||||||
|
data: datalist
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/pages/api/data/postData.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, Data } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
import type { DataType } from '@/types/data';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
let { name, type } = req.body as { name: string; type: DataType };
|
||||||
|
if (!name || !type) {
|
||||||
|
throw new Error('参数错误');
|
||||||
|
}
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
|
// 生成 data 集合
|
||||||
|
const data = await Data.create({
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
|
||||||
|
jsonRes(res, {
|
||||||
|
data: data._id
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/pages/api/data/putDataName.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, Data } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
import type { DataListItem } from '@/types/data';
|
||||||
|
import type { PagingData } from '@/types';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
throw new Error('缺少登录凭证');
|
||||||
|
}
|
||||||
|
|
||||||
|
await authToken(authorization);
|
||||||
|
|
||||||
|
const { dataId, name } = req.query as { dataId: string; name: string };
|
||||||
|
if (!dataId || !name) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
await Data.findByIdAndUpdate(dataId, {
|
||||||
|
name
|
||||||
|
});
|
||||||
|
|
||||||
|
jsonRes<PagingData<DataListItem>>(res);
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/pages/api/data/splitData.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { connectToDatabase, DataItem, Data } from '@/service/mongo';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
import { generateQA } from '@/service/events/generateQA';
|
||||||
|
import { generateAbstract } from '@/service/events/generateAbstract';
|
||||||
|
import { encode } from 'gpt-token-utils';
|
||||||
|
|
||||||
|
/* 拆分数据成QA */
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const { text, dataId } = req.body as { text: string; dataId: string };
|
||||||
|
if (!text || !dataId) {
|
||||||
|
throw new Error('参数错误');
|
||||||
|
}
|
||||||
|
await connectToDatabase();
|
||||||
|
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
|
const DataRecord = await Data.findById(dataId);
|
||||||
|
|
||||||
|
if (!DataRecord) {
|
||||||
|
throw new Error('找不到数据集');
|
||||||
|
}
|
||||||
|
const replaceText = text.replace(/[\\n]+/g, ' ');
|
||||||
|
|
||||||
|
// 文本拆分成 chunk
|
||||||
|
let chunks = replaceText.match(/[^!?.。]+[!?.。]/g) || [];
|
||||||
|
|
||||||
|
const dataItems: any[] = [];
|
||||||
|
let splitText = '';
|
||||||
|
|
||||||
|
chunks.forEach((chunk) => {
|
||||||
|
splitText += chunk;
|
||||||
|
const tokens = encode(splitText).length;
|
||||||
|
if (tokens >= 780) {
|
||||||
|
dataItems.push({
|
||||||
|
userId,
|
||||||
|
dataId,
|
||||||
|
type: DataRecord.type,
|
||||||
|
text: splitText,
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
splitText = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量插入数据
|
||||||
|
await DataItem.insertMany(dataItems);
|
||||||
|
|
||||||
|
try {
|
||||||
|
generateQA();
|
||||||
|
generateAbstract();
|
||||||
|
} catch (error) {
|
||||||
|
error;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonRes(res, {
|
||||||
|
data: { chunks, replaceText }
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,48 +3,42 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase } from '@/service/mongo';
|
import { connectToDatabase } from '@/service/mongo';
|
||||||
import { authToken } from '@/service/utils/tools';
|
import { authToken } from '@/service/utils/tools';
|
||||||
import { ModelStatusEnum, OpenAiList } from '@/constants/model';
|
import { ModelStatusEnum, modelList, ChatModelNameEnum, ChatModelNameMap } from '@/constants/model';
|
||||||
import { Model } from '@/service/models/model';
|
import { Model } from '@/service/models/model';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
const { name, serviceModelName, serviceModelCompany = 'openai' } = req.body;
|
const { name, serviceModelName } = req.body as {
|
||||||
|
name: string;
|
||||||
|
serviceModelName: `${ChatModelNameEnum}`;
|
||||||
|
};
|
||||||
const { authorization } = req.headers;
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
throw new Error('无权操作');
|
throw new Error('无权操作');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name || !serviceModelName || !serviceModelCompany) {
|
if (!name || !serviceModelName) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 凭证校验
|
// 凭证校验
|
||||||
const userId = await authToken(authorization);
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
const modelItem = OpenAiList.find((item) => item.model === serviceModelName);
|
const modelItem = modelList.find((item) => item.model === serviceModelName);
|
||||||
|
|
||||||
if (!modelItem) {
|
if (!modelItem) {
|
||||||
throw new Error('模型错误');
|
throw new Error('模型不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
// 重名校验
|
|
||||||
const authRepeatName = await Model.findOne({
|
|
||||||
name,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
if (authRepeatName) {
|
|
||||||
throw new Error('模型名重复');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上限校验
|
// 上限校验
|
||||||
const authCount = await Model.countDocuments({
|
const authCount = await Model.countDocuments({
|
||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
if (authCount >= 5) {
|
if (authCount >= 20) {
|
||||||
throw new Error('上限5个模型');
|
throw new Error('上限 20 个模型');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建模型
|
// 创建模型
|
||||||
@@ -53,10 +47,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
userId,
|
userId,
|
||||||
status: ModelStatusEnum.running,
|
status: ModelStatusEnum.running,
|
||||||
service: {
|
service: {
|
||||||
company: serviceModelCompany,
|
company: modelItem.serviceCompany,
|
||||||
trainId: modelItem.trainName,
|
trainId: '',
|
||||||
chatModel: modelItem.model,
|
chatModel: ChatModelNameMap[modelItem.model], // 聊天时用的模型
|
||||||
modelName: modelItem.model
|
modelName: modelItem.model // 最底层的模型,不会变,用于计费等核心操作
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
41
src/pages/api/model/data/delModelDataById.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { authToken } from '@/service/utils/tools';
|
||||||
|
import { connectRedis } from '@/service/redis';
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
|
try {
|
||||||
|
let { dataId } = req.query as {
|
||||||
|
dataId: string;
|
||||||
|
};
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
if (!authorization) {
|
||||||
|
throw new Error('无权操作');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dataId) {
|
||||||
|
throw new Error('缺少参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 凭证校验
|
||||||
|
const userId = await authToken(authorization);
|
||||||
|
|
||||||
|
const redis = await connectRedis();
|
||||||
|
|
||||||
|
// 校验是否为该用户的数据
|
||||||
|
const dataItemUserId = await redis.hGet(dataId, 'userId');
|
||||||
|
if (dataItemUserId !== userId) {
|
||||||
|
throw new Error('无权操作');
|
||||||
|
}
|
||||||
|
// 删除
|
||||||
|
await redis.del(dataId);
|
||||||
|
jsonRes(res);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
jsonRes(res, {
|
||||||
|
code: 500,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||