Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2d93c953 | ||
|
|
e60c36b423 | ||
|
|
1f112f7715 | ||
|
|
adbaa8b37b | ||
|
|
29c95d24ae | ||
|
|
e0b1a78344 | ||
|
|
2774940851 | ||
|
|
c2c73ed23c | ||
|
|
9682c82713 | ||
|
|
e8d4933dc4 | ||
|
|
0b6020a9cd | ||
|
|
894beee266 | ||
|
|
405e453ed3 | ||
|
|
79d289e25b | ||
|
|
51054f5829 | ||
|
|
317fba1855 | ||
|
|
f61e467d04 | ||
|
|
27de1cad47 | ||
|
|
3ea2cf1dcb | ||
|
|
4397a0ad6b | ||
|
|
4f51839026 | ||
|
|
3c0fa30aaf | ||
|
|
02abe42afe | ||
|
|
088a90de10 |
@@ -1,13 +1,25 @@
|
||||
# proxy
|
||||
AXIOS_PROXY_HOST=127.0.0.1
|
||||
AXIOS_PROXY_PORT_FAST=7890
|
||||
AXIOS_PROXY_PORT_NORMAL=7890
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false
|
||||
MY_MAIL=11111111@qq.com
|
||||
MAILE_CODE=sdasadasfasfad
|
||||
aliAccessKeyId=阿里云keyid
|
||||
aliAccessKeySecret=阿里云keysecret
|
||||
aliSignName=短信签名
|
||||
aliTemplateCode=SMS_号
|
||||
TOKEN_KEY=sssssssss
|
||||
OPENAIKEY=sk-afadfadfadfsd
|
||||
REDIS_URL=redis://default:password@0.0.0.0:8100
|
||||
queueTask=1
|
||||
parentUrl=https://hostname/api/openapi/startEvents
|
||||
# email
|
||||
MY_MAIL=xxx@qq.com
|
||||
MAILE_CODE=xxx
|
||||
# ali ems
|
||||
aliAccessKeyId=xxx
|
||||
aliAccessKeySecret=xxx
|
||||
aliSignName=xxx
|
||||
aliTemplateCode=SMS_xxx
|
||||
# token
|
||||
TOKEN_KEY=xxx
|
||||
# openai
|
||||
OPENAIKEY=sk-xxx
|
||||
# db
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
|
||||
PG_HOST=0.0.0.0
|
||||
PG_PORT=8100
|
||||
PG_USER=xxx
|
||||
PG_PASSWORD=xxx
|
||||
PG_DB_NAME=xxx
|
||||
48
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y nodejs npm
|
||||
- # Add support for more platforms with QEMU (optional)
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Login to gitbub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GH_PAT }}
|
||||
- name: build and publish image
|
||||
env:
|
||||
# fork friendly ^^
|
||||
DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/fast-gpt
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--label "org.opencontainers.image.licenses=MIT" \
|
||||
--push \
|
||||
-t ${DOCKER_REPO}:latest \
|
||||
-f Dockerfile \
|
||||
.
|
||||
256
README.md
@@ -1,20 +1,41 @@
|
||||
# Fast GPT
|
||||
|
||||
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
|
||||
Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,目前集成了 gpt35 和 embedding. 可构建自己的知识库。
|
||||
|
||||
## 知识库原理
|
||||

|
||||
|
||||
## 开发
|
||||
复制 .env.template 成 .env.local ,填写核心参数
|
||||
|
||||
```
|
||||
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
||||
AXIOS_PROXY_PORT_FAST=代理端口1,clash默认为7890
|
||||
AXIOS_PROXY_PORT_NORMAL=代理端口2
|
||||
MONGODB_URI=mongo数据库地址
|
||||
MY_MAIL=发送验证码邮箱
|
||||
MAILE_CODE=邮箱秘钥(代理里设置的是QQ邮箱,不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件")
|
||||
TOKEN_KEY=随便填一个,用于生成和校验 token
|
||||
OPENAIKEY=openai的key
|
||||
REDIS_URL=redis的地址
|
||||
```bash
|
||||
# proxy(不需要代理可忽略)
|
||||
AXIOS_PROXY_HOST=127.0.0.1
|
||||
AXIOS_PROXY_PORT_FAST=7890
|
||||
AXIOS_PROXY_PORT_NORMAL=7890
|
||||
queueTask=1
|
||||
parentUrl=https://hostname/api/openapi/startEvents
|
||||
# email,参考 nodeMail 获取参数
|
||||
MY_MAIL=xxx@qq.com
|
||||
MAILE_CODE=xxx
|
||||
# 阿里短信服务
|
||||
aliAccessKeyId=xxx
|
||||
aliAccessKeySecret=xxx
|
||||
aliSignName=xxx
|
||||
aliTemplateCode=SMS_xxx
|
||||
# token(随便填,登录凭证)
|
||||
TOKEN_KEY=xxx
|
||||
# openai key
|
||||
OPENAIKEY=sk-xxx
|
||||
# mongo连接地址
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
|
||||
MONGODB_NAME=xxx # mongo数据库名称
|
||||
# pg 数据库相关内容,和 docker-compose 对上
|
||||
PG_HOST=0.0.0.0
|
||||
PG_PORT=8102
|
||||
PG_USER=xxx
|
||||
PG_PASSWORD=xxx
|
||||
PG_DB_NAME=xxx
|
||||
```
|
||||
```bash
|
||||
pnpm dev
|
||||
@@ -22,25 +43,21 @@ pnpm dev
|
||||
|
||||
## 部署
|
||||
|
||||
### docker 模式
|
||||
请准备好 docker, mongo,代理, 和 nginx。 镜像走本机的代理,所以用 network=host,port 改成代理的端口,clash 一般都是 7890。
|
||||
|
||||
#### docker 打包
|
||||
```bash
|
||||
docker build -t imageName:tag .
|
||||
docker push imageName:tag
|
||||
# 或者直接拉镜像,见下方
|
||||
```
|
||||
|
||||
#### 软件教程:docker 安装
|
||||
### 安装 docker 和 docker-compose
|
||||
这个不同系统略有区别,百度安装下。验证安装成功后进行下一步。下面给出一个例子:
|
||||
```bash
|
||||
# 安装docker
|
||||
curl -sSL https://get.daocloud.io/docker | sh
|
||||
curl -L https://get.daocloud.io/docker | sh
|
||||
sudo systemctl start docker
|
||||
# 安装 docker-compose
|
||||
curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
# 验证安装
|
||||
docker -v
|
||||
docker-compose -v
|
||||
```
|
||||
|
||||
#### 软件教程: clash 代理
|
||||
### 安装 clash 代理(选)
|
||||
```bash
|
||||
# 下载包
|
||||
curl https://glados.rocks/tools/clash-linux.zip -o clash.zip
|
||||
@@ -71,72 +88,38 @@ 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
|
||||
### 本地 docker 打包
|
||||
```bash
|
||||
docker build -t imageName:tag .
|
||||
docker push imageName:tag
|
||||
# 或者直接拉镜像,见下方
|
||||
```
|
||||
**redis.conf**
|
||||
|
||||
### 准备初始化文件
|
||||
**/root/fast-gpt/pg/init.sql**
|
||||
```sql
|
||||
#!/bin/bash
|
||||
set -e
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
|
||||
CREATE EXTENSION vector;
|
||||
-- init table
|
||||
CREATE TABLE modelData (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vector VECTOR(1536),
|
||||
status VARCHAR(50) NOT NULL,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
model_id VARCHAR(50) NOT NULL,
|
||||
q TEXT NOT NULL,
|
||||
a TEXT NOT NULL
|
||||
);
|
||||
-- create index
|
||||
CREATE INDEX modelData_status_index ON modelData USING HASH (status);
|
||||
CREATE INDEX modelData_userId_index ON modelData USING HASH (user_id);
|
||||
CREATE INDEX modelData_modelId_index ON modelData USING HASH (model_id);
|
||||
EOSQL
|
||||
```
|
||||
## 开启aop持久化
|
||||
appendonly yes
|
||||
#default: 持久化文件
|
||||
appendfilename "appendonly.aof"
|
||||
#default: 每秒同步一次
|
||||
appendfsync everysec
|
||||
```
|
||||
**nginx.conf**
|
||||
**/root/fast-gpt/nginx/nginx.conf**
|
||||
```conf
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
@@ -169,9 +152,9 @@ http {
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name fastgpt.ahapocket.cn;
|
||||
ssl_certificate /ssl/fastgpt.pem;
|
||||
ssl_certificate_key /ssl/fastgpt.key;
|
||||
server_name docgpt.ahapocket.cn;
|
||||
ssl_certificate /ssl/docgpt.pem;
|
||||
ssl_certificate_key /ssl/docgpt.key;
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
location / {
|
||||
@@ -183,25 +166,91 @@ http {
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
server_name fastgpt.ahapocket.cn;
|
||||
server_name docgpt.ahapocket.cn;
|
||||
rewrite ^(.*) https://$server_name$1 permanent;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 运行脚本
|
||||
**redis 初始化**
|
||||
```bash
|
||||
# 进入容器
|
||||
docker exec -it 容器ID bash
|
||||
redis-cli -p 6379
|
||||
auth psw1234
|
||||
# 添加索引
|
||||
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
|
||||
**/root/fast-gpt/docker-compose.yml**
|
||||
```yml
|
||||
version: "3.3"
|
||||
services:
|
||||
fast-gpt:
|
||||
image: c121914yu/fast-gpt:latest
|
||||
network_mode: host
|
||||
restart: always
|
||||
container_name: fast-gpt
|
||||
environment:
|
||||
# 代理(不需要代理,可去掉下面三个参数)
|
||||
- AXIOS_PROXY_HOST=127.0.0.1
|
||||
- AXIOS_PROXY_PORT_FAST=7890
|
||||
- AXIOS_PROXY_PORT_NORMAL=7890
|
||||
# 邮箱
|
||||
- MY_MAIL=xxxx@qq.com
|
||||
- MAILE_CODE=xxxx
|
||||
# 阿里云短信
|
||||
- aliAccessKeyId=xxxx
|
||||
- aliAccessKeySecret=xxxx
|
||||
- aliSignName=xxxxx
|
||||
- aliTemplateCode=SMS_xxxx
|
||||
# 登录 key
|
||||
- TOKEN_KEY=xxxx
|
||||
# 是否开启队列任务。 1-开启,0-关闭(请求parentUrl去执行任务,单机时直接填1)
|
||||
- queueTask=1
|
||||
- parentUrl=https://hostname/api/openapi/startEvents
|
||||
# db
|
||||
- MONGODB_URI=mongodb://username:passsword@0.0.0.0:27017/?authSource=admin
|
||||
- MONGODB_NAME=xxx
|
||||
- PG_HOST=0.0.0.0
|
||||
- PG_PORT=8100
|
||||
- PG_USER=xxx
|
||||
- PG_PASSWORD=xxx
|
||||
- PG_DB_NAME=xxx
|
||||
# openai 账号
|
||||
- OPENAIKEY=sk-xxxxx
|
||||
nginx:
|
||||
image: nginx:alpine3.17
|
||||
container_name: nginx
|
||||
restart: always
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /root/fast-gpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- /root/fast-gpt/nginx/logs:/var/log/nginx
|
||||
- /root/fast-gpt/nginx/ssl/docgpt.key:/ssl/docgpt.key
|
||||
- /root/fast-gpt/nginx/ssl/docgpt.pem:/ssl/docgpt.pem
|
||||
pg:
|
||||
image: ankane/pgvector
|
||||
container_name: pg
|
||||
restart: always
|
||||
ports:
|
||||
- 8100:5432
|
||||
environment:
|
||||
- POSTGRES_USER=xxx
|
||||
- POSTGRES_PASSWORD=xxx
|
||||
- POSTGRES_DB=xxx
|
||||
volumes:
|
||||
- /root/fast-gpt/pg/data:/var/lib/postgresql/data
|
||||
- /root/fast-gpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
mongodb:
|
||||
image: mongo:4.0.1
|
||||
container_name: mongo
|
||||
restart: always
|
||||
ports:
|
||||
- 27017:27017
|
||||
environment:
|
||||
- MONGO_INITDB_ROOT_USERNAME=username
|
||||
- MONGO_INITDB_ROOT_PASSWORD=password
|
||||
volumes:
|
||||
- /root/fast-gpt/mongo/data:/data/db
|
||||
- /root/fast-gpt/mongo/logs:/var/log/mongodb
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
```
|
||||
### 辅助运行脚本
|
||||
**run.sh 运行文件**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
echo "Docker Compose 重新拉取镜像完成!"
|
||||
@@ -220,3 +269,6 @@ do
|
||||
docker rmi $image_id
|
||||
done
|
||||
```
|
||||
|
||||
## Mac 可能的问题
|
||||
> 因为教程有部分镜像不兼容arm64,所以写个文档指导新手如何快速在mac上面搭建fast-gpt[如何在mac上面部署fastgpt](./docs/mac.md)
|
||||
BIN
docs/imgs/KBProcess.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
100
docs/mac.md
Normal file
@@ -0,0 +1,100 @@
|
||||
## 怎么在mac上面部署fastgpt
|
||||
|
||||
### 前置条件
|
||||
|
||||
1、可以 curl api.openai.com
|
||||
|
||||
2、有openai key
|
||||
|
||||
3、有邮箱MAILE_CODE
|
||||
|
||||
4、有docker
|
||||
|
||||
```
|
||||
docker -v
|
||||
```
|
||||
|
||||
5、有pnpm ,可以使用`brew install pnpm`安装
|
||||
|
||||
6、需要创建一个放置pg和mongo数据的文件夹,这里创建在`~/fastgpt`目录中,里面有`pg` 和`mongo `两个文件夹
|
||||
|
||||
```
|
||||
➜ fast-gpt pwd
|
||||
/Users/jie/fast-gpt
|
||||
➜ fast-gpt ls
|
||||
mongo pg
|
||||
```
|
||||
|
||||
|
||||
|
||||
### docker部署方式
|
||||
|
||||
这种方式主要是为了方便调试,可以使用`pnpm dev ` 运行fast-gpt项目
|
||||
|
||||
**1、.env.local 文件**
|
||||
|
||||
```
|
||||
# proxy
|
||||
AXIOS_PROXY_HOST=127.0.0.1
|
||||
AXIOS_PROXY_PORT_FAST=7890
|
||||
AXIOS_PROXY_PORT_NORMAL=7890
|
||||
queueTask=1
|
||||
# email
|
||||
MY_MAIL= {Your Mail}
|
||||
MAILE_CODE={Yoir Mail code}
|
||||
# ali ems
|
||||
aliAccessKeyId=xxx
|
||||
aliAccessKeySecret=xxx
|
||||
aliSignName=xxx
|
||||
aliTemplateCode=SMS_xxx
|
||||
# token
|
||||
TOKEN_KEY=sswada
|
||||
# openai
|
||||
OPENAIKEY={Your openapi key}
|
||||
# db
|
||||
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
|
||||
PG_HOST=0.0.0.0
|
||||
PG_PORT=8100
|
||||
PG_USER=xxx
|
||||
PG_PASSWORD=xxx
|
||||
PG_DB_NAME=xxx
|
||||
```
|
||||
|
||||
**2、部署mongo**
|
||||
|
||||
```
|
||||
docker run --name mongo -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=username -e MONGO_INITDB_ROOT_PASSWORD=password -v ~/fast-gpt/mongo/data:/data/db -d mongo:4.0.1
|
||||
```
|
||||
|
||||
**3、部署pgsql**
|
||||
|
||||
```
|
||||
docker run -it --name pg -e "POSTGRES_PASSWORD=xxx" -e POSTGRES_USER=xxx -p 8100:5432 -v ~/fast-gpt/pg/data:/var/lib/postgresql/data -d octoberlan/pgvector:v0.4.1
|
||||
```
|
||||
|
||||
进pgsql容器运行
|
||||
|
||||
```
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
|
||||
CREATE EXTENSION vector;
|
||||
-- init table
|
||||
CREATE TABLE modelData (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vector VECTOR(1536),
|
||||
status VARCHAR(50) NOT NULL,
|
||||
user_id VARCHAR(50) NOT NULL,
|
||||
model_id VARCHAR(50) NOT NULL,
|
||||
q TEXT NOT NULL,
|
||||
a TEXT NOT NULL
|
||||
);
|
||||
-- create index
|
||||
CREATE INDEX modelData_status_index ON modelData (status);
|
||||
CREATE INDEX modelData_modelId_index ON modelData (modelId);
|
||||
CREATE INDEX modelData_userId_index ON modelData (userId);
|
||||
EOSQL
|
||||
```
|
||||
|
||||
|
||||
|
||||
4、**最后在FASTGPT项目里面运行pnpm dev 运行项目,然后进入localhost:3000 看项目是否跑起来了**
|
||||
@@ -1,13 +1,15 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const path = require('path');
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: false,
|
||||
reactStrictMode: true,
|
||||
compress: true,
|
||||
|
||||
webpack(config) {
|
||||
config.experiments = {
|
||||
asyncWebAssembly: true,
|
||||
layers: true
|
||||
};
|
||||
config.module.rules = config.module.rules.concat([
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@alicloud/tea-util": "^1.4.5",
|
||||
"@chakra-ui/icons": "^2.0.17",
|
||||
"@chakra-ui/react": "^2.5.1",
|
||||
"@chakra-ui/system": "^2.5.5",
|
||||
"@dqbd/tiktoken": "^1.0.6",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@next/font": "13.1.6",
|
||||
@@ -27,7 +29,7 @@
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^9.0.6",
|
||||
"gpt-token-utils": "^1.2.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"hyperdown": "^2.4.29",
|
||||
"immer": "^9.0.19",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
|
||||
137
pnpm-lock.yaml
generated
@@ -6,6 +6,8 @@ specifiers:
|
||||
'@alicloud/tea-util': ^1.4.5
|
||||
'@chakra-ui/icons': ^2.0.17
|
||||
'@chakra-ui/react': ^2.5.1
|
||||
'@chakra-ui/system': ^2.5.5
|
||||
'@dqbd/tiktoken': ^1.0.6
|
||||
'@emotion/react': ^11.10.6
|
||||
'@emotion/styled': ^11.10.6
|
||||
'@next/font': 13.1.6
|
||||
@@ -31,7 +33,7 @@ specifiers:
|
||||
eventsource-parser: ^0.1.0
|
||||
formidable: ^2.1.1
|
||||
framer-motion: ^9.0.6
|
||||
gpt-token-utils: ^1.2.0
|
||||
graphemer: ^1.4.0
|
||||
husky: ^8.0.3
|
||||
hyperdown: ^2.4.29
|
||||
immer: ^9.0.19
|
||||
@@ -68,8 +70,10 @@ dependencies:
|
||||
'@alicloud/dysmsapi20170525': registry.npmmirror.com/@alicloud/dysmsapi20170525/2.0.23
|
||||
'@alicloud/openapi-client': registry.npmmirror.com/@alicloud/openapi-client/0.4.5
|
||||
'@alicloud/tea-util': registry.npmmirror.com/@alicloud/tea-util/1.4.5
|
||||
'@chakra-ui/icons': registry.npmmirror.com/@chakra-ui/icons/2.0.17_react@18.2.0
|
||||
'@chakra-ui/icons': registry.npmmirror.com/@chakra-ui/icons/2.0.17_lze4h7kxffpjhokvtqbtrlfkmq
|
||||
'@chakra-ui/react': registry.npmmirror.com/@chakra-ui/react/2.5.1_e6pzu3hsaqmql4fl7jx73ckiym
|
||||
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba
|
||||
'@dqbd/tiktoken': registry.npmmirror.com/@dqbd/tiktoken/1.0.6
|
||||
'@emotion/react': registry.npmmirror.com/@emotion/react/11.10.6_pmekkgnqduwlme35zpnqhenc34
|
||||
'@emotion/styled': registry.npmmirror.com/@emotion/styled/11.10.6_oouaibmszuch5k64ms7uxp2aia
|
||||
'@next/font': registry.npmmirror.com/@next/font/13.1.6
|
||||
@@ -81,7 +85,7 @@ dependencies:
|
||||
eventsource-parser: registry.npmmirror.com/eventsource-parser/0.1.0
|
||||
formidable: registry.npmmirror.com/formidable/2.1.1
|
||||
framer-motion: registry.npmmirror.com/framer-motion/9.0.6_biqbaboplfbrettd7655fr4n2y
|
||||
gpt-token-utils: registry.npmmirror.com/gpt-token-utils/1.2.0
|
||||
graphemer: registry.npmmirror.com/graphemer/1.4.0
|
||||
hyperdown: registry.npmmirror.com/hyperdown/2.4.29
|
||||
immer: registry.npmmirror.com/immer/9.0.19
|
||||
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
|
||||
@@ -3085,6 +3089,20 @@ packages:
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/icon/3.0.16_lze4h7kxffpjhokvtqbtrlfkmq:
|
||||
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
|
||||
name: '@chakra-ui/icon'
|
||||
version: 3.0.16
|
||||
peerDependencies:
|
||||
'@chakra-ui/system': '>=2.0.0'
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/icon/3.0.16_n3dxrjldmk5gnycgnw7noyo5tu:
|
||||
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
|
||||
@@ -3099,20 +3117,7 @@ packages:
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/icon/3.0.16_react@18.2.0:
|
||||
resolution: {integrity: sha512-RpA1X5Ptz8Mt39HSyEIW1wxAz2AXyf9H0JJ5HVx/dBdMZaGMDJ0HyyPBVci0m4RCoJuyG1HHG/DXJaVfUTVAeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icon/-/icon-3.0.16.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/icon/3.0.16
|
||||
name: '@chakra-ui/icon'
|
||||
version: 3.0.16
|
||||
peerDependencies:
|
||||
'@chakra-ui/system': '>=2.0.0'
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/icons/2.0.17_react@18.2.0:
|
||||
registry.npmmirror.com/@chakra-ui/icons/2.0.17_lze4h7kxffpjhokvtqbtrlfkmq:
|
||||
resolution: {integrity: sha512-HMJP0WrJgAmFR9+Xh/CBH0nVnGMsJ4ZC8MK6tMgxPKd9/muvn0I4hsicHqdPlLpmB0TlxlhkBAKaVMtOdz6F0w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/icons/-/icons-2.0.17.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/icons/2.0.17
|
||||
name: '@chakra-ui/icons'
|
||||
@@ -3121,7 +3126,8 @@ packages:
|
||||
'@chakra-ui/system': '>=2.0.0'
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/icon': registry.npmmirror.com/@chakra-ui/icon/3.0.16_react@18.2.0
|
||||
'@chakra-ui/icon': registry.npmmirror.com/@chakra-ui/icon/3.0.16_lze4h7kxffpjhokvtqbtrlfkmq
|
||||
'@chakra-ui/system': registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
@@ -3868,6 +3874,16 @@ packages:
|
||||
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/styled-system/2.8.0:
|
||||
resolution: {integrity: sha512-bmRv/8ACJGGKGx84U1npiUddwdNifJ+/ETklGwooS5APM0ymwUtBYZpFxjYNJrqvVYpg3mVY6HhMyBVptLS7iA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/styled-system/-/styled-system-2.8.0.tgz}
|
||||
name: '@chakra-ui/styled-system'
|
||||
version: 2.8.0
|
||||
dependencies:
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
csstype: registry.npmmirror.com/csstype/3.1.1
|
||||
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/switch/2.0.22_6k64q2ggygf5zznlgufl3vff54:
|
||||
resolution: {integrity: sha512-+/Yy6y7VFD91uSPruF8ZvePi3tl5D8UNVATtWEQ+QBI92DLSM+PtgJ2F0Y9GMZ9NzMxpZ80DqwY7/kqcPCfLvw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/switch/-/switch-2.0.22.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/switch/2.0.22
|
||||
@@ -3907,6 +3923,28 @@ packages:
|
||||
react-fast-compare: registry.npmmirror.com/react-fast-compare/3.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/system/2.5.5_xqp3pgpqjlfxxa3zxu4zoc4fba:
|
||||
resolution: {integrity: sha512-52BIp/Zyvefgxn5RTByfkTeG4J+y81LWEjWm8jCaRFsLVm8IFgqIrngtcq4I7gD5n/UKbneHlb4eLHo4uc5yDQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/system/-/system-2.5.5.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/system/2.5.5
|
||||
name: '@chakra-ui/system'
|
||||
version: 2.5.5
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.0.0
|
||||
'@emotion/styled': ^11.0.0
|
||||
react: '>=18'
|
||||
dependencies:
|
||||
'@chakra-ui/color-mode': registry.npmmirror.com/@chakra-ui/color-mode/2.1.12_react@18.2.0
|
||||
'@chakra-ui/object-utils': registry.npmmirror.com/@chakra-ui/object-utils/2.0.8
|
||||
'@chakra-ui/react-utils': registry.npmmirror.com/@chakra-ui/react-utils/2.0.12_react@18.2.0
|
||||
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
|
||||
'@chakra-ui/theme-utils': registry.npmmirror.com/@chakra-ui/theme-utils/2.0.15
|
||||
'@chakra-ui/utils': registry.npmmirror.com/@chakra-ui/utils/2.0.15
|
||||
'@emotion/react': registry.npmmirror.com/@emotion/react/11.10.6_pmekkgnqduwlme35zpnqhenc34
|
||||
'@emotion/styled': registry.npmmirror.com/@emotion/styled/11.10.6_oouaibmszuch5k64ms7uxp2aia
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
react-fast-compare: registry.npmmirror.com/react-fast-compare/3.2.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/table/2.0.16_n3dxrjldmk5gnycgnw7noyo5tu:
|
||||
resolution: {integrity: sha512-vWDXZ6Ad3Aj66curp1tZBHvCfQHX2FJ4ijLiqGgQszWFIchfhJ5vMgEBJaFMZ+BN1draAjuRTZqaQefOApzvRg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/table/-/table-2.0.16.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/table/2.0.16
|
||||
@@ -3988,6 +4026,20 @@ packages:
|
||||
color2k: registry.npmmirror.com/color2k/2.0.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17_wv7sq5bj4kx5i3evdevscgumbi:
|
||||
resolution: {integrity: sha512-Auu38hnihlJZQcPok6itRDBbwof3TpXGYtDPnOvrq4Xp7jnab36HLt7KEXSDPXbtOk3ZqU99pvI1en5LbDrdjg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme-tools/-/theme-tools-2.0.17.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17
|
||||
name: '@chakra-ui/theme-tools'
|
||||
version: 2.0.17
|
||||
peerDependencies:
|
||||
'@chakra-ui/styled-system': '>=2.0.0'
|
||||
dependencies:
|
||||
'@chakra-ui/anatomy': registry.npmmirror.com/@chakra-ui/anatomy/2.1.2
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
|
||||
color2k: registry.npmmirror.com/color2k/2.0.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/theme-utils/2.0.11:
|
||||
resolution: {integrity: sha512-lBAay6Sq3/fl7exd3mFxWAbzgdQowytor0fnlHrpNStn1HgFjXukwsf6356XQOie2Vd8qaMM7qZtMh4AiC0dcg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme-utils/-/theme-utils-2.0.11.tgz}
|
||||
name: '@chakra-ui/theme-utils'
|
||||
@@ -3999,6 +4051,17 @@ packages:
|
||||
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/theme-utils/2.0.15:
|
||||
resolution: {integrity: sha512-UuxtEgE7gwMTGDXtUpTOI7F5X0iHB9ekEOG5PWPn2wWBL7rlk2JtPI7UP5Um5Yg6vvBfXYGK1ySahxqsgf+87g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme-utils/-/theme-utils-2.0.15.tgz}
|
||||
name: '@chakra-ui/theme-utils'
|
||||
version: 2.0.15
|
||||
dependencies:
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
|
||||
'@chakra-ui/theme': registry.npmmirror.com/@chakra-ui/theme/3.0.1_wv7sq5bj4kx5i3evdevscgumbi
|
||||
lodash.mergewith: registry.npmmirror.com/lodash.mergewith/4.6.2
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/theme/2.2.5_es2flcfvdj7o2v4vs237ptvmhy:
|
||||
resolution: {integrity: sha512-hYASZMwu0NqEv6PPydu+F3I+kMNd44yR4TwjR/lXBz/LEh64L6UPY6kQjebCfgdVtsGdl3HKg+eLlfa7SvfRgw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme/-/theme-2.2.5.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/theme/2.2.5
|
||||
@@ -4013,6 +4076,20 @@ packages:
|
||||
'@chakra-ui/theme-tools': registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17_es2flcfvdj7o2v4vs237ptvmhy
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/theme/3.0.1_wv7sq5bj4kx5i3evdevscgumbi:
|
||||
resolution: {integrity: sha512-92kDm/Ux/51uJqhRKevQo/O/rdwucDYcpHg2QuwzdAxISCeYvgtl2TtgOOl5EnqEP0j3IEAvZHZUlv8TTbawaw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/theme/-/theme-3.0.1.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/theme/3.0.1
|
||||
name: '@chakra-ui/theme'
|
||||
version: 3.0.1
|
||||
peerDependencies:
|
||||
'@chakra-ui/styled-system': '>=2.0.0'
|
||||
dependencies:
|
||||
'@chakra-ui/anatomy': registry.npmmirror.com/@chakra-ui/anatomy/2.1.2
|
||||
'@chakra-ui/shared-utils': registry.npmmirror.com/@chakra-ui/shared-utils/2.0.5
|
||||
'@chakra-ui/styled-system': registry.npmmirror.com/@chakra-ui/styled-system/2.8.0
|
||||
'@chakra-ui/theme-tools': registry.npmmirror.com/@chakra-ui/theme-tools/2.0.17_wv7sq5bj4kx5i3evdevscgumbi
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@chakra-ui/toast/6.0.1_jgj3ekl54faqnu3nlobnfmds2q:
|
||||
resolution: {integrity: sha512-ej2kJXvu/d2h6qnXU5D8XTyw0qpsfmbiU7hUffo/sPxkz89AUOQ08RUuUmB1ssW/FZcQvNMJ5WgzCTKHGBxtxw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@chakra-ui/toast/-/toast-6.0.1.tgz}
|
||||
id: registry.npmmirror.com/@chakra-ui/toast/6.0.1
|
||||
@@ -4101,6 +4178,12 @@ packages:
|
||||
react: registry.npmmirror.com/react/18.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@dqbd/tiktoken/1.0.6:
|
||||
resolution: {integrity: sha512-umSdeZTy/SbPPKVuZKV/XKyFPmXSN145CcM3iHjBbmhlohBJg7vaDp4cPCW+xNlWL6L2U1sp7T2BD+di2sUKdA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@dqbd/tiktoken/-/tiktoken-1.0.6.tgz}
|
||||
name: '@dqbd/tiktoken'
|
||||
version: 1.0.6
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/@emotion/babel-plugin/11.10.6:
|
||||
resolution: {integrity: sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz}
|
||||
name: '@emotion/babel-plugin'
|
||||
@@ -7583,12 +7666,6 @@ packages:
|
||||
get-intrinsic: registry.npmmirror.com/get-intrinsic/1.2.0
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/gpt-token-utils/1.2.0:
|
||||
resolution: {integrity: sha512-s8twaU38UE2Vp65JhQEjz8qvWhWY8KZYvmvYHapxlPT03Ok35Clq+gm9eE27wQILdFisseMVRSiC5lJR9GBklA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/gpt-token-utils/-/gpt-token-utils-1.2.0.tgz}
|
||||
name: gpt-token-utils
|
||||
version: 1.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz}
|
||||
name: graceful-fs
|
||||
@@ -7600,6 +7677,12 @@ packages:
|
||||
version: 1.0.4
|
||||
dev: true
|
||||
|
||||
registry.npmmirror.com/graphemer/1.4.0:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz}
|
||||
name: graphemer
|
||||
version: 1.4.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/has-bigints/1.0.2:
|
||||
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz}
|
||||
name: has-bigints
|
||||
@@ -10182,6 +10265,12 @@ packages:
|
||||
version: 3.2.0
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/react-fast-compare/3.2.1:
|
||||
resolution: {integrity: sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz}
|
||||
name: react-fast-compare
|
||||
version: 3.2.1
|
||||
dev: false
|
||||
|
||||
registry.npmmirror.com/react-focus-lock/2.9.4_pmekkgnqduwlme35zpnqhenc34:
|
||||
resolution: {integrity: sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-focus-lock/-/react-focus-lock-2.9.4.tgz}
|
||||
id: registry.npmmirror.com/react-focus-lock/2.9.4
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
## 常见问题
|
||||
### 常见问题
|
||||
**请求次数太多了**
|
||||
一般是因为自己的 openai 账号异常。请先检查自己的账号是否正常使用。
|
||||
**内容长度**
|
||||
chatgpt 上下文最长 4096 tokens, 上下文超长时会报错。
|
||||
|
||||
**删除和复制**
|
||||
点击对话头像,可以选择复制或删除该条内容。
|
||||
|
||||
电脑端:聊天内容右侧有复制和删除的图标。
|
||||
移动端:点击对话头像,可以选择复制或删除该条内容。
|
||||
**代理出错**
|
||||
服务器代理不稳定,可以过一会儿再尝试。
|
||||
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)
|
||||
@@ -15,6 +15,16 @@ wx号: fastgpt123
|
||||
4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
||||
|
||||
### 价格表
|
||||
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| chatgpt - 对话 | 0.03 |
|
||||
| 知识库 - 对话 | 0.03 |
|
||||
| 知识库 - 索引 | 0.004 |
|
||||
| 文件拆分 | 0.03 |
|
||||
|
||||
|
||||
### 定制 prompt
|
||||
|
||||
1. 进入模型编辑页
|
||||
@@ -29,12 +39,3 @@ wx号: fastgpt123
|
||||
4. 使用该模型对话。
|
||||
|
||||
注意:使用知识库模型对话时,tokens 消耗会加快。
|
||||
|
||||
### 价格表
|
||||
如果使用了自己的 Api Key,不会计费。可以在账号页,看到详细账单。单纯使用 chatGPT 模型进行对话,只有一个计费项目。使用知识库时,包含**对话**和**索引**生成两个计费项。
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
| chatgpt - 对话 | 0.03 |
|
||||
| 知识库 - 对话 | 0.03 |
|
||||
| 知识库 - 索引 | 0.004 |
|
||||
| 文件拆分 | 0.03 |
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## Fast GPT V2.7
|
||||
* FastGpt Api 允许你将 Fast Gpt 的部分功能通过 api 的形式,将知识库接入到自己的应用中,例如:飞书、企业微信、客服助手.
|
||||
* 通过 csv 文件导入和导出你的问答对。你可以将你的 csv 文件放置在飞书文档上,以便团队共享。
|
||||
### Fast GPT V2.8.1
|
||||
* 优化 - 知识库升级,内容条数不上限!
|
||||
* 优化 - 导入去重效果,可防止导出后的 csv 重复导入。
|
||||
* 优化 - 聊天框,电脑端复制删除图标。
|
||||
* 优化 - 聊天框,生成内容时,如果滚动条触底,则会自动向下滚动,不需要手动下滑。
|
||||
BIN
public/imgs/wx300-2.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
@@ -1,41 +1,32 @@
|
||||
import { GET, POST, DELETE } from './request';
|
||||
import type { ChatItemType, ChatSiteItemType } from '@/types/chat';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import type { InitChatResponse } from './response/chat';
|
||||
|
||||
/**
|
||||
* 获取一个聊天框的ID
|
||||
*/
|
||||
export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?modelId=${modelId}`);
|
||||
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
*/
|
||||
export const getInitChatSiteInfo = (chatId: string) =>
|
||||
GET<InitChatResponse>(`/chat/init?chatId=${chatId}`);
|
||||
export const getInitChatSiteInfo = (modelId: string, chatId: '' | string) =>
|
||||
GET<InitChatResponse>(`/chat/init?modelId=${modelId}&chatId=${chatId}`);
|
||||
|
||||
/**
|
||||
* 发送 GPT3 prompt
|
||||
* 获取历史记录
|
||||
*/
|
||||
export const postGPT3SendPrompt = ({
|
||||
chatId,
|
||||
prompt
|
||||
}: {
|
||||
prompt: ChatSiteItemType[];
|
||||
chatId: string;
|
||||
}) =>
|
||||
POST<string>(`/chat/gpt3`, {
|
||||
chatId,
|
||||
prompt: prompt.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
}))
|
||||
});
|
||||
export const getChatHistory = () =>
|
||||
GET<{ _id: string; title: string; modelId: string }[]>('/chat/getHistory');
|
||||
|
||||
/**
|
||||
* 删除一条历史记录
|
||||
*/
|
||||
export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${id}`);
|
||||
|
||||
/**
|
||||
* 存储一轮对话
|
||||
*/
|
||||
export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] }) =>
|
||||
POST('/chat/saveChat', data);
|
||||
export const postSaveChat = (data: {
|
||||
modelId: string;
|
||||
chatId: '' | string;
|
||||
prompts: ChatItemType[];
|
||||
}) => POST<string>('/chat/saveChat', data);
|
||||
|
||||
/**
|
||||
* 删除一句对话
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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)}`);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GET, POST, DELETE, PUT } from './request';
|
||||
import type { ModelSchema, ModelDataSchema, ModelSplitDataSchema } from '@/types/mongoSchema';
|
||||
import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema';
|
||||
import { ModelUpdateParams } from '@/types/model';
|
||||
import { TrainingItemType } from '../types/training';
|
||||
import { RequestPaging } from '../types/index';
|
||||
@@ -85,8 +85,12 @@ export const postModelDataInput = (data: {
|
||||
/**
|
||||
* 拆分数据
|
||||
*/
|
||||
export const postModelDataSplitData = (data: { modelId: string; text: string; prompt: string }) =>
|
||||
POST(`/model/data/splitData`, data);
|
||||
export const postModelDataSplitData = (data: {
|
||||
modelId: string;
|
||||
chunks: string[];
|
||||
prompt: string;
|
||||
mode: 'qa' | 'subsection';
|
||||
}) => POST(`/model/data/splitData`, data);
|
||||
|
||||
/**
|
||||
* json导入数据
|
||||
|
||||
8
src/api/response/user.d.ts
vendored
@@ -1,5 +1,13 @@
|
||||
import type { UserType } from '@/types/user';
|
||||
import type { PromotionRecordSchema } from '@/types/mongoSchema';
|
||||
export interface ResLogin {
|
||||
token: string;
|
||||
user: UserType;
|
||||
}
|
||||
|
||||
export interface PromotionRecordType {
|
||||
_id: PromotionRecordSchema['_id'];
|
||||
type: PromotionRecordSchema['type'];
|
||||
createTime: PromotionRecordSchema['createTime'];
|
||||
amount: PromotionRecordSchema['amount'];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GET, POST, PUT } from './request';
|
||||
import { createHashPassword, Obj2Query } from '@/utils/tools';
|
||||
import { ResLogin } from './response/user';
|
||||
import { ResLogin, PromotionRecordType } from './response/user';
|
||||
import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import { UserType, UserUpdateParams } from '@/types/user';
|
||||
import type { PagingData, RequestPaging } from '@/types';
|
||||
@@ -17,6 +17,14 @@ export const sendAuthCode = ({
|
||||
|
||||
export const getTokenLogin = () => GET<UserType>('/user/tokenLogin');
|
||||
|
||||
/* get promotion init data */
|
||||
export const getPromotionInitData = () =>
|
||||
GET<{
|
||||
invitedAmount: number;
|
||||
historyAmount: number;
|
||||
residueAmount: number;
|
||||
}>('/user/promotion/getPromotionData');
|
||||
|
||||
export const postRegister = ({
|
||||
username,
|
||||
password,
|
||||
@@ -73,3 +81,7 @@ export const getPayCode = (amount: number) =>
|
||||
}>(`/user/getPayCode?amount=${amount}`);
|
||||
|
||||
export const checkPayResult = (payId: string) => GET<number>(`/user/checkPayResult?payId=${payId}`);
|
||||
|
||||
/* promotion records */
|
||||
export const getPromotionRecords = (data: RequestPaging) =>
|
||||
GET<PromotionRecordType>(`/user/promotion/getPromotions?${Obj2Query(data)}`);
|
||||
|
||||
1
src/components/Icon/icons/dbModel.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="1682232349111" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7070" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28"><path d="M512 102.6c110.7 0 215 12.3 293.9 34.7 35.8 10.2 65 22.1 84.5 34.7 18.6 12 21.3 19.7 21.6 20.6-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7-78.9 22.3-183.2 34.7-293.9 34.7s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6 0.2-0.9 3-8.6 21.6-20.6 19.5-12.5 48.7-24.5 84.5-34.7 78.9-22.4 183.2-34.7 293.9-34.7m0-40c-243 0-440 58.2-440 130s197 130 440 130 440-58.2 440-130-197-130-440-130zM112 190.4H72v641h40v-641z m840-0.3h-40v641h40v-641zM912 831v0.5c-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7-78.9 22.3-183.2 34.6-293.9 34.6s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6v-0.3l-40 0.3v0.1c0 71.8 197 130 440 130s440-58.2 440-130v-0.4l-40-0.1z m0-210.5v0.5c-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7C727 698.6 622.7 711 512 711s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6v-0.3l-40 0.3v0.1c0 71.8 197 130 440 130s440-58.2 440-130v-0.4l-40-0.2z m0-221.5v0.5c-0.2 0.9-3 8.6-21.6 20.6-19.5 12.5-48.7 24.5-84.5 34.7-78.9 22.3-183.2 34.7-293.9 34.7s-215-12.3-293.9-34.7c-35.8-10.2-65-22.1-84.5-34.7-18.6-12-21.3-19.7-21.6-20.6v-0.3l-40 0.3v0.1c0 71.8 197 130 440 130s440-58.2 440-130v-0.4l-40-0.2z" fill="" p-id="7071"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/components/Icon/icons/delete.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="1681997838051" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4520" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M898 178.7H665.3c4.3-9.8 6.7-20.6 6.7-32 0-44-36-80-80-80H432c-44 0-80 36-80 80 0 11.4 2.4 22.2 6.7 32H126c-13.2 0-24 10.8-24 24s10.8 24 24 24h772c13.2 0 24-10.8 24-24s-10.8-24-24-24z m-466 0c-8.5 0-16.5-3.4-22.6-9.4-6.1-6.1-9.4-14.1-9.4-22.6s3.4-16.5 9.4-22.6c6.1-6.1 14.1-9.4 22.6-9.4h160c8.5 0 16.5 3.4 22.6 9.4 6.1 6.1 9.4 14.1 9.4 22.6 0 8.5-3.4 16.5-9.4 22.6-6.1 6.1-14.1 9.4-22.6 9.4H432zM513 774.7c18.1 0 33-14.8 33-33v-334c0-18.1-14.9-33-33-33h-2c-18.1 0-33 14.8-33 33v334c0 18.2 14.8 33 33 33h2zM363 774.7c18.1 0 33-14.8 33-33v-334c0-18.1-14.9-33-33-33h-2c-18.1 0-33 14.8-33 33v334c0 18.2 14.8 33 33 33h2zM663 774.7c18.1 0 33-14.8 33-33v-334c0-18.1-14.9-33-33-33h-2c-18.1 0-33 14.8-33 33v334c0 18.2 14.8 33 33 33h2z" p-id="4521"></path><path d="M812 280.7c-13.3 0-24 10.7-24 24v530c0 41.9-34.1 76-76 76H312c-41.9 0-76-34.1-76-76v-530c0-13.3-10.7-24-24-24s-24 10.7-24 24v530c0 68.4 55.6 124 124 124h400c68.4 0 124-55.6 124-124v-530c0-13.2-10.7-24-24-24z" p-id="4522"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/components/Icon/icons/history.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="1682232686576" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8959" xmlns:xlink="http://www.w3.org/1999/xlink" width="28" height="28"><path d="M762.805186 140.938939c-14.335497-9.66922-33.725102-5.887081-43.373857 8.398274-9.648754 14.295588-5.897314 33.714869 8.398274 43.373857 106.369609 71.852468 169.864736 191.267185 169.864736 319.445496 0 212.414831-172.802648 385.217479-385.217479 385.217479S127.259382 724.571397 127.259382 512.156566c0-128.178311 63.494103-247.593028 169.864736-319.445496 14.295588-9.658987 18.047028-29.078269 8.398274-43.373857-9.658987-14.285355-29.088502-18.067494-43.373857-8.398274C138.575102 224.432539 64.791655 363.206162 64.791655 512.156566c0 246.851131 200.834074 447.685205 447.685205 447.685205S960.162066 759.007697 960.162066 512.156566C960.162066 363.206162 886.377596 224.432539 762.805186 140.938939z" p-id="8960"></path><path d="M401.003 64.47136c-17.253966 0-31.234375 13.980409-31.234375 31.233352l0 30.470989c0 17.253966 13.980409 31.234375 31.234375 31.234375s31.234375-13.980409 31.234375-31.234375L432.237375 95.704712C432.236352 78.450746 418.256966 64.47136 401.003 64.47136z" p-id="8961"></path><path d="M623.950721 64.47136c-17.253966 0-31.233352 13.980409-31.233352 31.233352l0 30.470989c0 17.253966 13.980409 31.234375 31.233352 31.234375s31.234375-13.980409 31.234375-31.234375L655.185097 95.704712C655.184073 78.450746 641.204687 64.47136 623.950721 64.47136z" p-id="8962"></path><path d="M426.012603 227.493248c11.214413 18.047028 41.970904 48.589648 86.157265 48.589648 43.963281 0 75.105558-30.318516 86.574774-48.223305 9.222035-14.396895 5.03262-33.358759-9.242502-42.763966-14.304797-9.405207-33.593096-5.398964-43.159986 8.764618-0.132006 0.193405-13.614066 19.754926-34.172287 19.754926-19.989263 0-32.423457-18.098193-33.267685-19.36914-9.160637-14.427594-28.264741-18.799158-42.834574-9.770528C421.416935 193.584973 416.912341 212.841549 426.012603 227.493248z" p-id="8963"></path><path d="M510.781242 335.164502c-17.253966 0-31.233352 13.980409-31.233352 31.233352l0 208.225415c0 0.63445 0.149403 1.227967 0.187265 1.853208 0.067538 1.115404 0.148379 2.217505 0.333598 3.314489 0.168846 1.00898 0.416486 1.978051 0.679475 2.951215 0.258896 0.954745 0.529049 1.895163 0.87595 2.821255 0.36839 0.981351 0.801249 1.916653 1.26276 2.847861 0.431835 0.876973 0.880043 1.734504 1.393743 2.569522 0.532119 0.860601 1.115404 1.670036 1.727341 2.472308 0.610914 0.805342 1.235131 1.588171 1.926886 2.336208 0.688685 0.74292 1.424442 1.420349 2.181689 2.093684 0.741897 0.659009 1.484817 1.303692 2.298346 1.89721 0.899486 0.657986 1.850138 1.222851 2.819209 1.783623 0.544399 0.314155 1.00898 0.714268 1.577938 0.998747l208.225415 104.113219c4.484128 2.236947 9.252735 3.304256 13.94971 3.304256 11.44875 0 22.479991-6.334265 27.959795-17.274432 7.706519-15.433504 1.454118-34.192753-13.970176-41.909505l-190.961216-95.480608L542.015617 366.397854C542.015617 349.143888 528.035208 335.164502 510.781242 335.164502z" p-id="8964"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
1
src/components/Icon/icons/promotion.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="1682078370900" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3577" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M941.312 888.704H628.032a32 32 0 0 1 0-64h313.28a32 32 0 0 1 0 64zM519.808 576.768c-158.976 0-288.384-129.344-288.384-288.384S360.832 0 519.808 0s288.384 129.344 288.384 288.384-129.408 288.384-288.384 288.384z m0-512.768C396.096 64 295.424 164.672 295.424 288.384s100.672 224.384 224.384 224.384c123.776 0 224.384-100.672 224.384-224.384S643.584 64 519.808 64z" p-id="3578"></path><path d="M763.264 606.528a31.552 31.552 0 0 1-16.96-4.864 427.2 427.2 0 0 0-100.544-45.952 32 32 0 0 1-21.184-40 31.744 31.744 0 0 1 39.936-21.184 492.16 492.16 0 0 1 115.712 52.864 32 32 0 0 1-16.96 59.136zM59.776 996.928a32 32 0 0 1-32-32 489.6 489.6 0 0 1 347.328-470.464 32 32 0 1 1 18.816 61.184 425.856 425.856 0 0 0-302.144 409.28 32 32 0 0 1-32 32zM964.224 879.68a32.128 32.128 0 0 1-24.32-11.2l-108.224-126.336a32 32 0 1 1 48.64-41.6l108.224 126.336a32 32 0 0 1-24.32 52.8z" p-id="3579"></path><path d="M856 1024a32 32 0 0 1-25.664-51.2l108.224-144.32a32.064 32.064 0 0 1 51.264 38.336L881.6 1011.2a32 32 0 0 1-25.6 12.8z" p-id="3580"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/components/Icon/icons/withdraw.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="1682079057126" class="icon" viewBox="0 0 1322 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2677" xmlns:xlink="http://www.w3.org/1999/xlink" width="36.1484375" height="28"><path d="M952.04654459 837.88839531H336.95615443A113.52706888 113.52706888 0 0 1 223.61160468 724.54384556v-419.79462838h728.43493991a113.52706888 113.52706888 0 0 1 113.34454973 113.34454975V724.54384556a113.52706888 113.52706888 0 0 1-113.34454973 113.34454975zM278.36742569 359.13999928v365.03880736a58.77124787 58.77124787 0 0 0 58.58872873 58.58872874h615.09039016a58.77124787 58.77124787 0 0 0 58.58872874-58.58872874V417.72872802a58.77124787 58.77124787 0 0 0-58.58872874-58.58872874z" p-id="2678"></path><path d="M278.36742569 350.37906772H223.61160468V297.44844068A111.51935577 111.51935577 0 0 1 334.94844068 186.11160469h334.01050924a111.51935577 111.51935577 0 0 1 111.33683598 111.33683599v49.09771996h-54.75582101V297.44844068A56.76353475 56.76353475 0 0 0 668.95894991 240.8674257H334.94844068A56.76353475 56.76353475 0 0 0 278.36742569 297.44844068zM1038.19570329 704.83175018H825.92563707A131.59649008 131.59649008 0 0 1 825.92563707 441.63877003h208.43715913v54.75582103H825.92563707a76.84066906 76.84066906 0 0 0 0 153.86385725h212.27006621z" p-id="2679"></path><path d="M889.80742792 600.43065117h-65.34194654a27.37791082 27.37791082 0 0 1 0-54.75582103h65.34194654a27.37791082 27.37791082 0 0 1-1e-8 54.75582103z" p-id="2680"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -13,14 +13,27 @@ const map = {
|
||||
board: require('./icons/board.svg').default,
|
||||
develop: require('./icons/develop.svg').default,
|
||||
user: require('./icons/user.svg').default,
|
||||
chatting: require('./icons/chatting.svg').default
|
||||
chatting: require('./icons/chatting.svg').default,
|
||||
promotion: require('./icons/promotion.svg').default,
|
||||
delete: require('./icons/delete.svg').default,
|
||||
withdraw: require('./icons/withdraw.svg').default,
|
||||
dbModel: require('./icons/dbModel.svg').default,
|
||||
history: require('./icons/history.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
|
||||
return map[name] ? (
|
||||
<Icon as={map[name]} w={w} h={h} boxSizing={'content-box'} verticalAlign={'top'} {...props} />
|
||||
<Icon
|
||||
as={map[name]}
|
||||
w={w}
|
||||
h={h}
|
||||
boxSizing={'content-box'}
|
||||
verticalAlign={'top'}
|
||||
fill={'currentcolor'}
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
{
|
||||
onError(error) {
|
||||
console.log('error->', error);
|
||||
router.push('/login');
|
||||
router.replace('/login');
|
||||
toast();
|
||||
},
|
||||
onSettled() {
|
||||
|
||||
@@ -32,6 +32,12 @@ const navbarList = [
|
||||
link: '/number/setting',
|
||||
activeLink: ['/number/setting']
|
||||
},
|
||||
{
|
||||
label: '邀请',
|
||||
icon: 'promotion',
|
||||
link: '/promotion',
|
||||
activeLink: ['/promotion']
|
||||
},
|
||||
{
|
||||
label: '开发',
|
||||
icon: 'develop',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
@@ -13,7 +13,6 @@ 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();
|
||||
|
||||
return (
|
||||
@@ -63,7 +62,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
|
||||
}}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
{formatSource}
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
52
src/components/Radio/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Stack, Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import type { StackProps } from '@chakra-ui/react';
|
||||
|
||||
// @ts-ignore
|
||||
interface Props extends StackProps {
|
||||
list: { label: string; value: string | number }[];
|
||||
value: string | number;
|
||||
onChange: (e: string | number) => void;
|
||||
}
|
||||
|
||||
const Radio = ({ list, value, onChange, ...props }: Props) => {
|
||||
return (
|
||||
<Stack {...props} spacing={5} direction={'row'}>
|
||||
{list.map((item) => (
|
||||
<Flex
|
||||
key={item.value}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
_before={{
|
||||
content: '""',
|
||||
w: '16px',
|
||||
h: '16px',
|
||||
mr: 1,
|
||||
borderRadius: '16px',
|
||||
transition: '0.2s',
|
||||
...(value === item.value
|
||||
? {
|
||||
border: '5px solid',
|
||||
borderColor: 'blue.500'
|
||||
}
|
||||
: {
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.200'
|
||||
})
|
||||
}}
|
||||
_hover={{
|
||||
_before: {
|
||||
borderColor: 'blue.400'
|
||||
}
|
||||
}}
|
||||
onClick={() => onChange(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Radio;
|
||||
@@ -1,28 +1,32 @@
|
||||
import type { ServiceName, ModelDataType, ModelSchema } from '@/types/mongoSchema';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
|
||||
export enum ModelDataStatusEnum {
|
||||
ready = 'ready',
|
||||
waiting = 'waiting'
|
||||
}
|
||||
|
||||
export enum ChatModelNameEnum {
|
||||
GPT35 = 'gpt-3.5-turbo',
|
||||
VECTOR_GPT = 'VECTOR_GPT',
|
||||
VECTOR = 'text-embedding-ada-002'
|
||||
export const embeddingModel = 'text-embedding-ada-002';
|
||||
export enum ChatModelEnum {
|
||||
'GPT35' = 'gpt-3.5-turbo',
|
||||
'GPT4' = 'gpt-4',
|
||||
'GPT432k' = 'gpt-4-32k'
|
||||
}
|
||||
|
||||
export const ChatModelNameMap = {
|
||||
[ChatModelNameEnum.GPT35]: 'gpt-3.5-turbo',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo',
|
||||
[ChatModelNameEnum.VECTOR]: 'text-embedding-ada-002'
|
||||
export enum ModelNameEnum {
|
||||
GPT35 = 'gpt-3.5-turbo',
|
||||
VECTOR_GPT = 'VECTOR_GPT'
|
||||
}
|
||||
|
||||
export const Model2ChatModelMap: Record<`${ModelNameEnum}`, `${ChatModelEnum}`> = {
|
||||
[ModelNameEnum.GPT35]: 'gpt-3.5-turbo',
|
||||
[ModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo'
|
||||
};
|
||||
|
||||
export type ModelConstantsData = {
|
||||
serviceCompany: `${ServiceName}`;
|
||||
icon: 'model' | 'dbModel';
|
||||
name: string;
|
||||
model: `${ChatModelNameEnum}`;
|
||||
model: `${ModelNameEnum}`;
|
||||
trainName: string; // 空字符串代表不能训练
|
||||
maxToken: number;
|
||||
contextMaxToken: number;
|
||||
maxTemperature: number;
|
||||
price: number; // 多少钱 / 1token,单位: 0.00001元
|
||||
@@ -30,22 +34,20 @@ export type ModelConstantsData = {
|
||||
|
||||
export const modelList: ModelConstantsData[] = [
|
||||
{
|
||||
serviceCompany: 'openai',
|
||||
icon: 'model',
|
||||
name: 'chatGPT',
|
||||
model: ChatModelNameEnum.GPT35,
|
||||
model: ModelNameEnum.GPT35,
|
||||
trainName: '',
|
||||
maxToken: 4000,
|
||||
contextMaxToken: 7500,
|
||||
contextMaxToken: 4096,
|
||||
maxTemperature: 1.5,
|
||||
price: 3
|
||||
},
|
||||
{
|
||||
serviceCompany: 'openai',
|
||||
icon: 'dbModel',
|
||||
name: '知识库',
|
||||
model: ChatModelNameEnum.VECTOR_GPT,
|
||||
model: ModelNameEnum.VECTOR_GPT,
|
||||
trainName: 'vector',
|
||||
maxToken: 4000,
|
||||
contextMaxToken: 7000,
|
||||
contextMaxToken: 4096,
|
||||
maxTemperature: 1,
|
||||
price: 3
|
||||
}
|
||||
@@ -120,7 +122,7 @@ export const ModelVectorSearchModeMap: Record<
|
||||
export const defaultModel: ModelSchema = {
|
||||
_id: '',
|
||||
userId: '',
|
||||
name: '',
|
||||
name: 'modelName',
|
||||
avatar: '',
|
||||
status: ModelStatusEnum.pending,
|
||||
updateTime: Date.now(),
|
||||
@@ -132,10 +134,9 @@ export const defaultModel: ModelSchema = {
|
||||
mode: ModelVectorSearchModeEnum.hightSimilarity
|
||||
},
|
||||
service: {
|
||||
company: 'openai',
|
||||
trainId: '',
|
||||
chatModel: ChatModelNameEnum.GPT35,
|
||||
modelName: ChatModelNameEnum.GPT35
|
||||
chatModel: ModelNameEnum.GPT35,
|
||||
modelName: ModelNameEnum.GPT35
|
||||
},
|
||||
security: {
|
||||
domain: ['*'],
|
||||
|
||||
@@ -20,3 +20,15 @@ export const BillTypeMap: Record<`${BillTypeEnum}`, string> = {
|
||||
[BillTypeEnum.vector]: '索引生成',
|
||||
[BillTypeEnum.return]: '退款'
|
||||
};
|
||||
|
||||
export enum PromotionEnum {
|
||||
invite = 'invite',
|
||||
shareModel = 'shareModel',
|
||||
withdraw = 'withdraw'
|
||||
}
|
||||
|
||||
export const PromotionTypeMap = {
|
||||
[PromotionEnum.invite]: '好友充值',
|
||||
[PromotionEnum.shareModel]: '模型分享',
|
||||
[PromotionEnum.withdraw]: '提现'
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const usePagination = <T = any,>({
|
||||
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 maxPage = useMemo(() => Math.ceil(total / pageSize) || 1, [pageSize, total]);
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationFn: async (num: number = pageNum) => {
|
||||
|
||||
15
src/pages/_error.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
function Error({ statusCode }: { statusCode: number }) {
|
||||
return (
|
||||
<p>
|
||||
{statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
Error.getInitialProps = ({ res, err }: { res: any; err: any }) => {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
||||
console.log(err);
|
||||
return { statusCode };
|
||||
};
|
||||
|
||||
export default Error;
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/auth';
|
||||
import { httpsAgent, openaiChatFilter } 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 } from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
@@ -28,29 +26,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
const { chatId, prompt, modelId } = req.body as {
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
modelId: string;
|
||||
chatId: '' | string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
if (!modelId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
const { model, content, userApiKey, systemKey, userId } = await authChat({
|
||||
modelId,
|
||||
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 prompts = [...content, prompt];
|
||||
|
||||
// 如果有系统提示词,自动插入
|
||||
if (model.systemPrompt) {
|
||||
@@ -61,31 +63,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
// const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
const filterPrompts = openaiChatFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts,
|
||||
maxTokens: modelConstantsData.contextMaxToken - 500
|
||||
});
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
}));
|
||||
// console.log(formatPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
// console.log(filterPrompts);
|
||||
// 获取 chatAPI
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
// 发出请求
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
temperature,
|
||||
messages: filterPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: true,
|
||||
@@ -107,7 +101,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
stream,
|
||||
chatResponse
|
||||
});
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
@@ -115,7 +108,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptsContent + responseContent
|
||||
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Model, Chat } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
|
||||
/* 获取我的模型 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { modelId } = req.query as {
|
||||
modelId: string;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('无权生成对话');
|
||||
}
|
||||
|
||||
if (!modelId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 校验是否为用户的模型
|
||||
const model = await Model.findOne<ModelSchema>({
|
||||
_id: modelId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!model) {
|
||||
throw new Error('无权使用该模型');
|
||||
}
|
||||
|
||||
// 创建 chat 数据
|
||||
const response = await Chat.create({
|
||||
userId,
|
||||
modelId,
|
||||
content: []
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: response._id // 即聊天框的 ID
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
31
src/pages/api/chat/getHistory.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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 userId = await authToken(req.headers.authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const data = await Chat.find(
|
||||
{
|
||||
userId
|
||||
},
|
||||
'_id title modelId'
|
||||
)
|
||||
.sort({ updateTime: -1 })
|
||||
.limit(20);
|
||||
|
||||
jsonRes(res, {
|
||||
data
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import type { ChatPopulate } from '@/types/mongoSchema';
|
||||
import type { InitChatResponse } from '@/api/response/chat';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -11,43 +13,50 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { authorization } = req.headers;
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const { chatId } = req.query as { chatId: string };
|
||||
const { modelId, chatId } = req.query as { modelId: string; chatId: '' | string };
|
||||
|
||||
if (!chatId) {
|
||||
if (!modelId) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 获取 chat 数据
|
||||
const chat = await Chat.findOne<ChatPopulate>({
|
||||
_id: chatId,
|
||||
userId
|
||||
}).populate({
|
||||
path: 'modelId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
});
|
||||
// 获取 model 数据
|
||||
const { model } = await authModel(modelId, userId);
|
||||
|
||||
if (!chat) {
|
||||
throw new Error('聊天框不存在');
|
||||
// 历史记录
|
||||
let history: ChatItemType[] = [];
|
||||
|
||||
if (chatId) {
|
||||
// 获取 chat.content 数据
|
||||
history = await Chat.aggregate([
|
||||
{ $match: { _id: new mongoose.Types.ObjectId(chatId) } },
|
||||
{ $unwind: '$content' },
|
||||
{ $match: { 'content.deleted': false } },
|
||||
{ $sort: { 'content._id': -1 } },
|
||||
{ $limit: 50 },
|
||||
{
|
||||
$project: {
|
||||
id: '$content._id',
|
||||
obj: '$content.obj',
|
||||
value: '$content.value'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
history.reverse();
|
||||
}
|
||||
|
||||
// filter 掉被 deleted 的内容
|
||||
chat.content = chat.content.filter((item) => item.deleted !== true);
|
||||
|
||||
const model = chat.modelId;
|
||||
jsonRes<InitChatResponse>(res, {
|
||||
data: {
|
||||
chatId: chat._id,
|
||||
modelId: model._id,
|
||||
chatId: chatId || '',
|
||||
modelId: modelId,
|
||||
name: model.name,
|
||||
avatar: model.avatar,
|
||||
intro: model.intro,
|
||||
modelName: model.service.modelName,
|
||||
chatModel: model.service.chatModel,
|
||||
history: chat.content
|
||||
history
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
27
src/pages/api/chat/removeHistory.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
/* 获取历史记录 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { id } = req.query;
|
||||
const userId = await authToken(req.headers.authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
await Chat.findOneAndRemove({
|
||||
_id: id,
|
||||
userId
|
||||
});
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,54 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { connectToDatabase, Chat } from '@/service/mongo';
|
||||
import { authModel } from '@/service/utils/auth';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
/* 聊天内容存存储 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { chatId, prompts } = req.body as {
|
||||
chatId: string;
|
||||
const { chatId, modelId, prompts } = req.body as {
|
||||
chatId: '' | string;
|
||||
modelId: string;
|
||||
prompts: ChatItemType[];
|
||||
};
|
||||
|
||||
if (!chatId || !prompts) {
|
||||
if (!prompts) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
const userId = await authToken(req.headers.authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
// 存入库
|
||||
await Chat.findByIdAndUpdate(chatId, {
|
||||
$push: {
|
||||
content: {
|
||||
$each: prompts.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
}))
|
||||
}
|
||||
},
|
||||
updateTime: new Date()
|
||||
});
|
||||
const content = prompts.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
}));
|
||||
|
||||
// 没有 chatId, 创建一个对话
|
||||
if (!chatId) {
|
||||
await authModel(modelId, userId);
|
||||
const { _id } = await Chat.create({
|
||||
userId,
|
||||
modelId,
|
||||
content,
|
||||
title: content[0].value.slice(0, 20)
|
||||
});
|
||||
return jsonRes(res, {
|
||||
data: _id
|
||||
});
|
||||
} else {
|
||||
// 已经有记录,追加入库
|
||||
await Chat.findByIdAndUpdate(chatId, {
|
||||
$push: {
|
||||
content: {
|
||||
$each: content
|
||||
}
|
||||
},
|
||||
updateTime: new Date()
|
||||
});
|
||||
}
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authChat } from '@/service/utils/chat';
|
||||
import { httpsAgent, systemPromptFilter } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { authChat } from '@/service/utils/auth';
|
||||
import { httpsAgent, systemPromptFilter, openaiChatFilter } from '@/service/utils/tools';
|
||||
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 {
|
||||
modelList,
|
||||
ModelVectorSearchModeMap,
|
||||
ModelVectorSearchModeEnum,
|
||||
ModelDataStatusEnum
|
||||
} from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -30,29 +33,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
try {
|
||||
const { chatId, prompt } = req.body as {
|
||||
const { modelId, chatId, prompt } = req.body as {
|
||||
modelId: string;
|
||||
chatId: '' | string;
|
||||
prompt: ChatItemType;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
const { authorization } = req.headers;
|
||||
if (!chatId || !prompt) {
|
||||
if (!modelId || !prompt) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
await connectToDatabase();
|
||||
let startTime = Date.now();
|
||||
|
||||
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
|
||||
const { model, content, userApiKey, systemKey, userId } = await authChat({
|
||||
modelId,
|
||||
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 prompts = [...content, prompt];
|
||||
|
||||
// 获取提示词的向量
|
||||
const { vector: promptVector, chatAPI } = await openaiCreateEmbedding({
|
||||
@@ -66,9 +73,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
|
||||
const vectorSearch = await PgClient.select<{ id: string; q: string; a: string }>('modelData', {
|
||||
fields: ['id', 'q', 'a'],
|
||||
where: [['model_id', model._id], 'AND', `vector <=> '[${promptVector}]' < ${similarity}`],
|
||||
where: [
|
||||
['status', ModelDataStatusEnum.ready],
|
||||
'AND',
|
||||
['model_id', model._id],
|
||||
'AND',
|
||||
`vector <=> '[${promptVector}]' < ${similarity}`
|
||||
],
|
||||
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
|
||||
limit: 30
|
||||
limit: 20
|
||||
});
|
||||
|
||||
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
|
||||
@@ -90,32 +103,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
value: model.systemPrompt
|
||||
});
|
||||
} else {
|
||||
// 有匹配情况下,添加知识库内容。
|
||||
// 系统提示词过滤,最多 2000 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2000);
|
||||
// 有匹配情况下,system 添加知识库内容。
|
||||
// 系统提示词过滤,最多 2500 tokens
|
||||
const systemPrompt = systemPromptFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts: formatRedisPrompt,
|
||||
maxTokens: 2500
|
||||
});
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:当前时间为${dayjs().format(
|
||||
'YYYY/MM/DD HH:mm:ss'
|
||||
)}\n${systemPrompt}`
|
||||
value: `
|
||||
${model.systemPrompt}
|
||||
${
|
||||
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
|
||||
? `你只能从知识库选择内容回答.不在知识库内容拒绝回复`
|
||||
: ''
|
||||
}
|
||||
知识库内容为: 当前时间为${dayjs().format('YYYY/MM/DD HH:mm:ss')}\n${systemPrompt}'
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
// const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
const filterPrompts = openaiChatFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts,
|
||||
maxTokens: modelConstantsData.contextMaxToken - 500
|
||||
});
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
}));
|
||||
// console.log(formatPrompts);
|
||||
// console.log(filterPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
@@ -123,9 +140,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
temperature,
|
||||
messages: filterPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: true
|
||||
@@ -147,14 +163,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
chatResponse
|
||||
});
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: !userApiKey,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text: promptsContent + responseContent
|
||||
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
|
||||
});
|
||||
// jsonRes(res);
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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';
|
||||
import { countChatTokens } from '@/utils/tools';
|
||||
|
||||
/* 拆分数据成QA */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -34,7 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
splitText += chunk;
|
||||
const tokens = encode(splitText).length;
|
||||
const tokens = countChatTokens({ messages: [{ role: 'system', content: splitText }] });
|
||||
if (tokens >= 780) {
|
||||
dataItems.push({
|
||||
userId,
|
||||
|
||||
@@ -3,14 +3,14 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ModelStatusEnum, modelList, ChatModelNameEnum, ChatModelNameMap } from '@/constants/model';
|
||||
import { ModelStatusEnum, modelList, ModelNameEnum, Model2ChatModelMap } from '@/constants/model';
|
||||
import { Model } from '@/service/models/model';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { name, serviceModelName } = req.body as {
|
||||
name: string;
|
||||
serviceModelName: `${ChatModelNameEnum}`;
|
||||
serviceModelName: `${ModelNameEnum}`;
|
||||
};
|
||||
const { authorization } = req.headers;
|
||||
|
||||
@@ -47,9 +47,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
userId,
|
||||
status: ModelStatusEnum.running,
|
||||
service: {
|
||||
company: modelItem.serviceCompany,
|
||||
trainId: '',
|
||||
chatModel: ChatModelNameMap[modelItem.model], // 聊天时用的模型
|
||||
chatModel: Model2ChatModelMap[modelItem.model], // 聊天时用的模型
|
||||
modelName: modelItem.model // 最底层的模型,不会变,用于计费等核心操作
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,9 +36,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const where: any = [
|
||||
['user_id', userId],
|
||||
'AND',
|
||||
['model_id', modelId],
|
||||
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
|
||||
];
|
||||
|
||||
const searchRes = await PgClient.select<PgModelDataItemType>('modelData', {
|
||||
fields: ['id', 'q', 'a', 'status'],
|
||||
where: [['user_id', userId], 'AND', ['model_id', modelId]],
|
||||
where,
|
||||
order: [{ field: 'id', mode: 'DESC' }],
|
||||
limit: pageSize,
|
||||
offset: pageSize * (pageNum - 1)
|
||||
@@ -50,7 +57,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
pageSize,
|
||||
data: searchRes.rows,
|
||||
total: await PgClient.count('modelData', {
|
||||
where: [['user_id', userId], 'AND', ['model_id', modelId]]
|
||||
where
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -40,6 +40,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
// 去重
|
||||
const searchRes = await Promise.allSettled(
|
||||
data.map(async ([q, a]) => {
|
||||
if (!q || !a) {
|
||||
return Promise.reject('q/a为空');
|
||||
}
|
||||
try {
|
||||
q = q.replace(/\\n/g, '\n');
|
||||
a = a.replace(/\\n/g, '\n');
|
||||
|
||||
@@ -4,7 +4,7 @@ import { connectToDatabase, Model } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { ModelDataSchema } from '@/types/mongoSchema';
|
||||
import { generateVector } from '@/service/events/generateVector';
|
||||
import { connectPg, PgClient } from '@/service/pg';
|
||||
import { PgClient } from '@/service/pg';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
@@ -26,7 +26,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
const pg = await connectPg();
|
||||
|
||||
// 验证是否是该用户的 model
|
||||
const model = await Model.findOne({
|
||||
|
||||
@@ -2,14 +2,20 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { generateVector } from '@/service/events/generateVector';
|
||||
import { generateQA } from '@/service/events/generateQA';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { PgClient } from '@/service/pg';
|
||||
|
||||
/* 拆分数据成QA */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { text, modelId, prompt } = req.body as { text: string; modelId: string; prompt: string };
|
||||
if (!text || !modelId || !prompt) {
|
||||
const { chunks, modelId, prompt, mode } = req.body as {
|
||||
modelId: string;
|
||||
chunks: string[];
|
||||
prompt: string;
|
||||
mode: 'qa' | 'subsection';
|
||||
};
|
||||
if (!chunks || !modelId || !prompt) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
await connectToDatabase();
|
||||
@@ -28,46 +34,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error('无权操作该模型');
|
||||
}
|
||||
|
||||
const replaceText = text.replace(/\\n/g, '\n');
|
||||
if (mode === 'qa') {
|
||||
// 批量QA拆分插入数据
|
||||
await SplitData.create({
|
||||
userId,
|
||||
modelId,
|
||||
textList: chunks,
|
||||
prompt
|
||||
});
|
||||
|
||||
// 文本拆分成 chunk
|
||||
const chunks = replaceText.split('\n').filter((item) => item.trim());
|
||||
generateQA();
|
||||
} else if (mode === 'subsection') {
|
||||
// 插入记录
|
||||
await PgClient.insert('modelData', {
|
||||
values: chunks.map((item) => [
|
||||
{ key: 'user_id', value: userId },
|
||||
{ key: 'model_id', value: modelId },
|
||||
{ key: 'q', value: item },
|
||||
{ key: 'a', value: '' },
|
||||
{ key: 'status', value: 'waiting' }
|
||||
])
|
||||
});
|
||||
|
||||
const textList: string[] = [];
|
||||
let splitText = '';
|
||||
|
||||
/* 取 3k ~ 4K tokens 内容 */
|
||||
chunks.forEach((chunk) => {
|
||||
const tokens = encode(splitText + chunk).length;
|
||||
if (tokens >= 4000) {
|
||||
// 超过 4000,不要这块内容
|
||||
textList.push(splitText);
|
||||
splitText = chunk;
|
||||
} else if (tokens >= 3000) {
|
||||
// 超过 3000,取内容
|
||||
textList.push(splitText + chunk);
|
||||
splitText = '';
|
||||
} else {
|
||||
//没超过 3000,继续添加
|
||||
splitText += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
if (splitText) {
|
||||
textList.push(splitText);
|
||||
generateVector();
|
||||
}
|
||||
|
||||
// 批量插入数据
|
||||
await SplitData.create({
|
||||
userId,
|
||||
modelId,
|
||||
rawText: text,
|
||||
textList,
|
||||
prompt
|
||||
});
|
||||
|
||||
generateQA();
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Model } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getOpenAIApi } from '@/service/utils/auth';
|
||||
import { httpsAgent, openaiChatFilter, authOpenApiKey } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
@@ -74,17 +74,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
}));
|
||||
// console.log(formatPrompts);
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts,
|
||||
maxTokens: modelConstantsData.contextMaxToken - 500
|
||||
});
|
||||
|
||||
// console.log(filterPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
@@ -94,9 +91,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
// max_tokens: modelConstantsData.maxToken,
|
||||
messages: formatPrompts,
|
||||
temperature,
|
||||
messages: filterPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: isStream,
|
||||
@@ -128,14 +124,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
|
||||
// 只有使用平台的 key 才计费
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
text: promptsContent + responseContent
|
||||
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Model } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getOpenAIApi } from '@/service/utils/auth';
|
||||
import { authOpenApiKey } from '@/service/utils/tools';
|
||||
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { PassThrough } from 'stream';
|
||||
import {
|
||||
ChatModelNameEnum,
|
||||
ModelNameEnum,
|
||||
modelList,
|
||||
ChatModelNameMap,
|
||||
ModelVectorSearchModeMap
|
||||
ModelVectorSearchModeMap,
|
||||
ChatModelEnum
|
||||
} from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
@@ -60,9 +59,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error('找不到模型');
|
||||
}
|
||||
|
||||
const modelConstantsData = modelList.find(
|
||||
(item) => item.model === ChatModelNameEnum.VECTOR_GPT
|
||||
);
|
||||
const modelConstantsData = modelList.find((item) => item.model === ModelNameEnum.VECTOR_GPT);
|
||||
if (!modelConstantsData) {
|
||||
throw new Error('模型已下架');
|
||||
}
|
||||
@@ -74,7 +71,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// 请求一次 chatgpt 拆解需求
|
||||
const promptResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: ChatModelNameMap[ChatModelNameEnum.GPT35],
|
||||
model: ChatModelEnum.GPT35,
|
||||
temperature: 0,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
@@ -122,7 +119,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
]
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
timeout: 180000,
|
||||
httpsAgent: httpsAgent(true)
|
||||
}
|
||||
);
|
||||
@@ -163,30 +160,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
|
||||
|
||||
// textArr 筛选,最多 2500 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2500);
|
||||
// system 筛选,最多 2500 tokens
|
||||
const systemPrompt = systemPromptFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts: formatRedisPrompt,
|
||||
maxTokens: 2500
|
||||
});
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:${systemPrompt}`
|
||||
});
|
||||
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
|
||||
// 控制上下文 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts,
|
||||
maxTokens: modelConstantsData.contextMaxToken - 500
|
||||
});
|
||||
|
||||
// 格式化文本内容成 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);
|
||||
// console.log(filterPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
@@ -195,13 +188,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature,
|
||||
messages: formatPrompts,
|
||||
messages: filterPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: isStream
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
timeout: 180000,
|
||||
responseType: isStream ? 'stream' : 'json',
|
||||
httpsAgent: httpsAgent(true)
|
||||
}
|
||||
@@ -228,13 +221,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
console.log('laf gpt done. time:', `${(Date.now() - startTime) / 1000}s`);
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
text: promptsContent + responseContent
|
||||
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (step === 1) {
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Model } from '@/service/mongo';
|
||||
import { httpsAgent, systemPromptFilter, authOpenApiKey } from '@/service/utils/tools';
|
||||
import {
|
||||
httpsAgent,
|
||||
systemPromptFilter,
|
||||
authOpenApiKey,
|
||||
openaiChatFilter
|
||||
} from '@/service/utils/tools';
|
||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { PassThrough } from 'stream';
|
||||
import { modelList, ModelVectorSearchModeMap, ModelVectorSearchModeEnum } from '@/constants/model';
|
||||
import {
|
||||
modelList,
|
||||
ModelVectorSearchModeMap,
|
||||
ModelVectorSearchModeEnum,
|
||||
ModelDataStatusEnum
|
||||
} from '@/constants/model';
|
||||
import { pushChatBill } from '@/service/events/pushBill';
|
||||
import { openaiCreateEmbedding, gpt35StreamResponse } from '@/service/utils/openai';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -80,9 +90,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const similarity = ModelVectorSearchModeMap[model.search.mode]?.similarity || 0.22;
|
||||
const vectorSearch = await PgClient.select<{ id: string; q: string; a: string }>('modelData', {
|
||||
fields: ['id', 'q', 'a'],
|
||||
where: [['model_id', model._id], 'AND', `vector <=> '[${promptVector}]' < ${similarity}`],
|
||||
where: [
|
||||
['status', ModelDataStatusEnum.ready],
|
||||
'AND',
|
||||
['model_id', model._id],
|
||||
'AND',
|
||||
`vector <=> '[${promptVector}]' < ${similarity}`
|
||||
],
|
||||
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
|
||||
limit: 30
|
||||
limit: 20
|
||||
});
|
||||
|
||||
const formatRedisPrompt: string[] = vectorSearch.rows.map((item) => `${item.q}\n${item.a}`);
|
||||
@@ -110,28 +126,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
} else {
|
||||
// 有匹配或者低匹配度模式情况下,添加知识库内容。
|
||||
// 系统提示词过滤,最多 2000 tokens
|
||||
const systemPrompt = systemPromptFilter(formatRedisPrompt, 2000);
|
||||
// 系统提示词过滤,最多 2500 tokens
|
||||
const systemPrompt = systemPromptFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts: formatRedisPrompt,
|
||||
maxTokens: 2500
|
||||
});
|
||||
|
||||
prompts.unshift({
|
||||
obj: 'SYSTEM',
|
||||
value: `${model.systemPrompt} 知识库是最新的,下面是知识库内容:当前时间为${dayjs().format(
|
||||
'YYYY/MM/DD HH:mm:ss'
|
||||
)}\n${systemPrompt}`
|
||||
value: `
|
||||
${model.systemPrompt}
|
||||
${
|
||||
model.search.mode === ModelVectorSearchModeEnum.hightSimilarity
|
||||
? `你只能从知识库选择内容回答.不在知识库内容拒绝回复`
|
||||
: ''
|
||||
}
|
||||
知识库内容为: 当前时间为${dayjs().format('YYYY/MM/DD HH:mm:ss')}\n${systemPrompt}'
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = prompts.map((item: ChatItemType) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
}));
|
||||
// console.log(formatPrompts);
|
||||
// 控制在 tokens 数量,防止超出
|
||||
const filterPrompts = openaiChatFilter({
|
||||
model: model.service.chatModel,
|
||||
prompts,
|
||||
maxTokens: modelConstantsData.contextMaxToken - 500
|
||||
});
|
||||
|
||||
// console.log(filterPrompts);
|
||||
// 计算温度
|
||||
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
|
||||
|
||||
@@ -139,14 +162,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const chatResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: model.service.chatModel,
|
||||
temperature: temperature,
|
||||
messages: formatPrompts,
|
||||
temperature,
|
||||
messages: filterPrompts,
|
||||
frequency_penalty: 0.5, // 越大,重复内容越少
|
||||
presence_penalty: -0.5, // 越大,越容易出现新内容
|
||||
stream: isStream
|
||||
},
|
||||
{
|
||||
timeout: 120000,
|
||||
timeout: 180000,
|
||||
responseType: isStream ? 'stream' : 'json',
|
||||
httpsAgent: httpsAgent(true)
|
||||
}
|
||||
@@ -171,12 +194,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
}
|
||||
|
||||
const promptsContent = formatPrompts.map((item) => item.content).join('');
|
||||
pushChatBill({
|
||||
isPay: true,
|
||||
modelName: model.service.modelName,
|
||||
userId,
|
||||
text: promptsContent + responseContent
|
||||
messages: filterPrompts.concat({ role: 'assistant', content: responseContent })
|
||||
});
|
||||
// jsonRes(res);
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -2,9 +2,11 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, User, Pay } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import { PaySchema, UserModelSchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
import { getPayResult } from '@/service/utils/wxpay';
|
||||
import { pushPromotionRecord } from '@/service/utils/promotion';
|
||||
import { PRICE_SCALE } from '@/constants/common';
|
||||
|
||||
/* 校验支付结果 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -26,6 +28,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
throw new Error('订单已结算');
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
const user = await User.findById(userId);
|
||||
if (!user) {
|
||||
throw new Error('找不到用户');
|
||||
}
|
||||
// 获取邀请者
|
||||
let inviter: UserModelSchema | null = null;
|
||||
if (user.inviterId) {
|
||||
inviter = await User.findById(user.inviterId);
|
||||
}
|
||||
|
||||
const payRes = await getPayResult(payOrder.orderId);
|
||||
|
||||
// 校验下是否超过一天
|
||||
@@ -50,6 +63,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: payOrder.price }
|
||||
});
|
||||
// 推广佣金发放
|
||||
if (inviter) {
|
||||
pushPromotionRecord({
|
||||
userId: inviter._id,
|
||||
objUId: userId,
|
||||
type: 'invite',
|
||||
// amount 单位为元,需要除以缩放比例,最后乘比例
|
||||
amount: (payOrder.price / PRICE_SCALE) * inviter.promotion.rate * 0.01
|
||||
});
|
||||
}
|
||||
jsonRes(res, {
|
||||
data: '支付成功'
|
||||
});
|
||||
|
||||
@@ -15,7 +15,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
await connectToDatabase();
|
||||
|
||||
const records = await Pay.find({
|
||||
userId
|
||||
userId,
|
||||
status: { $ne: 'CLOSED' }
|
||||
}).sort({ createTime: -1 });
|
||||
|
||||
jsonRes(res, {
|
||||
|
||||
70
src/pages/api/user/promotion/getPromotionData.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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, User, promotionRecord } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
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 invitedAmount = await User.countDocuments({
|
||||
inviterId: userId
|
||||
});
|
||||
|
||||
// 计算累计合
|
||||
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
|
||||
{ $match: { userId: new mongoose.Types.ObjectId(userId), amount: { $gt: 0 } } },
|
||||
{
|
||||
$group: {
|
||||
_id: null, // 分组条件,这里使用 null 表示不分组
|
||||
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: false, // 排除 _id 字段
|
||||
totalAmount: true // 只返回 totalAmount 字段
|
||||
}
|
||||
}
|
||||
]);
|
||||
// 计算剩余金额
|
||||
const countResidue: { totalAmount: number }[] = await promotionRecord.aggregate([
|
||||
{ $match: { userId: new mongoose.Types.ObjectId(userId) } },
|
||||
{
|
||||
$group: {
|
||||
_id: null, // 分组条件,这里使用 null 表示不分组
|
||||
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: false, // 排除 _id 字段
|
||||
totalAmount: true // 只返回 totalAmount 字段
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
invitedAmount,
|
||||
historyAmount: countHistory[0]?.totalAmount || 0,
|
||||
residueAmount: countResidue[0]?.totalAmount || 0
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
48
src/pages/api/user/promotion/getPromotions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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, promotionRecord } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
|
||||
pageNum = +pageNum;
|
||||
pageSize = +pageSize;
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
const data = await promotionRecord
|
||||
.find(
|
||||
{
|
||||
userId
|
||||
},
|
||||
'_id createTime type amount'
|
||||
)
|
||||
.sort({ _id: -1 })
|
||||
.skip((pageNum - 1) * pageSize)
|
||||
.limit(pageSize);
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
pageNum,
|
||||
pageSize,
|
||||
data,
|
||||
total: await promotionRecord.countDocuments({
|
||||
userId
|
||||
})
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { AuthCode } from '@/service/models/authCode';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { sendPhoneCode, sendEmailCode } from '@/service/utils/sendNote';
|
||||
import { UserAuthTypeEnum } from '@/constants/common';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('123456789', 6);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
@@ -16,10 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += Math.floor(Math.random() * 10);
|
||||
}
|
||||
const code = nanoid();
|
||||
|
||||
// 判断 1 分钟内是否有重复数据
|
||||
const authCode = await AuthCode.findOne({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
@@ -11,28 +11,18 @@ import {
|
||||
Flex,
|
||||
Divider,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
useColorMode,
|
||||
useColorModeValue
|
||||
} from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getToken } from '@/utils/user';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import WxConcat from '@/components/WxConcat';
|
||||
import { useMarkdown } from '@/hooks/useMarkdown';
|
||||
import { getChatHistory, delChatHistoryById } from '@/api/chat';
|
||||
import { modelList } from '@/constants/model';
|
||||
|
||||
const SlideBar = ({
|
||||
chatId,
|
||||
@@ -42,32 +32,39 @@ const SlideBar = ({
|
||||
}: {
|
||||
chatId: string;
|
||||
modelId: string;
|
||||
resetChat: () => void;
|
||||
resetChat: (modelId?: string, chatId?: string) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { copyData } = useCopyData();
|
||||
const { myModels, getMyModels } = useUserStore();
|
||||
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
|
||||
const [hasReady, setHasReady] = useState(false);
|
||||
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
const { data: shareHint } = useMarkdown({ url: '/chatProblem.md' });
|
||||
const preChatId = useRef('chatId'); // 用于校验上一次chatId的情况,判断是否需要刷新历史记录
|
||||
|
||||
const { isSuccess } = useQuery(['init'], getMyModels, {
|
||||
const { isSuccess } = useQuery(['getMyModels'], getMyModels, {
|
||||
cacheTime: 5 * 60 * 1000
|
||||
});
|
||||
|
||||
const { data: chatHistory = [], mutate: loadChatHistory } = useMutation({
|
||||
mutationFn: getChatHistory
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setHasReady(true);
|
||||
}, []);
|
||||
if (chatId && preChatId.current === '') {
|
||||
loadChatHistory();
|
||||
}
|
||||
preChatId.current = chatId;
|
||||
}, [chatId, loadChatHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
loadChatHistory();
|
||||
}, [loadChatHistory]);
|
||||
|
||||
const RenderHistory = () => (
|
||||
<>
|
||||
{chatHistory.map((item) => (
|
||||
<Flex
|
||||
key={item.chatId}
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
p={3}
|
||||
borderRadius={'md'}
|
||||
@@ -78,15 +75,16 @@ const SlideBar = ({
|
||||
}}
|
||||
fontSize={'xs'}
|
||||
border={'1px solid transparent'}
|
||||
{...(item.chatId === chatId
|
||||
{...(item._id === chatId
|
||||
? {
|
||||
borderColor: 'rgba(255,255,255,0.5)',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
if (item.chatId === chatId) return;
|
||||
router.replace(`/chat?chatId=${item.chatId}`);
|
||||
if (item._id === chatId) return;
|
||||
preChatId.current = 'chatId';
|
||||
resetChat(item.modelId, item._id);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
@@ -100,12 +98,14 @@ const SlideBar = ({
|
||||
variant={'unstyled'}
|
||||
aria-label={'edit'}
|
||||
size={'xs'}
|
||||
onClick={(e) => {
|
||||
removeChatHistoryByWindowId(item.chatId);
|
||||
if (item.chatId === chatId) {
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
await delChatHistoryById(item._id);
|
||||
loadChatHistory();
|
||||
if (item._id === chatId) {
|
||||
resetChat();
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -155,58 +155,60 @@ const SlideBar = ({
|
||||
mb={4}
|
||||
mx={'auto'}
|
||||
leftIcon={<AddIcon />}
|
||||
onClick={resetChat}
|
||||
onClick={() => resetChat()}
|
||||
>
|
||||
新对话
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 我的模型 & 历史记录 折叠框*/}
|
||||
<Box flex={'1 0 0'} px={3} h={0} overflowY={'auto'}>
|
||||
<Accordion defaultIndex={[0]} allowMultiple>
|
||||
{isSuccess && (
|
||||
<AccordionItem borderTop={0} borderBottom={0}>
|
||||
<AccordionButton borderRadius={'md'} pl={1}>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
其他模型
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} px={0}>
|
||||
{myModels.map((item) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
p={3}
|
||||
borderRadius={'md'}
|
||||
mb={2}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
}}
|
||||
fontSize={'xs'}
|
||||
border={'1px solid transparent'}
|
||||
{...(item._id === modelId
|
||||
? {
|
||||
borderColor: 'rgba(255,255,255,0.5)',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
}
|
||||
: {})}
|
||||
onClick={async () => {
|
||||
if (item._id === modelId) return;
|
||||
router.replace(`/chat?chatId=${await getChatSiteId(item._id)}`);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<MyIcon name="model" mr={2} fill={'white'} w={'16px'} h={'16px'} />
|
||||
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<>
|
||||
<Box>
|
||||
{myModels.map((item) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'center'}
|
||||
p={3}
|
||||
borderRadius={'md'}
|
||||
mb={2}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
}}
|
||||
fontSize={'xs'}
|
||||
border={'1px solid transparent'}
|
||||
{...(item._id === modelId
|
||||
? {
|
||||
borderColor: 'rgba(255,255,255,0.5)',
|
||||
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||
}
|
||||
: {})}
|
||||
onClick={async () => {
|
||||
if (item._id === modelId) return;
|
||||
resetChat(item._id);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={
|
||||
modelList.find((model) => model.model === item.service.modelName)?.icon ||
|
||||
'model'
|
||||
}
|
||||
mr={2}
|
||||
color={'white'}
|
||||
w={'16px'}
|
||||
h={'16px'}
|
||||
/>
|
||||
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem borderTop={0} borderBottom={0}>
|
||||
<AccordionButton borderRadius={'md'} pl={1}>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
@@ -215,7 +217,7 @@ const SlideBar = ({
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={0} px={0}>
|
||||
{hasReady && <RenderHistory />}
|
||||
<RenderHistory />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@@ -230,12 +232,6 @@ const SlideBar = ({
|
||||
</>
|
||||
</RenderButton>
|
||||
|
||||
{/* <RenderButton onClick={onOpenShare}>
|
||||
<>
|
||||
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
|
||||
分享
|
||||
</>
|
||||
</RenderButton> */}
|
||||
<RenderButton onClick={() => router.push('/number/setting')}>
|
||||
<>
|
||||
<MyIcon name="pay" fill={'white'} w={'16px'} h={'16px'} mr={4} />
|
||||
@@ -260,49 +256,6 @@ const SlideBar = ({
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* 分享提示modal */}
|
||||
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
|
||||
<ModalOverlay />
|
||||
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||
<ModalHeader>分享对话</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Markdown source={shareHint} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button colorScheme="gray" variant={'outline'} mr={3} onClick={onCloseShare}>
|
||||
取消
|
||||
</Button>
|
||||
{getToken() && (
|
||||
<Button
|
||||
variant="outline"
|
||||
mr={3}
|
||||
onClick={async () => {
|
||||
copyData(
|
||||
`${location.origin}/chat?chatId=${await getChatSiteId(modelId)}`,
|
||||
'已复制分享链接'
|
||||
);
|
||||
onCloseShare();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
分享空白对话
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
|
||||
onCloseShare();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
分享聊天记录
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* wx 联系 */}
|
||||
{isOpenWx && <WxConcat onClose={onCloseWx} />}
|
||||
</Flex>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Image from 'next/image';
|
||||
import { getInitChatSiteInfo, getChatSiteId, delChatRecordByIndex, postSaveChat } from '@/api/chat';
|
||||
import { getInitChatSiteInfo, delChatRecordByIndex, postSaveChat } from '@/api/chat';
|
||||
import type { InitChatResponse } from '@/api/response/chat';
|
||||
import { ChatSiteItemType } from '@/types/chat';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import {
|
||||
Textarea,
|
||||
Box,
|
||||
@@ -21,14 +21,16 @@ import {
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChatModelNameEnum } from '@/constants/model';
|
||||
import { ModelNameEnum } from '@/constants/model';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import { streamFetch } from '@/api/fetch';
|
||||
import Icon from '@/components/Icon';
|
||||
import { modelList } from '@/constants/model';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { throttle } from 'lodash';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 5);
|
||||
|
||||
const SlideBar = dynamic(() => import('./components/SlideBar'));
|
||||
const Empty = dynamic(() => import('./components/Empty'));
|
||||
@@ -36,22 +38,26 @@ const Markdown = dynamic(() => import('@/components/Markdown'));
|
||||
|
||||
const textareaMinH = '22px';
|
||||
|
||||
export type ChatSiteItemType = {
|
||||
id: string;
|
||||
status: 'loading' | 'finish';
|
||||
} & ChatItemType;
|
||||
|
||||
interface ChatType extends InitChatResponse {
|
||||
history: ChatSiteItemType[];
|
||||
}
|
||||
|
||||
const Chat = ({ chatId }: { chatId: string }) => {
|
||||
const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const ChatBox = useRef<HTMLDivElement>(null);
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
// 中断请求
|
||||
const controller = useRef(new AbortController());
|
||||
const [chatData, setChatData] = useState<ChatType>({
|
||||
chatId: '',
|
||||
modelId: '',
|
||||
chatId,
|
||||
modelId,
|
||||
name: '',
|
||||
avatar: '',
|
||||
intro: '',
|
||||
@@ -59,6 +65,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
modelName: '',
|
||||
history: []
|
||||
}); // 聊天框整体数据
|
||||
|
||||
const [inputVal, setInputVal] = useState(''); // 输入的内容
|
||||
|
||||
const isChatting = useMemo(
|
||||
@@ -67,22 +74,34 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
);
|
||||
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
|
||||
|
||||
const { toast } = useToast();
|
||||
const { copyData } = useCopyData();
|
||||
const { isPc, media } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { pushChatHistory } = useChatStore();
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
ChatBox.current &&
|
||||
ChatBox.current.scrollTo({
|
||||
top: ChatBox.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100);
|
||||
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||
if (!ChatBox.current) return;
|
||||
ChatBox.current.scrollTo({
|
||||
top: ChatBox.current.scrollHeight,
|
||||
behavior
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const generatingMessage = useCallback(
|
||||
throttle(() => {
|
||||
if (!ChatBox.current) return;
|
||||
const isBottom =
|
||||
ChatBox.current.scrollTop + ChatBox.current.clientHeight + 150 >=
|
||||
ChatBox.current.scrollHeight;
|
||||
|
||||
isBottom && scrollToBottom('auto');
|
||||
}, 100),
|
||||
[]
|
||||
);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback((val: string) => {
|
||||
setInputVal(val);
|
||||
@@ -95,30 +114,90 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
// 重载对话
|
||||
const resetChat = useCallback(async () => {
|
||||
if (!chatData) return;
|
||||
try {
|
||||
router.replace(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '生成新对话失败',
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onCloseSlider();
|
||||
}, [chatData, onCloseSlider, router, toast]);
|
||||
// 获取对话信息
|
||||
const loadChatInfo = useCallback(
|
||||
async ({
|
||||
modelId,
|
||||
chatId,
|
||||
isLoading = false,
|
||||
isScroll = false
|
||||
}: {
|
||||
modelId: string;
|
||||
chatId: string;
|
||||
isLoading?: boolean;
|
||||
isScroll?: boolean;
|
||||
}) => {
|
||||
isLoading && setLoading(true);
|
||||
try {
|
||||
const res = await getInitChatSiteInfo(modelId, chatId);
|
||||
|
||||
setChatData({
|
||||
...res,
|
||||
history: res.history.map((item: any, i) => ({
|
||||
obj: item.obj,
|
||||
value: item.value,
|
||||
id: item.id || `${nanoid()}-${i}`,
|
||||
status: 'finish'
|
||||
}))
|
||||
});
|
||||
if (isScroll && res.history.length > 0) {
|
||||
setTimeout(() => {
|
||||
scrollToBottom('auto');
|
||||
}, 1200);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast({
|
||||
title: e?.message || '获取对话信息异常,请检查地址',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
duration: 5000
|
||||
});
|
||||
router.replace('/model/list');
|
||||
}
|
||||
setLoading(false);
|
||||
return null;
|
||||
},
|
||||
[router, scrollToBottom, setLoading, toast]
|
||||
);
|
||||
|
||||
// 重载新的对话
|
||||
const resetChat = useCallback(
|
||||
async (modelId = chatData.modelId, chatId = '') => {
|
||||
// 强制中断流
|
||||
controller.current?.abort();
|
||||
try {
|
||||
router.replace(`/chat?modelId=${modelId}&chatId=${chatId}`);
|
||||
loadChatInfo({
|
||||
modelId,
|
||||
chatId,
|
||||
isLoading: true,
|
||||
isScroll: true
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message || '生成新对话失败',
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onCloseSlider();
|
||||
},
|
||||
[chatData.modelId, loadChatInfo, onCloseSlider, router, toast]
|
||||
);
|
||||
|
||||
// gpt 对话
|
||||
const gptChatPrompt = useCallback(
|
||||
async (prompts: ChatSiteItemType) => {
|
||||
const urlMap: Record<string, string> = {
|
||||
[ChatModelNameEnum.GPT35]: '/api/chat/chatGpt',
|
||||
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt'
|
||||
[ModelNameEnum.GPT35]: '/api/chat/chatGpt',
|
||||
[ModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt'
|
||||
};
|
||||
|
||||
if (!urlMap[chatData.modelName]) return Promise.reject('找不到模型');
|
||||
|
||||
// create abort obj
|
||||
const abortSignal = new AbortController();
|
||||
controller.current = abortSignal;
|
||||
|
||||
const prompt = {
|
||||
obj: prompts.obj,
|
||||
value: prompts.value
|
||||
@@ -128,7 +207,8 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
url: urlMap[chatData.modelName],
|
||||
data: {
|
||||
prompt,
|
||||
chatId
|
||||
chatId,
|
||||
modelId
|
||||
},
|
||||
onMessage: (text: string) => {
|
||||
setChatData((state) => ({
|
||||
@@ -141,13 +221,16 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
};
|
||||
})
|
||||
}));
|
||||
generatingMessage();
|
||||
},
|
||||
abortSignal: controller.current
|
||||
abortSignal
|
||||
});
|
||||
|
||||
let id = '';
|
||||
// 保存对话信息
|
||||
try {
|
||||
await postSaveChat({
|
||||
id = await postSaveChat({
|
||||
modelId,
|
||||
chatId,
|
||||
prompts: [
|
||||
prompt,
|
||||
@@ -157,6 +240,9 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
if (id) {
|
||||
router.replace(`/chat?modelId=${modelId}&chatId=${id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: '对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
|
||||
@@ -169,6 +255,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
// 设置完成状态
|
||||
setChatData((state) => ({
|
||||
...state,
|
||||
chatId: id || state.chatId, // 如果有 Id,说明是新创建的对话
|
||||
history: state.history.map((item, index) => {
|
||||
if (index !== state.history.length - 1) return item;
|
||||
return {
|
||||
@@ -178,7 +265,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
})
|
||||
}));
|
||||
},
|
||||
[chatData.modelName, chatId, toast]
|
||||
[chatData.modelName, chatId, generatingMessage, modelId, router, toast]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -196,7 +283,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
// 去除空行
|
||||
const val = inputVal.trim().replace(/\n\s*/g, '\n');
|
||||
|
||||
if (!chatData?.modelId || !val) {
|
||||
if (!val) {
|
||||
toast({
|
||||
title: '内容为空',
|
||||
status: 'warning'
|
||||
@@ -207,11 +294,13 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...chatData.history,
|
||||
{
|
||||
id: nanoid(),
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
@@ -226,19 +315,12 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
scrollToBottom();
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
await gptChatPrompt(newChatList[newChatList.length - 2]);
|
||||
|
||||
// 如果是 Human 第一次发送,插入历史记录
|
||||
const humanChat = newChatList.filter((item) => item.obj === 'Human');
|
||||
if (humanChat.length === 1) {
|
||||
pushChatHistory({
|
||||
chatId,
|
||||
title: humanChat[0].value
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||
@@ -254,17 +336,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
history: newChatList.slice(0, newChatList.length - 2)
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
inputVal,
|
||||
chatData,
|
||||
isChatting,
|
||||
resetInputVal,
|
||||
scrollToBottom,
|
||||
toast,
|
||||
gptChatPrompt,
|
||||
pushChatHistory,
|
||||
chatId
|
||||
]);
|
||||
}, [isChatting, inputVal, chatData.history, resetInputVal, toast, scrollToBottom, gptChatPrompt]);
|
||||
|
||||
// 删除一句话
|
||||
const delChatRecord = useCallback(
|
||||
@@ -296,50 +368,23 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
);
|
||||
|
||||
// 初始化聊天框
|
||||
useQuery(
|
||||
['init', chatId],
|
||||
() => {
|
||||
setLoading(true);
|
||||
return getInitChatSiteInfo(chatId);
|
||||
},
|
||||
{
|
||||
onSuccess(res) {
|
||||
setChatData({
|
||||
...res,
|
||||
history: res.history.map((item) => ({
|
||||
...item,
|
||||
status: 'finish'
|
||||
}))
|
||||
});
|
||||
if (res.history.length > 0) {
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
onError(e: any) {
|
||||
toast({
|
||||
title: e?.message || '初始化异常,请检查地址',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
duration: 5000
|
||||
});
|
||||
router.push('/model/list');
|
||||
},
|
||||
onSettled() {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
useQuery(['init'], () =>
|
||||
loadChatInfo({
|
||||
modelId,
|
||||
chatId,
|
||||
isLoading: true,
|
||||
isScroll: true
|
||||
})
|
||||
);
|
||||
|
||||
// 更新流中断对象
|
||||
useEffect(() => {
|
||||
controller.current = new AbortController();
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
controller.current?.abort();
|
||||
};
|
||||
}, [chatId]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
h={'100%'}
|
||||
@@ -351,7 +396,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
<SlideBar
|
||||
resetChat={resetChat}
|
||||
chatId={chatId}
|
||||
modelId={chatData.modelId}
|
||||
modelId={modelId}
|
||||
onClose={onCloseSlider}
|
||||
/>
|
||||
</Box>
|
||||
@@ -382,7 +427,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
<SlideBar
|
||||
resetChat={resetChat}
|
||||
chatId={chatId}
|
||||
modelId={chatData.modelId}
|
||||
modelId={modelId}
|
||||
onClose={onCloseSlider}
|
||||
/>
|
||||
</DrawerContent>
|
||||
@@ -399,7 +444,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
<Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
|
||||
{chatData.history.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
key={item.id}
|
||||
py={media(9, 6)}
|
||||
px={media(4, 2)}
|
||||
backgroundColor={
|
||||
@@ -433,6 +478,29 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
|
||||
)}
|
||||
</Box>
|
||||
{isPc && (
|
||||
<Flex h={'100%'} flexDirection={'column'} ml={2} w={'14px'} height={'100%'}>
|
||||
<Box minH={'40px'} flex={1}>
|
||||
<MyIcon
|
||||
name="copy"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
color={'alphaBlack.400'}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</Box>
|
||||
<MyIcon
|
||||
name="delete"
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
color={'alphaBlack.400'}
|
||||
_hover={{
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={() => delChatRecord(index)}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
@@ -525,9 +593,10 @@ const Chat = ({ chatId }: { chatId: string }) => {
|
||||
export default Chat;
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const chatId = context?.query?.chatId || 'noid';
|
||||
const modelId = context?.query?.modelId || '';
|
||||
const chatId = context?.query?.chatId || '';
|
||||
|
||||
return {
|
||||
props: { chatId }
|
||||
props: { modelId, chatId }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
FormControl,
|
||||
FormErrorMessage
|
||||
} from '@chakra-ui/react';
|
||||
import { postData } from '@/api/data';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { DataType } from '@/types/data';
|
||||
import { DataTypeTextMap } from '@/constants/data';
|
||||
|
||||
export interface CreateDataProps {
|
||||
name: string;
|
||||
type: DataType;
|
||||
}
|
||||
|
||||
const CreateDataModal = ({
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const [inputVal, setInputVal] = useState('');
|
||||
const {
|
||||
getValues,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<CreateDataProps>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
type: 'abstract'
|
||||
}
|
||||
});
|
||||
|
||||
const { isLoading, mutate } = useMutation({
|
||||
mutationFn: (e: CreateDataProps) => postData(e),
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>创建数据集</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<FormControl mb={8} isInvalid={!!errors.name}>
|
||||
<Input
|
||||
placeholder="数据集名称"
|
||||
{...register('name', {
|
||||
required: '数据集名称不能为空'
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.name && errors.name.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Select placeholder="数据集类型" {...register('type', {})}>
|
||||
{Object.entries(DataTypeTextMap).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme={'gray'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button ml={3} isLoading={isLoading} onClick={handleSubmit(mutate as any)}>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDataModal;
|
||||
@@ -1,229 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Box,
|
||||
Flex,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useTabs } from '@/hooks/useTabs';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/file';
|
||||
import { postSplitData } from '@/api/data';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { modelList, ChatModelNameEnum } from '@/constants/model';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
|
||||
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
|
||||
|
||||
const ImportDataModal = ({
|
||||
dataId,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
dataId: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认提交生成任务?该任务无法终止!'
|
||||
});
|
||||
const { toast } = useToast();
|
||||
const { setIsLoading, Loading } = useLoading();
|
||||
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
|
||||
const { tabs, activeTab, setActiveTab } = useTabs({
|
||||
tabs: [
|
||||
{ id: 'text', label: '文本' },
|
||||
{ id: 'doc', label: '文件' }
|
||||
// { id: 'url', label: '链接' }
|
||||
]
|
||||
});
|
||||
|
||||
const [textInput, setTextInput] = useState('');
|
||||
const [fileText, setFileText] = useState('');
|
||||
|
||||
const { mutate: handleClickSubmit, isLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
let text = '';
|
||||
if (activeTab === 'text') {
|
||||
text = textInput;
|
||||
} else if (activeTab === 'doc') {
|
||||
text = fileText;
|
||||
} else if (activeTab === 'url') {
|
||||
}
|
||||
if (!text) return;
|
||||
return postSplitData(dataId, text);
|
||||
},
|
||||
onSuccess() {
|
||||
toast({
|
||||
title: '任务提交成功',
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
onSuccess();
|
||||
},
|
||||
onError(err: any) {
|
||||
toast({
|
||||
title: err?.message || '提交任务异常',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const fileTexts = (
|
||||
await Promise.all(
|
||||
e.map((file) => {
|
||||
// @ts-ignore
|
||||
const extension = file?.name?.split('.').pop().toLowerCase();
|
||||
switch (extension) {
|
||||
case 'txt':
|
||||
case 'md':
|
||||
return readTxtContent(file);
|
||||
case 'pdf':
|
||||
return readPdfContent(file);
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return readDocContent(file);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.join('\n')
|
||||
.replace(/\n+/g, '\n');
|
||||
setFileText(fileTexts);
|
||||
console.log(encode(fileTexts));
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
toast({
|
||||
title: typeof error === 'string' ? error : '解析文件失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setIsLoading, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent position={'relative'} maxW={['90vw', '800px']}>
|
||||
<ModalHeader>
|
||||
导入数据,生成QA
|
||||
<Box ml={2} as={'span'} fontSize={'sm'} color={'blackAlpha.600'}>
|
||||
{formatPrice(
|
||||
modelList.find((item) => item.model === ChatModelNameEnum.GPT35)?.price || 0,
|
||||
1000
|
||||
)}
|
||||
元/1K tokens
|
||||
</Box>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody display={'flex'}>
|
||||
<Box>
|
||||
{tabs.map((item) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
display={'block'}
|
||||
variant={activeTab === item.id ? 'solid' : 'outline'}
|
||||
_notLast={{
|
||||
mb: 3
|
||||
}}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box flex={'1 0 0'} w={0} ml={3} minH={'200px'}>
|
||||
{activeTab === 'text' && (
|
||||
<>
|
||||
<Textarea
|
||||
h={'100%'}
|
||||
maxLength={-1}
|
||||
value={textInput}
|
||||
placeholder={'请粘贴或输入需要处理的文本'}
|
||||
onChange={(e) => setTextInput(e.target.value)}
|
||||
/>
|
||||
<Box mt={2}>
|
||||
一共 {textInput.length} 个字,{encode(textInput).length} 个tokens
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'doc' && (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
p={2}
|
||||
h={'100%'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
border={'1px solid '}
|
||||
borderColor={'blackAlpha.200'}
|
||||
borderRadius={'md'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Button onClick={onOpen}>选择文件</Button>
|
||||
<Box mt={2}>支持 {fileExtension} 文件</Box>
|
||||
{fileText && (
|
||||
<>
|
||||
<Box mt={2}>
|
||||
一共 {fileText.length} 个字,{encode(fileText).length} 个tokens
|
||||
</Box>
|
||||
<Box
|
||||
maxH={'300px'}
|
||||
w={'100%'}
|
||||
overflow={'auto'}
|
||||
p={2}
|
||||
backgroundColor={'blackAlpha.50'}
|
||||
whiteSpace={'pre'}
|
||||
fontSize={'xs'}
|
||||
>
|
||||
{fileText}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme={'gray'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
ml={3}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!textInput && !fileText}
|
||||
onClick={openConfirm(handleClickSubmit)}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<Loading />
|
||||
</ModalContent>
|
||||
|
||||
<ConfirmChild />
|
||||
<File onSelect={onSelectFile} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportDataModal;
|
||||
@@ -1,67 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Card } from '@chakra-ui/react';
|
||||
import ScrollData from '@/components/ScrollData';
|
||||
import { getDataItems } from '@/api/data';
|
||||
import { usePaging } from '@/hooks/usePaging';
|
||||
import type { DataItemSchema } from '@/types/mongoSchema';
|
||||
|
||||
const DataDetail = ({ dataName, dataId }: { dataName: string; dataId: string }) => {
|
||||
const {
|
||||
nextPage,
|
||||
isLoadAll,
|
||||
requesting,
|
||||
data: dataItems
|
||||
} = usePaging<DataItemSchema>({
|
||||
api: getDataItems,
|
||||
pageSize: 10,
|
||||
params: {
|
||||
dataId
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Card py={4} h={'100%'} display={'flex'} flexDirection={'column'}>
|
||||
<Box px={6} fontSize={'xl'} fontWeight={'bold'}>
|
||||
{dataName} 结果
|
||||
</Box>
|
||||
<ScrollData
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
px={6}
|
||||
mt={3}
|
||||
isLoadAll={isLoadAll}
|
||||
requesting={requesting}
|
||||
nextPage={nextPage}
|
||||
fontSize={'xs'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
>
|
||||
{dataItems.map((item) => (
|
||||
<Box key={item._id}>
|
||||
{item.result.map((result, i) => (
|
||||
<Box key={i} mb={3}>
|
||||
{item.type === 'QA' && (
|
||||
<>
|
||||
<Box fontWeight={'bold'}>Q: {result.q}</Box>
|
||||
<Box>A: {result.a}</Box>
|
||||
</>
|
||||
)}
|
||||
{item.type === 'abstract' && <Box fontSize={'sm'}>{result.abstract}</Box>}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</ScrollData>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataDetail;
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: {
|
||||
dataName: context.query?.dataName || '',
|
||||
dataId: context.query?.dataId || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
useDisclosure,
|
||||
Input,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { getDataList, updateDataName, delData, getDataItems } from '@/api/data';
|
||||
import type { DataListItem } from '@/types/data';
|
||||
import dayjs from 'dayjs';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { DataItemSchema } from '@/types/mongoSchema';
|
||||
import { DataTypeTextMap } from '@/constants/data';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
const nanoid = customAlphabet('.,', 1);
|
||||
|
||||
const CreateDataModal = dynamic(() => import('./components/CreateDataModal'));
|
||||
const ImportDataModal = dynamic(() => import('./components/ImportDataModal'));
|
||||
|
||||
export type ExportDataType = 'jsonl' | 'txt';
|
||||
|
||||
const DataList = () => {
|
||||
const router = useRouter();
|
||||
const [ImportDataId, setImportDataId] = useState<string>();
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '删除数据集,将删除里面的所有内容,请确认!'
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenCreateDataModal,
|
||||
onOpen: onOpenCreateDataModal,
|
||||
onClose: onCloseCreateDataModal
|
||||
} = useDisclosure();
|
||||
|
||||
const { data: dataList = [], refetch } = useQuery(['getDataList'], getDataList, {
|
||||
refetchInterval: 10000
|
||||
});
|
||||
|
||||
const { mutate: handleDelData, isLoading: isDeleting } = useRequest({
|
||||
mutationFn: (dataId: string) => delData(dataId),
|
||||
successToast: '删除数据集成功',
|
||||
errorToast: '删除数据集异常',
|
||||
onSuccess() {
|
||||
refetch();
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: handleExportData, isLoading: isExporting } = useRequest({
|
||||
mutationFn: async ({ data, type }: { data: DataListItem; type: ExportDataType }) => ({
|
||||
type,
|
||||
data: await getDataItems({ dataId: data._id, pageNum: 1, pageSize: data.totalData }).then(
|
||||
(res) => res.data
|
||||
)
|
||||
}),
|
||||
successToast: '导出数据集成功',
|
||||
errorToast: '导出数据集异常',
|
||||
onSuccess(res: { type: ExportDataType; data: DataItemSchema[] }) {
|
||||
// 合并数据
|
||||
const data = res.data.map((item) => item.result).flat();
|
||||
let text = '';
|
||||
// 生成 jsonl
|
||||
data.forEach((item) => {
|
||||
if (res.type === 'jsonl' && item.q && item.a) {
|
||||
const result = JSON.stringify({
|
||||
prompt: `${item.q.toLocaleLowerCase()}${nanoid()}</s>`,
|
||||
completion: ` ${item.a}###`
|
||||
});
|
||||
text += `${result}\n`;
|
||||
} else if (res.type === 'txt' && item.abstract) {
|
||||
text += `${item.abstract}\n`;
|
||||
}
|
||||
});
|
||||
// 去掉最后一个 \n
|
||||
text = text.substring(0, text.length - 1);
|
||||
|
||||
// 导出为文件
|
||||
const blob = new Blob([text], { type: 'application/json;charset=utf-8' });
|
||||
|
||||
// 创建下载链接
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
downloadLink.download = `data.${res.type}`;
|
||||
|
||||
// 添加链接到页面并触发下载
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box display={['block', 'flex']} flexDirection={'column'} h={'100%'}>
|
||||
<Card px={6} py={4}>
|
||||
<Flex>
|
||||
<Box flex={1} mr={1}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
训练数据管理
|
||||
</Box>
|
||||
<Box fontSize={'xs'} color={'blackAlpha.600'}>
|
||||
允许你将任意文本数据拆分成 QA 形式,或者进行文本摘要总结。
|
||||
</Box>
|
||||
</Box>
|
||||
<Button variant={'outline'} onClick={onOpenCreateDataModal}>
|
||||
创建数据集
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
{/* 数据表 */}
|
||||
<TableContainer
|
||||
mt={3}
|
||||
flex={'1 0 0'}
|
||||
h={['auto', '0']}
|
||||
overflowY={'auto'}
|
||||
px={6}
|
||||
py={4}
|
||||
backgroundColor={'white'}
|
||||
borderRadius={'md'}
|
||||
boxShadow={'base'}
|
||||
>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>集合名</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>创建时间</Th>
|
||||
<Th>训练中 / 总数据</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{dataList.map((item, i) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
<Input
|
||||
minW={'150px'}
|
||||
placeholder="请输入数据集名称"
|
||||
defaultValue={item.name}
|
||||
size={'sm'}
|
||||
onBlur={(e) => {
|
||||
if (!e.target.value || e.target.value === item.name) return;
|
||||
updateDataName(item._id, e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{DataTypeTextMap[item.type || 'QA']}</Td>
|
||||
<Td>{dayjs(item.createTime).format('YYYY/MM/DD HH:mm')}</Td>
|
||||
<Td>
|
||||
{item.trainingData} / {item.totalData}
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'outline'}
|
||||
colorScheme={'gray'}
|
||||
mr={2}
|
||||
onClick={() =>
|
||||
router.push(`/data/detail?dataId=${item._id}&dataName=${item.name}`)
|
||||
}
|
||||
>
|
||||
详细
|
||||
</Button>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'outline'}
|
||||
mr={2}
|
||||
onClick={() => setImportDataId(item._id)}
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
{/* <Menu>
|
||||
<MenuButton as={Button} mr={2} size={'sm'} isLoading={isExporting}>
|
||||
导出
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{item.type === 'QA' && (
|
||||
<MenuItem onClick={() => handleExportData({ data: item, type: 'jsonl' })}>
|
||||
jsonl
|
||||
</MenuItem>
|
||||
)}
|
||||
{item.type === 'abstract' && (
|
||||
<MenuItem onClick={() => handleExportData({ data: item, type: 'txt' })}>
|
||||
txt
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Menu> */}
|
||||
|
||||
<Button
|
||||
size={'sm'}
|
||||
colorScheme={'red'}
|
||||
isLoading={isDeleting}
|
||||
onClick={openConfirm(() => handleDelData(item._id))}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{ImportDataId && (
|
||||
<ImportDataModal
|
||||
dataId={ImportDataId}
|
||||
onClose={() => setImportDataId(undefined)}
|
||||
onSuccess={refetch}
|
||||
/>
|
||||
)}
|
||||
{isOpenCreateDataModal && (
|
||||
<CreateDataModal onClose={onCloseCreateDataModal} onSuccess={refetch} />
|
||||
)}
|
||||
<ConfirmChild />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataList;
|
||||
@@ -1,15 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Card } from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Card, Box, Link } from '@chakra-ui/react';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useMarkdown } from '@/hooks/useMarkdown';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const Home = () => {
|
||||
const { inviterId } = useRouter().query as { inviterId: string };
|
||||
const { data } = useMarkdown({ url: '/intro.md' });
|
||||
|
||||
useEffect(() => {
|
||||
if (inviterId) {
|
||||
localStorage.setItem('inviterId', inviterId);
|
||||
}
|
||||
}, [inviterId]);
|
||||
|
||||
return (
|
||||
<Card p={5} lineHeight={2}>
|
||||
<Markdown source={data} isChatting={false} />
|
||||
</Card>
|
||||
<>
|
||||
<Card p={5} lineHeight={2}>
|
||||
<Markdown source={data} isChatting={false} />
|
||||
</Card>
|
||||
|
||||
<Card p={5} mt={4} textAlign={'center'}>
|
||||
<Box>
|
||||
{/* <Link href="https://beian.miit.gov.cn/" target="_blank">
|
||||
浙B2-20080101
|
||||
</Link> */}
|
||||
</Box>
|
||||
<Box>Made by FastGpt Team.</Box>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
username,
|
||||
code,
|
||||
password,
|
||||
inviterId
|
||||
inviterId: inviterId || localStorage.getItem('inviterId') || ''
|
||||
})
|
||||
);
|
||||
toast({
|
||||
|
||||
@@ -122,9 +122,9 @@ const InputDataModal = ({
|
||||
<Box h={'30px'}>问题</Box>
|
||||
<Textarea
|
||||
placeholder={
|
||||
'相关问题,可以输入多个问法, 最多500字。例如:\n1. laf 是什么?\n2. laf 可以做什么?\n3. laf怎么用'
|
||||
'相关问题,可以输入多个问法, 最多 1000 字。例如:\n1. laf 是什么?\n2. laf 可以做什么?\n3. laf怎么用'
|
||||
}
|
||||
maxLength={500}
|
||||
maxLength={1000}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
{...register(`q`, {
|
||||
@@ -136,9 +136,9 @@ const InputDataModal = ({
|
||||
<Box h={'30px'}>知识点</Box>
|
||||
<Textarea
|
||||
placeholder={
|
||||
'知识点,最多1000字。请保持主语的完整性,缺少主语会导致效果不佳。例如:\n1. laf是一个云函数开发平台。\n2. laf 什么都能做。\n3. 下面是使用 laf 的例子: ……'
|
||||
'知识点,最多 2000 字。例如:\n1. laf是一个云函数开发平台。\n2. laf 什么都能做。\n3. 下面是使用 laf 的例子: ……'
|
||||
}
|
||||
maxLength={1000}
|
||||
maxLength={2000}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
{...register(`a`, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TableContainer,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
MenuItem,
|
||||
Input
|
||||
} from '@chakra-ui/react';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import type { ModelDataItemType } from '@/types/model';
|
||||
import { ModelDataStatusMap } from '@/constants/model';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
@@ -33,19 +33,23 @@ import { useLoading } from '@/hooks/useLoading';
|
||||
import { fileDownload } from '@/utils/file';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import type { FormData as InputDataType } from './InputDataModal';
|
||||
import Papa from 'papaparse';
|
||||
import InputModal, { FormData as InputDataType } from './InputDataModal';
|
||||
|
||||
const InputModel = dynamic(() => import('./InputDataModal'));
|
||||
const SelectFileModel = dynamic(() => import('./SelectFileModal'));
|
||||
const SelectUrlModel = dynamic(() => import('./SelectUrlModal'));
|
||||
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
|
||||
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
||||
|
||||
let lastSearch = '';
|
||||
|
||||
const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
const ModelDataCard = ({ modelId }: { modelId: string }) => {
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const lastSearch = useRef('');
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const tdStyles = useRef<BoxProps>({
|
||||
fontSize: 'xs',
|
||||
maxW: '500px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
maxH: '250px',
|
||||
overflowY: 'auto'
|
||||
});
|
||||
const {
|
||||
data: modelDataList,
|
||||
isLoading,
|
||||
@@ -57,7 +61,7 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
api: getModelDataList,
|
||||
pageSize: 10,
|
||||
params: {
|
||||
modelId: model._id,
|
||||
modelId,
|
||||
searchText
|
||||
}
|
||||
});
|
||||
@@ -69,19 +73,20 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
onOpen: onOpenSelectFileModal,
|
||||
onClose: onCloseSelectFileModal
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenSelectUrlModal,
|
||||
onOpen: onOpenSelectUrlModal,
|
||||
onClose: onCloseSelectUrlModal
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenSelectCsvModal,
|
||||
onOpen: onOpenSelectCsvModal,
|
||||
onClose: onCloseSelectCsvModal
|
||||
} = useDisclosure();
|
||||
|
||||
const { data: splitDataLen, refetch } = useQuery(['getModelSplitDataList'], () =>
|
||||
getModelSplitDataListLen(model._id)
|
||||
const { data: splitDataLen = 0, refetch } = useQuery(
|
||||
['getModelSplitDataList'],
|
||||
() => getModelSplitDataListLen(modelId),
|
||||
{
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const refetchData = useCallback(
|
||||
@@ -93,8 +98,8 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
);
|
||||
|
||||
// 获取所有的数据,并导出 json
|
||||
const { mutate: onclickExport, isLoading: isLoadingExport } = useMutation({
|
||||
mutationFn: () => getExportDataList(model._id),
|
||||
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
|
||||
mutationFn: () => getExportDataList(modelId),
|
||||
onSuccess(res) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -111,6 +116,9 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
error;
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,7 +149,7 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
>
|
||||
导出
|
||||
</Button>
|
||||
<Menu>
|
||||
<Menu autoSelect={false}>
|
||||
<MenuButton as={Button} size={'sm'}>
|
||||
导入
|
||||
</MenuButton>
|
||||
@@ -156,18 +164,14 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
>
|
||||
手动输入
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectFileModal}>文本/文件 QA 拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectUrlModal}>网站内容 QA 拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectFileModal}>文本/文件拆分</MenuItem>
|
||||
<MenuItem onClick={onOpenSelectCsvModal}>csv 问答对导入</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
<Flex mt={4}>
|
||||
{/* 拆分数据提示 */}
|
||||
{!!(splitDataLen && splitDataLen > 0) && (
|
||||
<Box fontSize={'xs'}>{splitDataLen}条数据正在拆分...</Box>
|
||||
)}
|
||||
<Box flex={1}></Box>
|
||||
{splitDataLen > 0 && <Box fontSize={'xs'}>{splitDataLen}条数据正在拆分...</Box>}
|
||||
<Box flex={1} />
|
||||
<Input
|
||||
maxW={'240px'}
|
||||
size={'sm'}
|
||||
@@ -175,15 +179,15 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
placeholder="搜索相关问题和答案,回车确认"
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (searchText === lastSearch) return;
|
||||
if (searchText === lastSearch.current) return;
|
||||
getData(1);
|
||||
lastSearch = searchText;
|
||||
lastSearch.current = searchText;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (searchText === lastSearch) return;
|
||||
if (searchText === lastSearch.current) return;
|
||||
if (e.key === 'Enter') {
|
||||
getData(1);
|
||||
lastSearch = searchText;
|
||||
lastSearch.current = searchText;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -191,33 +195,23 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
|
||||
<Box mt={4}>
|
||||
<TableContainer minH={'500px'}>
|
||||
<Table variant={'simple'}>
|
||||
<Table variant={'simple'} w={'100%'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Question</Th>
|
||||
<Th>Text</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>{'匹配内容(问题)'}</Th>
|
||||
<Th>对应答案</Th>
|
||||
<Th>状态</Th>
|
||||
<Th>操作</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{modelDataList.map((item) => (
|
||||
<Tr key={item.id}>
|
||||
<Td minW={'200px'}>
|
||||
<Box fontSize={'xs'} whiteSpace={'pre-wrap'}>
|
||||
{item.q}
|
||||
</Box>
|
||||
<Td>
|
||||
<Box {...tdStyles.current}>{item.q}</Box>
|
||||
</Td>
|
||||
<Td minW={'200px'}>
|
||||
<Box
|
||||
w={'100%'}
|
||||
fontSize={'xs'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
maxH={'250px'}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{item.a}
|
||||
</Box>
|
||||
<Td>
|
||||
<Box {...tdStyles.current}>{item.a || '-'}</Box>
|
||||
</Td>
|
||||
<Td>{ModelDataStatusMap[item.status]}</Td>
|
||||
<Td>
|
||||
@@ -259,33 +253,22 @@ const ModelDataCard = ({ model }: { model: ModelSchema }) => {
|
||||
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
{editInputData !== undefined && (
|
||||
<InputModel
|
||||
modelId={model._id}
|
||||
<InputModal
|
||||
modelId={modelId}
|
||||
defaultValues={editInputData}
|
||||
onClose={() => setEditInputData(undefined)}
|
||||
onSuccess={refetchData}
|
||||
/>
|
||||
)}
|
||||
{isOpenSelectFileModal && (
|
||||
<SelectFileModel
|
||||
modelId={model._id}
|
||||
<SelectFileModal
|
||||
modelId={modelId}
|
||||
onClose={onCloseSelectFileModal}
|
||||
onSuccess={refetchData}
|
||||
/>
|
||||
)}
|
||||
{isOpenSelectUrlModal && (
|
||||
<SelectUrlModel
|
||||
modelId={model._id}
|
||||
onClose={onCloseSelectUrlModal}
|
||||
onSuccess={refetchData}
|
||||
/>
|
||||
)}
|
||||
{isOpenSelectCsvModal && (
|
||||
<SelectCsvModal
|
||||
modelId={model._id}
|
||||
onClose={onCloseSelectCsvModal}
|
||||
onSuccess={refetchData}
|
||||
/>
|
||||
<SelectCsvModal modelId={modelId} onClose={onCloseSelectCsvModal} onSuccess={refetchData} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -14,15 +14,32 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/file';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { postModelDataSplitData } from '@/api/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import Radio from '@/components/Radio';
|
||||
import { splitText } from '@/utils/file';
|
||||
import { countChatTokens } from '@/utils/tools';
|
||||
|
||||
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
|
||||
|
||||
const modeMap = {
|
||||
qa: {
|
||||
maxLen: 2800,
|
||||
slideLen: 800,
|
||||
price: 4,
|
||||
isPrompt: true
|
||||
},
|
||||
subsection: {
|
||||
maxLen: 800,
|
||||
slideLen: 300,
|
||||
price: 0.4,
|
||||
isPrompt: false
|
||||
}
|
||||
};
|
||||
|
||||
const SelectFileModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
@@ -36,38 +53,45 @@ const SelectFileModal = ({
|
||||
const { toast } = useToast();
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
|
||||
const [fileText, setFileText] = useState('');
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,任务讲被终止。'
|
||||
const [mode, setMode] = useState<'qa' | 'subsection'>('qa');
|
||||
const [fileTextArr, setFileTextArr] = useState<string[]>(['']);
|
||||
const [splitRes, setSplitRes] = useState<{ tokens: number; chunks: string[] }>({
|
||||
tokens: 0,
|
||||
chunks: []
|
||||
});
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: `确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,未完成的任务会被直接清除。一共 ${
|
||||
splitRes.chunks.length
|
||||
} 组,大约 ${splitRes.tokens} 个tokens, 约 ${formatPrice(
|
||||
splitRes.tokens * modeMap[mode].price
|
||||
)} 元`
|
||||
});
|
||||
|
||||
const fileText = useMemo(() => fileTextArr.join(''), [fileTextArr]);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
setSelecting(true);
|
||||
try {
|
||||
const fileTexts = (
|
||||
await Promise.all(
|
||||
e.map((file) => {
|
||||
// @ts-ignore
|
||||
const extension = file?.name?.split('.').pop().toLowerCase();
|
||||
switch (extension) {
|
||||
case 'txt':
|
||||
case 'md':
|
||||
return readTxtContent(file);
|
||||
case 'pdf':
|
||||
return readPdfContent(file);
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return readDocContent(file);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.join(' ')
|
||||
.replace(/(\\n|\n)+/g, '\n');
|
||||
setFileText(fileTexts);
|
||||
const fileTexts = await Promise.all(
|
||||
e.map((file) => {
|
||||
// @ts-ignore
|
||||
const extension = file?.name?.split('.').pop().toLowerCase();
|
||||
switch (extension) {
|
||||
case 'txt':
|
||||
case 'md':
|
||||
return readTxtContent(file);
|
||||
case 'pdf':
|
||||
return readPdfContent(file);
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return readDocContent(file);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
);
|
||||
setFileTextArr(fileTexts);
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
toast({
|
||||
@@ -77,16 +101,18 @@ const SelectFileModal = ({
|
||||
}
|
||||
setSelecting(false);
|
||||
},
|
||||
[setSelecting, toast]
|
||||
[toast]
|
||||
);
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!fileText) return;
|
||||
if (splitRes.chunks.length === 0) return;
|
||||
|
||||
await postModelDataSplitData({
|
||||
modelId,
|
||||
text: fileText.replace(/\\n/g, '\n').replace(/\n+/g, '\n'),
|
||||
prompt: `下面是${prompt || '一段长文本'}`
|
||||
chunks: splitRes.chunks,
|
||||
prompt: `下面是"${prompt || '一段长文本'}"`,
|
||||
mode
|
||||
});
|
||||
toast({
|
||||
title: '导入数据成功,需要一段拆解和训练',
|
||||
@@ -103,64 +129,109 @@ const SelectFileModal = ({
|
||||
}
|
||||
});
|
||||
|
||||
const onclickImport = useCallback(() => {
|
||||
const chunks = fileTextArr
|
||||
.map((item) =>
|
||||
splitText({
|
||||
text: item,
|
||||
...modeMap[mode]
|
||||
})
|
||||
)
|
||||
.flat();
|
||||
// count tokens
|
||||
const tokens = chunks.map((item) =>
|
||||
countChatTokens({ messages: [{ role: 'system', content: item }] })
|
||||
);
|
||||
|
||||
setSplitRes({
|
||||
tokens: tokens.reduce((sum, item) => sum + item, 0),
|
||||
chunks
|
||||
});
|
||||
|
||||
openConfirm(mutate)();
|
||||
}, [fileTextArr, mode, mutate, openConfirm]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'min(900px, 90vw)'} m={0} position={'relative'} h={'90vh'}>
|
||||
<ModalContent maxW={'min(1000px, 90vw)'} m={0} position={'relative'} h={'90vh'}>
|
||||
<ModalHeader>文件导入</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
p={4}
|
||||
p={0}
|
||||
h={'100%'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Button isLoading={selecting} onClick={onOpen}>
|
||||
选择文件
|
||||
</Button>
|
||||
<Box mt={2} maxW={['100%', '70%']}>
|
||||
<Box mt={2} px={5} maxW={['100%', '70%']} textAlign={'justify'} color={'blackAlpha.600'}>
|
||||
支持 {fileExtension} 文件。模型会自动对文本进行 QA 拆分,需要较长训练时间,拆分需要消耗
|
||||
tokens,账号余额不足时,未拆分的数据会被删除。
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
一共 {encode(fileText).length} 个tokens,大约 {formatPrice(encode(fileText).length * 3)}
|
||||
元
|
||||
</Box>
|
||||
<Flex w={'100%'} alignItems={'center'} my={4}>
|
||||
<Box flex={'0 0 auto'} mr={2}>
|
||||
下面是
|
||||
</Box>
|
||||
<Input
|
||||
placeholder="提示词,例如: Laf的介绍/关于gpt4的论文/一段长文本"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
size={'sm'}
|
||||
{/* 拆分模式 */}
|
||||
<Flex w={'100%'} px={5} alignItems={'center'} mt={4}>
|
||||
<Box flex={'0 0 70px'}>分段模式:</Box>
|
||||
<Radio
|
||||
ml={3}
|
||||
list={[
|
||||
{ label: 'QA拆分', value: 'qa' },
|
||||
{ label: '直接分段', value: 'subsection' }
|
||||
]}
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e as 'subsection' | 'qa')}
|
||||
/>
|
||||
</Flex>
|
||||
<Textarea
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
w={'100%'}
|
||||
placeholder="文件内容"
|
||||
maxLength={-1}
|
||||
resize={'none'}
|
||||
fontSize={'xs'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
value={fileText}
|
||||
onChange={(e) => setFileText(e.target.value)}
|
||||
/>
|
||||
{/* 内容介绍 */}
|
||||
{modeMap[mode].isPrompt && (
|
||||
<Flex w={'100%'} px={5} alignItems={'center'} mt={4}>
|
||||
<Box flex={'0 0 70px'} mr={2}>
|
||||
下面是
|
||||
</Box>
|
||||
<Input
|
||||
placeholder="提示词,例如: Laf的介绍/关于gpt4的论文/一段长文本"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
size={'sm'}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{/* 文本内容 */}
|
||||
<Box flex={'1 0 0'} px={5} h={0} w={'100%'} overflowY={'auto'} mt={4}>
|
||||
{fileTextArr.map((item, i) => (
|
||||
<Box key={i} mb={5}>
|
||||
<Box mb={1}>文本{i + 1}</Box>
|
||||
<Textarea
|
||||
placeholder="文件内容"
|
||||
maxLength={-1}
|
||||
rows={10}
|
||||
fontSize={'xs'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
value={item}
|
||||
onChange={(e) => {
|
||||
setFileTextArr([
|
||||
...fileTextArr.slice(0, i),
|
||||
e.target.value,
|
||||
...fileTextArr.slice(i + 1)
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<Flex px={6} pt={2} pb={4}>
|
||||
<Button isLoading={selecting} onClick={onOpen}>
|
||||
选择文件
|
||||
</Button>
|
||||
<Box flex={1}></Box>
|
||||
<Button variant={'outline'} mr={3} onClick={onClose}>
|
||||
<Button variant={'outline'} colorScheme={'gray'} mr={3} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={isLoading} isDisabled={fileText === ''} onClick={openConfirm(mutate)}>
|
||||
<Button isLoading={isLoading} isDisabled={fileText === ''} onClick={onclickImport}>
|
||||
确认导入
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -13,15 +13,11 @@ import {
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { postModelDataSplitData, getWebContent } from '@/api/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||
|
||||
const SelectUrlModal = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
@@ -44,8 +40,9 @@ const SelectUrlModal = ({
|
||||
if (!webText) return;
|
||||
await postModelDataSplitData({
|
||||
modelId,
|
||||
text: webText,
|
||||
prompt: `下面是${prompt || '一段长文本'}`
|
||||
chunks: [],
|
||||
prompt: `下面是"${prompt || '一段长文本'}"`,
|
||||
mode: 'qa'
|
||||
});
|
||||
toast({
|
||||
title: '导入数据成功,需要一段拆解和训练',
|
||||
@@ -89,7 +86,7 @@ const SelectUrlModal = ({
|
||||
<Modal isOpen={true} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'min(900px, 90vw)'} m={0} position={'relative'} h={'90vh'}>
|
||||
<ModalHeader>网站地址导入</ModalHeader>
|
||||
<ModalHeader>静态网站内容导入</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody
|
||||
@@ -102,12 +99,9 @@ const SelectUrlModal = ({
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Box mt={2} maxW={['100%', '70%']}>
|
||||
根据网站地址,获取网站文本内容(请注意获取后的内容,不是每个网站内容都能获取到的)。模型会对文本进行
|
||||
根据网站地址,获取网站文本内容(请注意仅能获取静态网站文本,注意看下获取后的内容是否正确)。模型会对文本进行
|
||||
QA 拆分,需要较长训练时间,拆分需要消耗 tokens,账号余额不足时,未拆分的数据会被删除。
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
一共 {encode(webText).length} 个tokens,大约 {formatPrice(encode(webText).length * 3)}元
|
||||
</Box>
|
||||
<Flex w={'100%'} alignItems={'center'} my={4}>
|
||||
<Box flex={'0 0 70px'}>网站地址</Box>
|
||||
<Input
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
|
||||
import React, { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getModelById, delModelById, putModelTrainingStatus, putModelById } from '@/api/model';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
@@ -9,16 +8,16 @@ import { useForm } from 'react-hook-form';
|
||||
import { formatModelStatus, ModelStatusEnum, modelList, defaultModel } from '@/constants/model';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import ModelEditForm from './components/ModelEditForm';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'));
|
||||
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
|
||||
|
||||
const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc, media } = useScreen();
|
||||
const { isPc } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
const [model, setModel] = useState<ModelSchema>(defaultModel);
|
||||
@@ -70,44 +69,12 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
const handlePreviewChat = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const chatId = await getChatSiteId(model._id);
|
||||
|
||||
router.push(`/chat?chatId=${chatId}`);
|
||||
router.push(`/chat?modelId=${modelId}`);
|
||||
} catch (err) {
|
||||
console.log('error->', err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, model, router]);
|
||||
|
||||
/* 上传数据集,触发微调 */
|
||||
// const startTraining = useCallback(
|
||||
// async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// if (!modelId || !e.target.files || e.target.files?.length === 0) return;
|
||||
// setLoading(true);
|
||||
// try {
|
||||
// const file = e.target.files[0];
|
||||
// const formData = new FormData();
|
||||
// formData.append('file', file);
|
||||
// await postTrainModel(modelId, formData);
|
||||
|
||||
// toast({
|
||||
// title: '开始训练...',
|
||||
// status: 'success'
|
||||
// });
|
||||
|
||||
// // 重新获取模型
|
||||
// loadModel();
|
||||
// } catch (err: any) {
|
||||
// toast({
|
||||
// title: err?.message || '上传文件失败',
|
||||
// status: 'error'
|
||||
// });
|
||||
// console.log('error->', err);
|
||||
// }
|
||||
// setLoading(false);
|
||||
// },
|
||||
// [setLoading, loadModel, modelId, toast]
|
||||
// );
|
||||
}, [setLoading, router, modelId]);
|
||||
|
||||
/* 点击更新模型状态 */
|
||||
const handleClickUpdateStatus = useCallback(async () => {
|
||||
@@ -239,21 +206,12 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
|
||||
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
|
||||
|
||||
{canTrain && model._id && (
|
||||
<Card
|
||||
p={4}
|
||||
{...media(
|
||||
{
|
||||
gridColumnStart: 1,
|
||||
gridColumnEnd: 3
|
||||
},
|
||||
{}
|
||||
)}
|
||||
>
|
||||
<ModelDataCard model={model} />
|
||||
{canTrain && !!model._id && (
|
||||
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
|
||||
<ModelDataCard modelId={model._id} />
|
||||
</Card>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, Button, Flex, Card } from '@chakra-ui/react';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { useRouter } from 'next/router';
|
||||
import ModelTable from './components/ModelTable';
|
||||
@@ -38,9 +37,7 @@ const modelList = () => {
|
||||
async (modelId: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const chatId = await getChatSiteId(modelId);
|
||||
|
||||
router.push(`/chat?chatId=${chatId}`, undefined, {
|
||||
router.push(`/chat?modelId=${modelId}`, undefined, {
|
||||
shallow: true
|
||||
});
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -45,11 +45,12 @@ const BillTable = () => {
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Box mt={4} mr={4} textAlign={'end'}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</TableContainer>
|
||||
<Box mt={4} mr={4} textAlign={'end'}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useToast } from '@/hooks/useToast';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { UserType } from '@/types/user';
|
||||
|
||||
import { clearToken } from '@/utils/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
@@ -16,7 +17,8 @@ const BilTable = dynamic(() => import('./components/BillTable'));
|
||||
const PayModal = dynamic(() => import('./components/PayModal'));
|
||||
|
||||
const NumberSetting = () => {
|
||||
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||
const router = useRouter();
|
||||
const { userInfo, updateUserInfo, initUserInfo, setUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { register, handleSubmit } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
@@ -43,15 +45,26 @@ const NumberSetting = () => {
|
||||
|
||||
useQuery(['init'], initUserInfo);
|
||||
|
||||
const onclickLogOut = useCallback(() => {
|
||||
clearToken();
|
||||
setUserInfo(null);
|
||||
router.replace('/login');
|
||||
}, [router, setUserInfo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 核心信息 */}
|
||||
<Card px={6} py={4}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
账号信息
|
||||
</Box>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
账号信息
|
||||
</Box>
|
||||
<Button variant={'outline'} size={'xs'} onClick={onclickLogOut}>
|
||||
退出登录
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'}>
|
||||
<Box flex={'0 0 60px'}>用户账号:</Box>
|
||||
<Box flex={'0 0 60px'}>账号:</Box>
|
||||
<Box>{userInfo?.username}</Box>
|
||||
</Flex>
|
||||
<Box mt={6}>
|
||||
|
||||
179
src/pages/promotion/index.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useColorModeValue,
|
||||
ModalFooter,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { getPromotionRecords } from '@/api/user';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { PromotionRecordType } from '@/api/response/user';
|
||||
import { PromotionTypeMap } from '@/constants/user';
|
||||
import { getPromotionInitData } from '@/api/user';
|
||||
import Image from 'next/image';
|
||||
|
||||
const OpenApi = () => {
|
||||
const { Loading } = useLoading();
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
const { copyData } = useCopyData();
|
||||
const {
|
||||
isOpen: isOpenWithdraw,
|
||||
onClose: onCloseWithdraw,
|
||||
onOpen: onOpenWithdraw
|
||||
} = useDisclosure();
|
||||
|
||||
useQuery(['init'], initUserInfo);
|
||||
const { data: { invitedAmount = 0, historyAmount = 0, residueAmount = 0 } = {} } = useQuery(
|
||||
['getInvitedCountAmount'],
|
||||
getPromotionInitData
|
||||
);
|
||||
|
||||
const {
|
||||
data: promotionRecords,
|
||||
isLoading,
|
||||
Pagination,
|
||||
total
|
||||
} = usePagination<PromotionRecordType>({
|
||||
api: getPromotionRecords
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card px={6} py={4} position={'relative'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
我的邀请
|
||||
</Box>
|
||||
<Box my={2} color={'blackAlpha.600'} fontSize={'sm'}>
|
||||
你可以通过邀请链接邀请好友注册 FastGpt 账号。好友在 FastGpt
|
||||
平台的每次充值,你都会获得一定比例的佣金。
|
||||
</Box>
|
||||
<Flex my={2} alignItems={'center'}>
|
||||
<Box>当前剩余佣金: ¥</Box>
|
||||
<Box mx={2} fontSize={'xl'} lineHeight={1} fontWeight={'bold'}>
|
||||
{residueAmount}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Button
|
||||
mr={4}
|
||||
variant={'outline'}
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}?inviterId=${userInfo?._id}`, '已复制邀请链接');
|
||||
}}
|
||||
>
|
||||
复制邀请链接
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<MyIcon name="withdraw" w={'22px'} />}
|
||||
px={4}
|
||||
title={residueAmount < 50 ? '最低提现额度为50元' : ''}
|
||||
isDisabled={residueAmount < 50}
|
||||
onClick={onOpenWithdraw}
|
||||
>
|
||||
提现
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card mt={4} px={6} py={4} position={'relative'}>
|
||||
<Flex alignItems={'center'} mb={3} justifyContent={['space-between', 'flex-start']}>
|
||||
<Box w={'120px'}>佣金比例</Box>
|
||||
<Box fontWeight={'bold'}>{userInfo?.promotion.rate || 15}%</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mb={3} justifyContent={['space-between', 'flex-start']}>
|
||||
<Box w={'120px'}>已注册用户数</Box>
|
||||
<Box fontWeight={'bold'}>{invitedAmount}人</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} justifyContent={['space-between', 'flex-start']}>
|
||||
<Box w={'120px'}>累计佣金</Box>
|
||||
<Box fontWeight={'bold'}>¥ {historyAmount}</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card mt={4} px={6} py={4} position={'relative'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
佣金记录 ({total})
|
||||
</Box>
|
||||
<TableContainer position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>金额</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{promotionRecords.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{PromotionTypeMap[item.type]}</Td>
|
||||
<Td>{item.amount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</TableContainer>
|
||||
<Box mt={4} mr={4} textAlign={'end'}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</Card>
|
||||
<Modal isOpen={isOpenWithdraw} onClose={onCloseWithdraw}>
|
||||
<ModalOverlay />
|
||||
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||
<ModalHeader>提现联系</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody textAlign={'center'}>
|
||||
<Image
|
||||
style={{ margin: 'auto' }}
|
||||
src={'/imgs/wx300-2.jpg'}
|
||||
width={200}
|
||||
height={200}
|
||||
alt=""
|
||||
/>
|
||||
<Box mt={2}>
|
||||
微信号:
|
||||
<Box as={'span'} userSelect={'all'}>
|
||||
YNyiqi
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>发送你的邀请链接和需要提现的金额</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'outline'} onClick={onCloseWithdraw}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenApi;
|
||||
@@ -2,8 +2,7 @@ export const openaiError: Record<string, string> = {
|
||||
context_length_exceeded: '内容超长了,请重置对话',
|
||||
Unauthorized: 'API-KEY 不合法',
|
||||
rate_limit_reached: 'API被限制,请稍后再试',
|
||||
'Bad Request': 'Bad Request~ 可能内容太多了',
|
||||
'Too Many Requests': '请求次数太多了,请慢点~',
|
||||
'Bad Request': 'Bad Request~ openai 异常',
|
||||
'Bad Gateway': '网关异常,请重试'
|
||||
};
|
||||
export const openaiError2: Record<string, string> = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { DataItem } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getOpenAIApi } from '@/service/utils/auth';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { getOpenApiKey } from '../utils/openai';
|
||||
import type { ChatCompletionRequestMessage } from 'openai';
|
||||
import { DataItemSchema } from '@/types/mongoSchema';
|
||||
import { ChatModelNameEnum } from '@/constants/model';
|
||||
import { ChatModelEnum } from '@/constants/model';
|
||||
import { pushSplitDataBill } from '@/service/events/pushBill';
|
||||
|
||||
export async function generateAbstract(next = false): Promise<any> {
|
||||
@@ -68,7 +68,7 @@ export async function generateAbstract(next = false): Promise<any> {
|
||||
// 请求 chatgpt 获取摘要
|
||||
const abstractResponse = await chatAPI.createChatCompletion(
|
||||
{
|
||||
model: ChatModelNameEnum.GPT35,
|
||||
model: ChatModelEnum.GPT35,
|
||||
temperature: 0.8,
|
||||
n: 1,
|
||||
messages: [
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SplitData } from '@/service/mongo';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getOpenAIApi } from '@/service/utils/auth';
|
||||
import { httpsAgent } from '@/service/utils/tools';
|
||||
import { getOpenApiKey } from '../utils/openai';
|
||||
import type { ChatCompletionRequestMessage } from 'openai';
|
||||
import { ChatModelNameEnum } from '@/constants/model';
|
||||
import { ChatModelEnum } from '@/constants/model';
|
||||
import { pushSplitDataBill } from '@/service/events/pushBill';
|
||||
import { generateVector } from './generateVector';
|
||||
import { openaiError2 } from '../errorCode';
|
||||
@@ -69,9 +69,13 @@ export async function generateQA(next = false): Promise<any> {
|
||||
const chatAPI = getOpenAIApi(userApiKey || systemKey);
|
||||
const systemPrompt: ChatCompletionRequestMessage = {
|
||||
role: 'system',
|
||||
content: `${
|
||||
dataItem.prompt || '下面是一段长文本'
|
||||
},请从中提取出5至30个问题和答案,并按以下格式返回: Q1:\nA1:\nQ2:\nA2:\n`
|
||||
content: `你是出题人
|
||||
${dataItem.prompt || '下面是"一段长文本"'}
|
||||
从中选出5至20个题目和答案,题目包含问答题,计算题,代码题等.答案要详细.按格式返回: Q1:
|
||||
A1:
|
||||
Q2:
|
||||
A2:
|
||||
...`
|
||||
};
|
||||
|
||||
// 请求 chatgpt 获取回答
|
||||
@@ -80,7 +84,7 @@ export async function generateQA(next = false): Promise<any> {
|
||||
chatAPI
|
||||
.createChatCompletion(
|
||||
{
|
||||
model: ChatModelNameEnum.GPT35,
|
||||
model: ChatModelEnum.GPT35,
|
||||
temperature: 0.8,
|
||||
n: 1,
|
||||
messages: [
|
||||
@@ -114,7 +118,8 @@ export async function generateQA(next = false): Promise<any> {
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('QA 拆分错误', err);
|
||||
console.log('QA拆分错误');
|
||||
console.log(err.response?.status, err.response?.statusText, err.response?.data);
|
||||
return Promise.reject(err);
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import { connectToDatabase, Bill, User } from '../mongo';
|
||||
import { modelList, ChatModelNameEnum } from '@/constants/model';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import {
|
||||
modelList,
|
||||
ChatModelEnum,
|
||||
ModelNameEnum,
|
||||
Model2ChatModelMap,
|
||||
embeddingModel
|
||||
} from '@/constants/model';
|
||||
import { BillTypeEnum } from '@/constants/user';
|
||||
import type { DataType } from '@/types/data';
|
||||
import { countChatTokens } from '@/utils/tools';
|
||||
|
||||
export const pushChatBill = async ({
|
||||
isPay,
|
||||
modelName,
|
||||
userId,
|
||||
chatId,
|
||||
text
|
||||
messages
|
||||
}: {
|
||||
isPay: boolean;
|
||||
modelName: string;
|
||||
modelName: `${ModelNameEnum}`;
|
||||
userId: string;
|
||||
chatId?: string;
|
||||
text: string;
|
||||
chatId?: '' | string;
|
||||
messages: { role: 'system' | 'user' | 'assistant'; content: string }[];
|
||||
}) => {
|
||||
let billId;
|
||||
let billId = '';
|
||||
|
||||
try {
|
||||
// 计算 token 数量
|
||||
const tokens = Math.floor(encode(text).length * 0.75);
|
||||
const tokens = countChatTokens({ model: Model2ChatModelMap[modelName] as any, messages });
|
||||
const text = messages.map((item) => item.content).join('');
|
||||
|
||||
console.log(
|
||||
`chat generate success. text len: ${text.length}. token len: ${tokens}. pay:${isPay}`
|
||||
@@ -42,7 +49,7 @@ export const pushChatBill = async ({
|
||||
userId,
|
||||
type: 'chat',
|
||||
modelName,
|
||||
chatId,
|
||||
chatId: chatId ? chatId : undefined,
|
||||
textLen: text.length,
|
||||
tokenLen: tokens,
|
||||
price
|
||||
@@ -88,7 +95,7 @@ export const pushSplitDataBill = async ({
|
||||
if (isPay) {
|
||||
try {
|
||||
// 获取模型单价格, 都是用 gpt35 拆分
|
||||
const modelItem = modelList.find((item) => item.model === ChatModelNameEnum.GPT35);
|
||||
const modelItem = modelList.find((item) => item.model === ChatModelEnum.GPT35);
|
||||
const unitPrice = modelItem?.price || 3;
|
||||
// 计算价格
|
||||
const price = unitPrice * tokenLen;
|
||||
@@ -97,7 +104,7 @@ export const pushSplitDataBill = async ({
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
type,
|
||||
modelName: ChatModelNameEnum.GPT35,
|
||||
modelName: ChatModelEnum.GPT35,
|
||||
textLen: text.length,
|
||||
tokenLen,
|
||||
price
|
||||
@@ -149,7 +156,7 @@ export const pushGenerateVectorBill = async ({
|
||||
const res = await Bill.create({
|
||||
userId,
|
||||
type: BillTypeEnum.vector,
|
||||
modelName: ChatModelNameEnum.VECTOR,
|
||||
modelName: embeddingModel,
|
||||
textLen: text.length,
|
||||
tokenLen,
|
||||
price
|
||||
|
||||
@@ -26,6 +26,10 @@ const ChatSchema = new Schema({
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '历史记录'
|
||||
},
|
||||
content: {
|
||||
type: [
|
||||
{
|
||||
|
||||
@@ -53,11 +53,6 @@ const ModelSchema = new Schema({
|
||||
}
|
||||
},
|
||||
service: {
|
||||
company: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['openai']
|
||||
},
|
||||
trainId: {
|
||||
// 训练时需要的 ID, 不能训练的模型没有这个值。
|
||||
type: String,
|
||||
|
||||
31
src/service/models/promotionRecord.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Schema, model, models, Model } from 'mongoose';
|
||||
import { PromotionRecordSchema as PromotionRecordType } from '@/types/mongoSchema';
|
||||
|
||||
const PromotionRecordSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: true
|
||||
},
|
||||
objUId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'user',
|
||||
required: false
|
||||
},
|
||||
createTime: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['invite', 'shareModel', 'withdraw']
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
export const promotionRecord: Model<PromotionRecordType> =
|
||||
models['promotionRecord'] || model('promotionRecord', PromotionRecordSchema);
|
||||
@@ -18,10 +18,6 @@ const SplitDataSchema = new Schema({
|
||||
ref: 'model',
|
||||
required: true
|
||||
},
|
||||
rawText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
textList: {
|
||||
type: [String],
|
||||
default: []
|
||||
|
||||
@@ -31,11 +31,6 @@ const UserSchema = new Schema({
|
||||
// 返现比例
|
||||
type: Number,
|
||||
default: 15
|
||||
},
|
||||
amount: {
|
||||
// 推广金额
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
openaiKey: {
|
||||
|
||||
@@ -62,3 +62,4 @@ export * from './models/data';
|
||||
export * from './models/dataItem';
|
||||
export * from './models/splitData';
|
||||
export * from './models/openapi';
|
||||
export * from './models/promotionRecord';
|
||||
|
||||
@@ -32,6 +32,8 @@ export const jsonRes = <T = any>(
|
||||
msg = error;
|
||||
} else if (proxyError[error?.code]) {
|
||||
msg = '服务器代理出错';
|
||||
} else if (error?.response?.data?.error?.message) {
|
||||
msg = error?.response?.data?.error?.message;
|
||||
} else if (openaiError2[error?.response?.data?.error?.type]) {
|
||||
msg = openaiError2[error?.response?.data?.error?.type];
|
||||
} else if (openaiError[error?.response?.statusText]) {
|
||||
|
||||
74
src/service/utils/auth.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
import { Chat, Model } from '../mongo';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { authToken } from './tools';
|
||||
import { getOpenApiKey } from './openai';
|
||||
import type { ChatItemType } from '@/types/chat';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export const getOpenAIApi = (apiKey: string) => {
|
||||
const configuration = new Configuration({
|
||||
apiKey
|
||||
});
|
||||
|
||||
return new OpenAIApi(configuration, undefined);
|
||||
};
|
||||
|
||||
// 模型使用权校验
|
||||
export const authModel = async (modelId: string, userId: string) => {
|
||||
// 获取 model 数据
|
||||
const model = await Model.findById<ModelSchema>(modelId);
|
||||
if (!model) {
|
||||
return Promise.reject('模型不存在');
|
||||
}
|
||||
// 凭证校验
|
||||
if (userId !== String(model.userId)) {
|
||||
return Promise.reject('无权使用该模型');
|
||||
}
|
||||
return { model };
|
||||
};
|
||||
|
||||
// 获取对话校验
|
||||
export const authChat = async ({
|
||||
modelId,
|
||||
chatId,
|
||||
authorization
|
||||
}: {
|
||||
modelId: string;
|
||||
chatId: '' | string;
|
||||
authorization?: string;
|
||||
}) => {
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
// 获取 model 数据
|
||||
const { model } = await authModel(modelId, userId);
|
||||
|
||||
// 聊天内容
|
||||
let content: ChatItemType[] = [];
|
||||
|
||||
if (chatId) {
|
||||
// 获取 chat 数据
|
||||
content = await Chat.aggregate([
|
||||
{ $match: { _id: new mongoose.Types.ObjectId(chatId) } },
|
||||
{ $unwind: '$content' },
|
||||
{ $match: { 'content.deleted': false } },
|
||||
{
|
||||
$project: {
|
||||
obj: '$content.obj',
|
||||
value: '$content.value'
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// 获取 user 的 apiKey
|
||||
const { userApiKey, systemKey } = await getOpenApiKey(userId);
|
||||
|
||||
return {
|
||||
userApiKey,
|
||||
systemKey,
|
||||
content,
|
||||
userId,
|
||||
model
|
||||
};
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
import { Chat } from '../mongo';
|
||||
import type { ChatPopulate } from '@/types/mongoSchema';
|
||||
import { authToken } from './tools';
|
||||
import { getOpenApiKey } from './openai';
|
||||
|
||||
export const getOpenAIApi = (apiKey: string) => {
|
||||
const configuration = new Configuration({
|
||||
apiKey
|
||||
});
|
||||
|
||||
return new OpenAIApi(configuration, undefined);
|
||||
};
|
||||
|
||||
export const authChat = async (chatId: string, authorization?: string) => {
|
||||
// 获取 chat 数据
|
||||
const chat = await Chat.findById<ChatPopulate>(chatId).populate({
|
||||
path: 'modelId',
|
||||
options: {
|
||||
strictPopulate: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!chat || !chat.modelId || !chat.userId) {
|
||||
return Promise.reject('模型不存在');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const userId = await authToken(authorization);
|
||||
if (userId !== String(chat.userId._id)) {
|
||||
return Promise.reject('无权使用该对话');
|
||||
}
|
||||
|
||||
// 获取 user 的 apiKey
|
||||
const { user, userApiKey, systemKey } = await getOpenApiKey(chat.userId as unknown as string);
|
||||
|
||||
// filter 掉被 deleted 的内容
|
||||
chat.content = chat.content.filter((item) => item.deleted !== true);
|
||||
|
||||
return {
|
||||
userApiKey,
|
||||
systemKey,
|
||||
chat,
|
||||
userId: user._id
|
||||
};
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiResponse } from 'next';
|
||||
import type { PassThrough } from 'stream';
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||
import { getOpenAIApi } from '@/service/utils/chat';
|
||||
import { getOpenAIApi } from '@/service/utils/auth';
|
||||
import { httpsAgent } from './tools';
|
||||
import { User } from '../models/user';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { ChatModelNameEnum } from '@/constants/model';
|
||||
import { embeddingModel } from '@/constants/model';
|
||||
import { pushGenerateVectorBill } from '../events/pushBill';
|
||||
|
||||
/* 获取用户 api 的 openai 信息 */
|
||||
@@ -80,7 +80,7 @@ export const openaiCreateEmbedding = async ({
|
||||
const res = await chatAPI
|
||||
.createEmbedding(
|
||||
{
|
||||
model: ChatModelNameEnum.VECTOR,
|
||||
model: embeddingModel,
|
||||
input: text
|
||||
},
|
||||
{
|
||||
@@ -134,11 +134,11 @@ export const gpt35StreamResponse = ({
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content: string = json?.choices?.[0].delta.content || '';
|
||||
// console.log('content:', content);
|
||||
if (!content || (responseContent === '' && content === '\n')) return;
|
||||
|
||||
responseContent += content;
|
||||
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
|
||||
|
||||
if (!stream.destroyed && content) {
|
||||
stream.push(content.replace(/\n/g, '<br/>'));
|
||||
}
|
||||
} catch (error) {
|
||||
error;
|
||||
}
|
||||
|
||||
36
src/service/utils/promotion.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { promotionRecord } from '../mongo';
|
||||
|
||||
export const pushPromotionRecord = async ({
|
||||
userId,
|
||||
objUId,
|
||||
type,
|
||||
amount
|
||||
}: {
|
||||
userId: string;
|
||||
objUId: string;
|
||||
type: 'invite' | 'shareModel';
|
||||
amount: number;
|
||||
}) => {
|
||||
try {
|
||||
await promotionRecord.create({
|
||||
userId,
|
||||
objUId,
|
||||
type,
|
||||
amount
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('创建推广记录异常', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const withdrawRecord = async ({ userId, amount }: { userId: string; amount: number }) => {
|
||||
try {
|
||||
await promotionRecord.create({
|
||||
userId,
|
||||
type: 'withdraw',
|
||||
amount
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('提现记录异常', error);
|
||||
}
|
||||
};
|
||||
@@ -2,10 +2,12 @@ import type { NextApiRequest } from 'next';
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { ChatItemType } from '@/types/chat';
|
||||
import { encode } from 'gpt-token-utils';
|
||||
import { OpenApi, User } from '../mongo';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
import { ERROR_ENUM } from '../errorCode';
|
||||
import { countChatTokens } from '@/utils/tools';
|
||||
import { ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||
import { ChatModelEnum } from '@/constants/model';
|
||||
|
||||
/* 密码加密 */
|
||||
export const hashPassword = (psw: string) => {
|
||||
@@ -86,8 +88,16 @@ export const authOpenApiKey = async (req: NextApiRequest) => {
|
||||
export const httpsAgent = (fast: boolean) =>
|
||||
fast ? global.httpsAgentFast : global.httpsAgentNormal;
|
||||
|
||||
/* tokens 截断 */
|
||||
export const openaiChatFilter = (prompts: ChatItemType[], maxTokens: number) => {
|
||||
/* 聊天内容 tokens 截断 */
|
||||
export const openaiChatFilter = ({
|
||||
model,
|
||||
prompts,
|
||||
maxTokens
|
||||
}: {
|
||||
model: `${ChatModelEnum}`;
|
||||
prompts: ChatItemType[];
|
||||
maxTokens: number;
|
||||
}) => {
|
||||
const formatPrompts = prompts.map((item) => ({
|
||||
obj: item.obj,
|
||||
value: item.value
|
||||
@@ -97,41 +107,64 @@ export const openaiChatFilter = (prompts: ChatItemType[], maxTokens: number) =>
|
||||
.trim()
|
||||
}));
|
||||
|
||||
let res: ChatItemType[] = [];
|
||||
|
||||
let chats: ChatItemType[] = [];
|
||||
let systemPrompt: ChatItemType | null = null;
|
||||
|
||||
// System 词保留
|
||||
if (formatPrompts[0]?.obj === 'SYSTEM') {
|
||||
systemPrompt = formatPrompts.shift() as ChatItemType;
|
||||
maxTokens -= encode(formatPrompts[0].value).length;
|
||||
}
|
||||
|
||||
// 从后往前截取
|
||||
// 格式化文本内容成 chatgpt 格式
|
||||
const map = {
|
||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||
};
|
||||
|
||||
let messages: { role: ChatCompletionRequestMessageRoleEnum; content: string }[] = [];
|
||||
|
||||
// 从后往前截取对话内容
|
||||
for (let i = formatPrompts.length - 1; i >= 0; i--) {
|
||||
const tokens = encode(formatPrompts[i].value).length;
|
||||
res.unshift(formatPrompts[i]);
|
||||
chats.unshift(formatPrompts[i]);
|
||||
|
||||
messages = (systemPrompt ? [systemPrompt, ...chats] : chats).map((item) => ({
|
||||
role: map[item.obj],
|
||||
content: item.value
|
||||
}));
|
||||
|
||||
const tokens = countChatTokens({
|
||||
model,
|
||||
messages
|
||||
});
|
||||
|
||||
/* 整体 tokens 超出范围 */
|
||||
if (tokens >= maxTokens) {
|
||||
break;
|
||||
}
|
||||
|
||||
maxTokens -= tokens;
|
||||
}
|
||||
|
||||
return systemPrompt ? [systemPrompt, ...res] : res;
|
||||
return messages;
|
||||
};
|
||||
|
||||
/* system 内容截断 */
|
||||
export const systemPromptFilter = (prompts: string[], maxTokens: number) => {
|
||||
export const systemPromptFilter = ({
|
||||
model,
|
||||
prompts,
|
||||
maxTokens
|
||||
}: {
|
||||
model: 'gpt-4' | 'gpt-4-32k' | 'gpt-3.5-turbo';
|
||||
prompts: string[];
|
||||
maxTokens: number;
|
||||
}) => {
|
||||
let splitText = '';
|
||||
|
||||
// 从前往前截取
|
||||
for (let i = 0; i < prompts.length; i++) {
|
||||
const prompt = prompts[i];
|
||||
const prompt = prompts[i].replace(/\n+/g, '\n');
|
||||
|
||||
splitText += `${prompt}\n`;
|
||||
const tokens = encode(splitText).length;
|
||||
const tokens = countChatTokens({ model, messages: [{ role: 'system', content: splitText }] });
|
||||
if (tokens >= maxTokens) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import type { HistoryItem } from '@/types/chat';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
|
||||
type Props = {
|
||||
chatHistory: HistoryItem[];
|
||||
pushChatHistory: (e: HistoryItem) => void;
|
||||
updateChatHistory: (chatId: string, title: string) => void;
|
||||
removeChatHistoryByWindowId: (chatId: string) => void;
|
||||
clearHistory: () => void;
|
||||
generateChatWindow: (modelId: string) => Promise<string>;
|
||||
};
|
||||
export const useChatStore = create<Props>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
chatHistory: [],
|
||||
pushChatHistory(item: HistoryItem) {
|
||||
set((state) => {
|
||||
if (state.chatHistory.find((history) => history.chatId === item.chatId)) return;
|
||||
state.chatHistory = [item, ...state.chatHistory].slice(0, 20);
|
||||
});
|
||||
},
|
||||
updateChatHistory(chatId: string, title: string) {
|
||||
set((state) => {
|
||||
state.chatHistory = state.chatHistory.map((item) => ({
|
||||
...item,
|
||||
title: item.chatId === chatId ? title : item.title
|
||||
}));
|
||||
});
|
||||
},
|
||||
removeChatHistoryByWindowId(chatId: string) {
|
||||
set((state) => {
|
||||
state.chatHistory = state.chatHistory.filter((item) => item.chatId !== chatId);
|
||||
});
|
||||
},
|
||||
clearHistory() {
|
||||
set((state) => {
|
||||
state.chatHistory = [];
|
||||
});
|
||||
},
|
||||
generateChatWindow(modelId: string) {
|
||||
return getChatSiteId(modelId);
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'chatHistory'
|
||||
// serialize: JSON.stringify,
|
||||
// deserialize: (data) => (data ? JSON.parse(data) : []),
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -11,7 +11,7 @@ import { getTokenLogin } from '@/api/user';
|
||||
type State = {
|
||||
userInfo: UserType | null;
|
||||
initUserInfo: () => Promise<null>;
|
||||
setUserInfo: (user: UserType, token?: string) => void;
|
||||
setUserInfo: (user: UserType | null, token?: string) => void;
|
||||
updateUserInfo: (user: UserUpdateParams) => void;
|
||||
myModels: ModelSchema[];
|
||||
getMyModels: () => void;
|
||||
@@ -27,12 +27,14 @@ export const useUserStore = create<State>()(
|
||||
get().setUserInfo(res);
|
||||
return null;
|
||||
},
|
||||
setUserInfo(user: UserType, token?: string) {
|
||||
setUserInfo(user: UserType | null, token?: string) {
|
||||
set((state) => {
|
||||
state.userInfo = {
|
||||
...user,
|
||||
balance: formatPrice(user.balance)
|
||||
};
|
||||
state.userInfo = user
|
||||
? {
|
||||
...user,
|
||||
balance: formatPrice(user.balance)
|
||||
}
|
||||
: null;
|
||||
});
|
||||
token && setToken(token);
|
||||
},
|
||||
|
||||
9
src/types/chat.d.ts
vendored
@@ -3,12 +3,3 @@ export type ChatItemType = {
|
||||
value: string;
|
||||
deleted?: boolean;
|
||||
};
|
||||
|
||||
export type ChatSiteItemType = {
|
||||
status: 'loading' | 'finish';
|
||||
} & ChatItemType;
|
||||
|
||||
export type HistoryItem = {
|
||||
chatId: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
26
src/types/mongoSchema.d.ts
vendored
@@ -2,13 +2,12 @@ import type { ChatItemType } from './chat';
|
||||
import {
|
||||
ModelStatusEnum,
|
||||
TrainingStatusEnum,
|
||||
ChatModelNameEnum,
|
||||
ModelVectorSearchModeEnum
|
||||
ModelNameEnum,
|
||||
ModelVectorSearchModeEnum,
|
||||
ChatModelEnum
|
||||
} from '@/constants/model';
|
||||
import type { DataType } from './data';
|
||||
|
||||
export type ServiceName = 'openai';
|
||||
|
||||
export interface UserModelSchema {
|
||||
_id: string;
|
||||
username: string;
|
||||
@@ -18,6 +17,9 @@ export interface UserModelSchema {
|
||||
promotionAmount: number;
|
||||
openaiKey: string;
|
||||
createTime: number;
|
||||
promotion: {
|
||||
rate: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthCodeSchema {
|
||||
@@ -43,10 +45,9 @@ export interface ModelSchema {
|
||||
mode: `${ModelVectorSearchModeEnum}`;
|
||||
};
|
||||
service: {
|
||||
company: ServiceName;
|
||||
trainId: string; // 训练的模型,训练后就是训练的模型id
|
||||
chatModel: string; // 聊天时用的模型,训练后就是训练的模型
|
||||
modelName: `${ChatModelNameEnum}`; // 底层模型名称,不会变
|
||||
chatModel: `${ChatModelEnum}`; // 聊天时用的模型,训练后就是训练的模型
|
||||
modelName: `${ModelNameEnum}`; // 底层模型名称,不会变
|
||||
};
|
||||
security: {
|
||||
domain: string[];
|
||||
@@ -75,7 +76,6 @@ export interface ModelSplitDataSchema {
|
||||
_id: string;
|
||||
userId: string;
|
||||
modelId: string;
|
||||
rawText: string;
|
||||
prompt: string;
|
||||
errorText: string;
|
||||
textList: string[];
|
||||
@@ -83,7 +83,6 @@ export interface ModelSplitDataSchema {
|
||||
|
||||
export interface TrainingSchema {
|
||||
_id: string;
|
||||
serviceName: ServiceName;
|
||||
tuneId: string;
|
||||
modelId: string;
|
||||
status: `${TrainingStatusEnum}`;
|
||||
@@ -162,3 +161,12 @@ export interface OpenApiSchema {
|
||||
lastUsedTime?: Date;
|
||||
apiKey: String;
|
||||
}
|
||||
|
||||
export interface PromotionRecordSchema {
|
||||
_id: string;
|
||||
userId: string; // 收益人
|
||||
objUId?: string; // 目标对象(如果是withdraw则为空)
|
||||
type: 'invite' | 'shareModel' | 'withdraw';
|
||||
createTime: Date; // 记录时间
|
||||
amount: number;
|
||||
}
|
||||
|
||||
3
src/types/user.d.ts
vendored
@@ -3,6 +3,9 @@ export interface UserType {
|
||||
username: string;
|
||||
openaiKey: string;
|
||||
balance: number;
|
||||
promotion: {
|
||||
rate: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserUpdateParams {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import mammoth from 'mammoth';
|
||||
import Papa from 'papaparse';
|
||||
import { countChatTokens } from './tools';
|
||||
|
||||
/**
|
||||
* 读取 txt 文件内容
|
||||
@@ -137,3 +138,53 @@ export const fileDownload = ({
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
/**
|
||||
* text split into chunks
|
||||
* maxLen - one chunk len. max: 3500
|
||||
* slideLen - The size of the before and after Text
|
||||
* maxLen > slideLen
|
||||
*/
|
||||
export const splitText = ({
|
||||
text,
|
||||
maxLen,
|
||||
slideLen
|
||||
}: {
|
||||
text: string;
|
||||
maxLen: number;
|
||||
slideLen: number;
|
||||
}) => {
|
||||
const textArr =
|
||||
text.match(/[!?。\n.]+|[^\s]+/g)?.filter((item) => {
|
||||
const text = item.replace(/(\\n)/g, '\n').trim();
|
||||
if (text && text !== '\n') return true;
|
||||
return false;
|
||||
}) || [];
|
||||
|
||||
const chunks: { sum: number; arr: string[] }[] = [{ sum: 0, arr: [] }];
|
||||
|
||||
for (let i = 0; i < textArr.length; i++) {
|
||||
const tokenLen = countChatTokens({ messages: [{ role: 'system', content: textArr[i] }] });
|
||||
chunks[chunks.length - 1].sum += tokenLen;
|
||||
chunks[chunks.length - 1].arr.push(textArr[i]);
|
||||
|
||||
// current length is over maxLen. create new chunk
|
||||
if (chunks[chunks.length - 1].sum + tokenLen >= maxLen) {
|
||||
// get slide len text as the initial value
|
||||
const chunk: { sum: number; arr: string[] } = { sum: 0, arr: [] };
|
||||
for (let j = chunks[chunks.length - 1].arr.length - 1; j >= 0; j--) {
|
||||
const chunkText = chunks[chunks.length - 1].arr[j];
|
||||
const tokenLen = countChatTokens({ messages: [{ role: 'system', content: chunkText }] });
|
||||
chunk.sum += tokenLen;
|
||||
chunk.arr.unshift(chunkText);
|
||||
|
||||
if (chunk.sum >= slideLen) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
const result = chunks.map((item) => item.arr.join(''));
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
import crypto from 'crypto';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { encoding_for_model, type Tiktoken } from '@dqbd/tiktoken';
|
||||
import Graphemer from 'graphemer';
|
||||
import { ChatModelEnum } from '@/constants/model';
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
const graphemer = new Graphemer();
|
||||
const encMap = {
|
||||
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
'<|im_sep|>': 100266
|
||||
}),
|
||||
'gpt-4': encoding_for_model('gpt-4', {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
'<|im_sep|>': 100266
|
||||
}),
|
||||
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265,
|
||||
'<|im_sep|>': 100266
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* copy text data
|
||||
@@ -51,3 +74,60 @@ export const Obj2Query = (obj: Record<string, string | number>) => {
|
||||
}
|
||||
return queryParams.toString();
|
||||
};
|
||||
|
||||
/* 格式化 chat 聊天内容 */
|
||||
function getChatGPTEncodingText(
|
||||
messages: { role: 'system' | 'user' | 'assistant'; content: string; name?: string }[],
|
||||
model: 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k'
|
||||
) {
|
||||
const isGpt3 = model === 'gpt-3.5-turbo';
|
||||
|
||||
const msgSep = isGpt3 ? '\n' : '';
|
||||
const roleSep = isGpt3 ? '\n' : '<|im_sep|>';
|
||||
|
||||
return [
|
||||
messages
|
||||
.map(({ name = '', role, content }) => {
|
||||
return `<|im_start|>${name || role}${roleSep}${content}<|im_end|>`;
|
||||
})
|
||||
.join(msgSep),
|
||||
`<|im_start|>assistant${roleSep}`
|
||||
].join(msgSep);
|
||||
}
|
||||
function text2TokensLen(encoder: Tiktoken, inputText: string) {
|
||||
const encoding = encoder.encode(inputText, 'all');
|
||||
const segments: { text: string; tokens: { id: number; idx: number }[] }[] = [];
|
||||
|
||||
let byteAcc: number[] = [];
|
||||
let tokenAcc: { id: number; idx: number }[] = [];
|
||||
let inputGraphemes = graphemer.splitGraphemes(inputText);
|
||||
|
||||
for (let idx = 0; idx < encoding.length; idx++) {
|
||||
const token = encoding[idx]!;
|
||||
byteAcc.push(...encoder.decode_single_token_bytes(token));
|
||||
tokenAcc.push({ id: token, idx });
|
||||
|
||||
const segmentText = textDecoder.decode(new Uint8Array(byteAcc));
|
||||
const graphemes = graphemer.splitGraphemes(segmentText);
|
||||
|
||||
if (graphemes.every((item, idx) => inputGraphemes[idx] === item)) {
|
||||
segments.push({ text: segmentText, tokens: tokenAcc });
|
||||
|
||||
byteAcc = [];
|
||||
tokenAcc = [];
|
||||
inputGraphemes = inputGraphemes.slice(graphemes.length);
|
||||
}
|
||||
}
|
||||
|
||||
return segments.reduce((memo, i) => memo + i.tokens.length, 0) ?? 0;
|
||||
}
|
||||
export const countChatTokens = ({
|
||||
model = 'gpt-3.5-turbo',
|
||||
messages
|
||||
}: {
|
||||
model?: `${ChatModelEnum}`;
|
||||
messages: { role: 'system' | 'user' | 'assistant'; content: string }[];
|
||||
}) => {
|
||||
const text = getChatGPTEncodingText(messages, model);
|
||||
return text2TokensLen(encMap[model], text);
|
||||
};
|
||||
|
||||