Compare commits

..

2 Commits
v3.7 ... v3.6

Author SHA1 Message Date
archer
c7f0db0811 fix: ui;perf: docs 2023-05-14 22:04:19 +08:00
archer
c16d59671f feat: share chat page 2023-05-14 21:37:26 +08:00
89 changed files with 1364 additions and 1945 deletions

View File

@@ -29,8 +29,8 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
## 🏘️ 交流群
添加 wx 进入:
![Demo](https://otnvvf-imgs.oss.laf.run/wx300.png?raw=true 'wx')
wx: fastgpt123
![Demo](docs/imgs/wx300.jpg?raw=true 'wx')
## 👀 其他

View File

@@ -35,10 +35,185 @@ docker-compose -v
### 2. 创建 3 个初始化文件
fastgpt 文件夹。分别为fastgpt/docker-compose.yaml, fastgpt/pg/init.sql, fastgpt/nginx/nginx.conf
手动创建或者直接把 fastgpt 文件夹复制过去。
**/root/fastgpt/pg/init.sql PG 数据库初始化**
```sql
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
```
**/root/fastgpt/nginx/nginx.conf Nginx 配置**
```conf
user nginx;
worker_processes auto;
worker_rlimit_nofile 51200;
events {
worker_connections 1024;
}
http {
resolver 8.8.8.8;
proxy_ssl_server_name on;
access_log off;
server_names_hash_bucket_size 512;
client_header_buffer_size 64k;
large_client_header_buffers 4 64k;
client_max_body_size 50M;
proxy_connect_timeout 240s;
proxy_read_timeout 240s;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 8k;
gzip_http_version 1.1;
gzip_comp_level 6;
gzip_vary on;
gzip_types text/plain application/x-javascript text/css application/javascript application/json application/xml;
gzip_disable "MSIE [1-6]\.";
open_file_cache max=1000 inactive=1d;
open_file_cache_valid 30s;
open_file_cache_min_uses 8;
open_file_cache_errors off;
server {
listen 443 ssl;
# 改成自己的域名和证书
server_name docgpt.ahapocket.cn;
ssl_certificate /ssl/docgpt.pem;
ssl_certificate_key /ssl/docgpt.key;
ssl_session_timeout 5m;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name docgpt.ahapocket.cn;
rewrite ^(.*) https://$server_name$1 permanent;
}
}
```
**/root/fastgpt/docker-compose.yml 核心部署文件**
环境变量内容和开发时的环境变量基本相同,除了数据库的地址。
```yml
version: '3.3'
services:
pg:
image: ankane/pgvector:v0.4.1
container_name: pg
restart: always
ports:
- 8100:5432
environment:
# 这里的配置只有首次运行生效。修改后,重启镜像是不会生效的。需要把持久化数据删除再重启,才有效果
- POSTGRES_USER=fastgpt
- POSTGRES_PASSWORD=1234
- POSTGRES_DB=fastgpt
volumes:
# 刚创建的文件
- /root/fastgpt/pg/init.sql:/docker-entrypoint-initdb.d/init.sh
- /root/fastgpt/pg/data:/var/lib/postgresql/data
- /etc/localtime:/etc/localtime:ro
mongodb:
image: mongo:6.0.4
container_name: mongo
restart: always
ports:
- 27017:27017
environment:
# 这里的配置只有首次运行生效。修改后,重启镜像是不会生效的。需要把持久化数据删除再重启,才有效果
- MONGO_INITDB_ROOT_USERNAME=username
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- /root/fastgpt/mongo/data:/data/db
- /root/fastgpt/mongo/logs:/var/log/mongodb
- /etc/localtime:/etc/localtime:ro
fastgpt:
image: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:latest
network_mode: host
restart: always
container_name: fastgpt
environment:
# proxy可选
- AXIOS_PROXY_HOST=127.0.0.1
- AXIOS_PROXY_PORT=7890
# 是否开启队列任务。 1-开启0-关闭(请求 parentUrl 去执行任务,单机时直接填1
- queueTask=1
- parentUrl=https://hostname/api/openapi/startEvents
# 发送邮箱验证码配置。用的是QQ邮箱。参考 nodeMail 获取MAILE_CODE自行百度。
- MY_MAIL=xxxx@qq.com
- MAILE_CODE=xxxx
# 阿里短信服务(邮箱和短信至少二选一)
- aliAccessKeyId=xxxx
- aliAccessKeySecret=xxxx
- aliSignName=xxxxx
- aliTemplateCode=SMS_xxxx
# token加密凭证随便填作为登录凭证
- TOKEN_KEY=xxxx
# 和上方mongo镜像的username,password对应
- MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=fastgpt
- PG_HOST=0.0.0.0
- PG_PORT=8100
# 和上方PG镜像对应.
- PG_USER=fastgpt # POSTGRES_USER
- PG_PASSWORD=1234 # POSTGRES_PASSWORD
- PG_DB_NAME=fastgpt # POSTGRES_DB
# openai
- OPENAIKEY=sk-xxxxx
- GPT4KEY=sk-xxx
- OPENAI_BASE_URL=https://api.openai.com/v1
- OPENAI_BASE_URL_AUTH=可选的安全凭证
# claude
- CLAUDE_BASE_URL=calude模型请求地址
- CLAUDE_KEY=CLAUDE_KEY
nginx:
image: nginx:alpine3.17
container_name: nginx
restart: always
network_mode: host
volumes:
# 刚创建的文件
- /root/fastgpt/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /root/fastgpt/nginx/logs:/var/log/nginx
# https证书没有的话不填对应的nginx.conf也要修改
- /root/fastgpt/nginx/ssl/docgpt.key:/ssl/docgpt.key
- /root/fastgpt/nginx/ssl/docgpt.pem:/ssl/docgpt.pem
```
### 3. 运行 docker-compose
下面是一个辅助脚本,也可以直接 docker-compose up -d

View File

@@ -1,7 +1,7 @@
version: '3.3'
services:
pg:
image: ankane/pgvector:v0.4.2
image: ankane/pgvector:v0.4.1
container_name: pg
restart: always
ports:

View File

@@ -8,14 +8,12 @@ CREATE TABLE modelData (
vector VECTOR(1536),
status VARCHAR(50) NOT NULL,
user_id VARCHAR(50) NOT NULL,
model_id VARCHAR(50),
kb_id VARCHAR(50),
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_userId_index ON modelData USING HASH (model_id);
CREATE INDEX modelData_kbId_index ON modelData USING HASH (kb_id);
CREATE INDEX modelData_modelId_index ON modelData USING HASH (model_id);
EOSQL

View File

@@ -67,7 +67,7 @@ docker run --name mongo -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=username -e
**3、部署 pgsql**
```
docker run -it --name pg -e "POSTGRES_DB=fastgpt" -e "POSTGRES_PASSWORD=xxx" -e POSTGRES_USER=xxx -p 8100:5432 -v ~/fastgpt/pg/data:/var/lib/postgresql/data -d octoberlan/pgvector:v0.4.1
docker run -it --name pg -e "POSTGRES_PASSWORD=xxx" -e POSTGRES_USER=xxx -p 8100:5432 -v ~/fastgpt/pg/data:/var/lib/postgresql/data -d octoberlan/pgvector:v0.4.1
```
进 pgsql 容器运行
@@ -88,8 +88,8 @@ CREATE TABLE modelData (
);
-- create index
CREATE INDEX modelData_status_index ON modelData (status);
CREATE INDEX modelData_modelId_index ON modelData (model_id);
CREATE INDEX modelData_userId_index ON modelData (user_id);
CREATE INDEX modelData_modelId_index ON modelData (modelId);
CREATE INDEX modelData_userId_index ON modelData (userId);
EOSQL
```

BIN
docs/imgs/wx300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -51,7 +51,6 @@
"react-syntax-highlighter": "^15.5.0",
"redis": "^4.6.5",
"rehype-katex": "^6.0.2",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.58.3",

215
pnpm-lock.yaml generated
View File

@@ -59,7 +59,6 @@ specifiers:
react-syntax-highlighter: ^15.5.0
redis: ^4.6.5
rehype-katex: ^6.0.2
rehype-raw: ^6.1.1
remark-gfm: ^3.0.1
remark-math: ^5.1.1
sass: ^1.58.3
@@ -110,7 +109,6 @@ dependencies:
react-syntax-highlighter: registry.npmmirror.com/react-syntax-highlighter/15.5.0_react@18.2.0
redis: registry.npmmirror.com/redis/4.6.5
rehype-katex: registry.npmmirror.com/rehype-katex/6.0.2
rehype-raw: 6.1.1
remark-gfm: registry.npmmirror.com/remark-gfm/3.0.1
remark-math: registry.npmmirror.com/remark-math/5.1.1
sass: registry.npmmirror.com/sass/1.58.3
@@ -296,37 +294,11 @@ packages:
resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==}
dev: true
/@types/hast/2.3.4:
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
dependencies:
'@types/unist': 2.0.6
dev: false
/@types/parse5/6.0.3:
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
dev: false
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: false
/bail/2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
dev: false
/comma-separated-tokens/2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
dev: false
/cookie/0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/extend/3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
dev: false
/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -337,95 +309,9 @@ packages:
/graceful-fs/4.2.10:
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
requiresBuild: true
dev: false
optional: true
/hast-util-from-parse5/7.1.2:
resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==}
dependencies:
'@types/hast': 2.3.4
'@types/unist': 2.0.6
hastscript: 7.2.0
property-information: 6.2.0
vfile: 5.3.7
vfile-location: 4.1.0
web-namespaces: 2.0.1
dev: false
/hast-util-parse-selector/3.1.1:
resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==}
dependencies:
'@types/hast': 2.3.4
dev: false
/hast-util-raw/7.2.3:
resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==}
dependencies:
'@types/hast': 2.3.4
'@types/parse5': 6.0.3
hast-util-from-parse5: 7.1.2
hast-util-to-parse5: 7.1.0
html-void-elements: 2.0.1
parse5: 6.0.1
unist-util-position: 4.0.4
unist-util-visit: 4.1.2
vfile: 5.3.7
web-namespaces: 2.0.1
zwitch: 2.0.4
dev: false
/hast-util-to-parse5/7.1.0:
resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==}
dependencies:
'@types/hast': 2.3.4
comma-separated-tokens: 2.0.3
property-information: 6.2.0
space-separated-tokens: 2.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
dev: false
/hastscript/7.2.0:
resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==}
dependencies:
'@types/hast': 2.3.4
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 3.1.1
property-information: 6.2.0
space-separated-tokens: 2.0.2
dev: false
/html-void-elements/2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
dev: false
/is-buffer/2.0.5:
resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
engines: {node: '>=4'}
dev: false
/is-plain-obj/4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
dev: false
/parse5/6.0.1:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
dev: false
/property-information/6.2.0:
resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==}
dev: false
/rehype-raw/6.1.1:
resolution: {integrity: sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==}
dependencies:
'@types/hast': 2.3.4
hast-util-raw: 7.2.3
unified: 10.1.2
dev: false
/saslprep/1.0.3:
resolution: {integrity: sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==}
engines: {node: '>=6'}
@@ -438,92 +324,9 @@ packages:
/source-map/0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: false
optional: true
/space-separated-tokens/2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
dev: false
/trough/2.1.0:
resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==}
dev: false
/unified/10.1.2:
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
dependencies:
'@types/unist': 2.0.6
bail: 2.0.2
extend: 3.0.2
is-buffer: 2.0.5
is-plain-obj: 4.1.0
trough: 2.1.0
vfile: 5.3.7
dev: false
/unist-util-is/5.2.0:
resolution: {integrity: sha512-Glt17jWwZeyqrFqOK0pF1Ded5U3yzJnFr8CG1GMjCWTp9zDo2p+cmD6pWbZU8AgM5WU3IzRv6+rBwhzsGh6hBQ==}
dev: false
/unist-util-position/4.0.4:
resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==}
dependencies:
'@types/unist': 2.0.6
dev: false
/unist-util-stringify-position/3.0.3:
resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==}
dependencies:
'@types/unist': 2.0.6
dev: false
/unist-util-visit-parents/5.1.3:
resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==}
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.2.0
dev: false
/unist-util-visit/4.1.2:
resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==}
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.2.0
unist-util-visit-parents: 5.1.3
dev: false
/vfile-location/4.1.0:
resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==}
dependencies:
'@types/unist': 2.0.6
vfile: 5.3.7
dev: false
/vfile-message/3.1.4:
resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==}
dependencies:
'@types/unist': 2.0.6
unist-util-stringify-position: 3.0.3
dev: false
/vfile/5.3.7:
resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==}
dependencies:
'@types/unist': 2.0.6
is-buffer: 2.0.5
unist-util-stringify-position: 3.0.3
vfile-message: 3.1.4
dev: false
/web-namespaces/2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
dev: false
/zwitch/2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false
registry.npmmirror.com/@alicloud/credentials/2.2.6:
resolution: {integrity: sha512-jG+msY77dHmAF3x+8VTy7fEgORyXLHmDci8t92HeipBdCHsPptDegA++GEwKgR7f6G4wvafYt+aqMZ1iligdrQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.2.6.tgz}
name: '@alicloud/credentials'
@@ -7939,7 +7742,7 @@ packages:
name: hast-util-from-parse5
version: 7.1.2
dependencies:
'@types/hast': 2.3.4
'@types/hast': registry.npmmirror.com/@types/hast/2.3.4
'@types/unist': registry.npmmirror.com/@types/unist/2.0.6
hastscript: registry.npmmirror.com/hastscript/7.2.0
property-information: registry.npmmirror.com/property-information/6.2.0
@@ -7953,7 +7756,7 @@ packages:
name: hast-util-is-element
version: 2.1.3
dependencies:
'@types/hast': 2.3.4
'@types/hast': registry.npmmirror.com/@types/hast/2.3.4
'@types/unist': registry.npmmirror.com/@types/unist/2.0.6
dev: false
@@ -7968,7 +7771,7 @@ packages:
name: hast-util-parse-selector
version: 3.1.1
dependencies:
'@types/hast': 2.3.4
'@types/hast': registry.npmmirror.com/@types/hast/2.3.4
dev: false
registry.npmmirror.com/hast-util-to-text/3.1.2:
@@ -7993,7 +7796,7 @@ packages:
name: hastscript
version: 6.0.0
dependencies:
'@types/hast': 2.3.4
'@types/hast': registry.npmmirror.com/@types/hast/2.3.4
comma-separated-tokens: registry.npmmirror.com/comma-separated-tokens/1.0.8
hast-util-parse-selector: registry.npmmirror.com/hast-util-parse-selector/2.2.5
property-information: registry.npmmirror.com/property-information/5.6.0
@@ -8005,7 +7808,7 @@ packages:
name: hastscript
version: 7.2.0
dependencies:
'@types/hast': 2.3.4
'@types/hast': registry.npmmirror.com/@types/hast/2.3.4
comma-separated-tokens: registry.npmmirror.com/comma-separated-tokens/2.0.3
hast-util-parse-selector: registry.npmmirror.com/hast-util-parse-selector/3.1.1
property-information: registry.npmmirror.com/property-information/6.2.0
@@ -8959,7 +8762,7 @@ packages:
version: 5.1.2
dependencies:
'@types/mdast': registry.npmmirror.com/@types/mdast/3.0.10
'@types/unist': 2.0.6
'@types/unist': registry.npmmirror.com/@types/unist/2.0.6
unist-util-visit: registry.npmmirror.com/unist-util-visit/4.1.2
dev: false
@@ -9087,7 +8890,7 @@ packages:
name: mdast-util-to-hast
version: 12.3.0
dependencies:
'@types/hast': 2.3.4
'@types/hast': registry.npmmirror.com/@types/hast/2.3.4
'@types/mdast': registry.npmmirror.com/@types/mdast/3.0.10
mdast-util-definitions: registry.npmmirror.com/mdast-util-definitions/5.1.2
micromark-util-sanitize-uri: registry.npmmirror.com/micromark-util-sanitize-uri/1.1.0
@@ -11821,7 +11624,7 @@ packages:
name: unist-util-position
version: 4.0.4
dependencies:
'@types/unist': 2.0.6
'@types/unist': registry.npmmirror.com/@types/unist/2.0.6
dev: false
registry.npmmirror.com/unist-util-remove-position/4.0.2:
@@ -12014,7 +11817,7 @@ packages:
name: vfile-location
version: 4.1.0
dependencies:
'@types/unist': 2.0.6
'@types/unist': registry.npmmirror.com/@types/unist/2.0.6
vfile: registry.npmmirror.com/vfile/5.3.7
dev: false

View File

@@ -1,27 +1,16 @@
### 常见问题
**Git 地址**
[项目地址,完全开源,随便用。](https://github.com/c121914yu/FastGPT)
**问题文档**
[先看文档,再提问](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
**请求次数太多了**
一般是因为自己的 openai 账号异常。请先检查自己的账号是否正常使用。
**内容长度**
chatgpt 上下文最长 4096 tokens, 会自动截取上下文,超过 4096 部分会被遗忘。
**删除和复制**
电脑端:聊天内容右侧有复制和删除的图标。
移动端:点击对话头像,可以选择复制或删除该条内容。
**价格表**
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| claude - 对话 | 免费 |
| chatgpt - 对话 | 0.03 |
| gpt4 - 对话 | 0.5 |
| 知识库 - 索引 | 免费 |
| 文件拆分 | 0.03 |
**代理出错**
服务器代理不稳定,可以过一会儿再尝试。 或者可以访问国外服务器: [FastGpt](https://fastgpt.run/)
**其他问题**
请 WX 联系: fastgpt123
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](https://otnvvf-imgs.oss.laf.run/wxqun300.jpg) | ![](https://otnvvf-imgs.oss.laf.run/wx300.png) |
| ![](/imgs/wxqun300.jpg) | ![](/imgs/wx300.jpg) |

View File

@@ -1,9 +1,7 @@
接受一个 csv 文件,表格头包含 question 和 answer。question 代表问题answer 代表答案。
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
### 请保证 csv 文件为 utf-8 编码
| question | answer |
| ------------- | ------------------------------------------------------ |
| 什么是 laf | laf 是一个云函数开发平台…… |
导入前会进行去重,如果问题和答案完全相同,则不会被导入,所以最终导入的内容可能会比文件的内容少。但是,对于带有换行的内容,目前无法去重。
**请保证 csv 文件为 utf-8 编码**
| question | answer |
| --- | --- |
| 什么是 laf | laf 是一个云函数开发平台…… |
| 什么是 sealos | Sealos 是以 kubernetes 为内核的云操作系统发行版,可以…… |

View File

@@ -29,6 +29,6 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
如果群满了,可加个小助手,定时拉
wx 号: fastgpt123
| 交流群 | 小助手 |
| ------------------------------------------------- | ---------------------------------------------- |
| ![](https://otnvvf-imgs.oss.laf.run/wxqun300.jpg) | ![](https://otnvvf-imgs.oss.laf.run/wx300.png) |
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](/imgs/wxqun300.jpg) | ![](/imgs/wx300.jpg) |

BIN
public/imgs/wx300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,7 +1,8 @@
import { GET, POST, DELETE, PUT } from './request';
import type { ModelSchema } from '@/types/mongoSchema';
import type { ModelSchema, ModelDataSchema } from '@/types/mongoSchema';
import type { ModelUpdateParams, ShareModelItem } from '@/types/model';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
import type { ModelListResponse } from './response/model';
/**
@@ -30,6 +31,72 @@ export const getModelById = (id: string) => GET<ModelSchema>(`/model/detail?mode
export const putModelById = (id: string, data: ModelUpdateParams) =>
PUT(`/model/update?modelId=${id}`, data);
/* 模型 data */
type GetModelDataListProps = RequestPaging & {
modelId: string;
searchText: string;
};
/**
* 获取模型的知识库数据
*/
export const getModelDataList = (props: GetModelDataListProps) =>
GET(`/model/data/getModelData?${Obj2Query(props)}`);
/**
* 获取导出数据(不分页)
*/
export const getExportDataList = (modelId: string) =>
GET<[string, string][]>(`/model/data/exportModelData?modelId=${modelId}`);
/**
* 获取模型正在拆分数据的数量
*/
export const getModelSplitDataListLen = (modelId: string) =>
GET<{
splitDataQueue: number;
embeddingQueue: number;
}>(`/model/data/getTrainingData?modelId=${modelId}`);
/**
* 获取 web 页面内容
*/
export const getWebContent = (url: string) => POST<string>(`/model/data/fetchingUrlData`, { url });
/**
* 手动输入数据
*/
export const postModelDataInput = (data: {
modelId: string;
data: { a: ModelDataSchema['a']; q: ModelDataSchema['q'] }[];
}) => POST<number>(`/model/data/pushModelDataInput`, data);
/**
* 拆分数据
*/
export const postModelDataSplitData = (data: {
modelId: string;
chunks: string[];
prompt: string;
mode: 'qa' | 'subsection';
}) => POST(`/model/data/splitData`, data);
/**
* json导入数据
*/
export const postModelDataCsvData = (modelId: string, data: string[][]) =>
POST<number>(`/model/data/pushModelDataCsv`, { modelId, data: data });
/**
* 更新模型数据
*/
export const putModelDataById = (data: { dataId: string; a: string; q?: string }) =>
PUT('/model/data/putModelData', data);
/**
* 删除一条模型数据
*/
export const delOneModelData = (dataId: string) =>
DELETE(`/model/data/delModelDataById?dataId=${dataId}`);
/* 共享市场 */
/**
* 获取共享市场模型

View File

@@ -1,73 +0,0 @@
import { GET, POST, PUT, DELETE } from '../request';
import type { KbItemType } from '@/types/plugin';
import { RequestPaging } from '@/types/index';
import { SplitTextTypEnum } from '@/constants/plugin';
import { KbDataItemType } from '@/types/plugin';
export type KbUpdateParams = { id: string; name: string; tags: string; avatar: string };
/* knowledge base */
export const getKbList = () => GET<KbItemType[]>(`/plugins/kb/list`);
export const postCreateKb = (data: { name: string }) => POST<string>(`/plugins/kb/create`, data);
export const putKbById = (data: KbUpdateParams) => PUT(`/plugins/kb/update`, data);
export const delKbById = (id: string) => DELETE(`/plugins/kb/delete?id=${id}`);
/* kb data */
type GetKbDataListProps = RequestPaging & {
kbId: string;
searchText: string;
};
export const getKbDataList = (data: GetKbDataListProps) =>
POST(`/plugins/kb/data/getDataList`, data);
/**
* 获取导出数据(不分页)
*/
export const getExportDataList = (kbId: string) =>
GET<[string, string][]>(`/plugins/kb/data/exportModelData?kbId=${kbId}`);
/**
* 获取模型正在拆分数据的数量
*/
export const getTrainingData = (kbId: string) =>
GET<{
splitDataQueue: number;
embeddingQueue: number;
}>(`/plugins/kb/data/getTrainingData?kbId=${kbId}`);
/**
* 获取 web 页面内容
*/
export const getWebContent = (url: string) => POST<string>(`/model/data/fetchingUrlData`, { url });
/**
* 直接push数据
*/
export const postKbDataFromList = (data: {
kbId: string;
data: { a: KbDataItemType['a']; q: KbDataItemType['q'] }[];
}) => POST(`/openapi/kb/pushData`, data);
/**
* 更新一条数据
*/
export const putKbDataById = (data: { dataId: string; a: string; q?: string }) =>
PUT('/openapi/kb/updateData', data);
/**
* 删除一条知识库数据
*/
export const delOneKbDataByDataId = (dataId: string) =>
DELETE(`/openapi/kb/delDataById?dataId=${dataId}`);
/**
* 拆分数据
*/
export const postSplitData = (data: {
kbId: string;
chunks: string[];
prompt: string;
mode: `${SplitTextTypEnum}`;
}) => POST(`/openapi/text/splitText`, data);

View File

@@ -1 +0,0 @@
<?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="1684122143852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2364" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z" p-id="2365"></path></svg>

Before

Width:  |  Height:  |  Size: 1013 B

View File

@@ -1 +0,0 @@
<?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="1684163814302" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3451" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M512 384c-229.8 0-416-57.3-416-128v256c0 70.7 186.2 128 416 128s416-57.3 416-128V256c0 70.7-186.2 128-416 128z" p-id="3452"></path><path d="M512 704c-229.8 0-416-57.3-416-128v256c0 70.7 186.2 128 416 128s416-57.3 416-128V576c0 70.7-186.2 128-416 128zM512 320c229.8 0 416-57.3 416-128S741.8 64 512 64 96 121.3 96 192s186.2 128 416 128z" p-id="3453"></path></svg>

Before

Width:  |  Height:  |  Size: 694 B

View File

@@ -25,9 +25,7 @@ const map = {
tabbarMe: require('./icons/phoneTabbar/me.svg').default,
closeSolid: require('./icons/closeSolid.svg').default,
wx: require('./icons/wx.svg').default,
out: require('./icons/out.svg').default,
git: require('./icons/git.svg').default,
kb: require('./icons/kb.svg').default
out: require('./icons/out.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -7,8 +7,7 @@ import { useQuery } from '@tanstack/react-query';
const unAuthPage: { [key: string]: boolean } = {
'/': true,
'/login': true,
'/model/share': true,
'/chat/share': true
'/model/share': true
};
const Auth = ({ children }: { children: JSX.Element }) => {

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useMemo } from 'react';
import { Box, useColorMode, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useScreen } from '@/hooks/useScreen';
import { useLoading } from '@/hooks/useLoading';
import { useGlobalStore } from '@/store/global';
import { throttle } from 'lodash';
import Auth from './auth';
import Navbar from './navbar';
import NavbarPhone from './navbarPhone';
@@ -19,11 +19,12 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/chat/share': true
};
const Layout = ({ children }: { children: JSX.Element }) => {
const Layout = ({ children, isPcDevice }: { children: JSX.Element; isPcDevice: boolean }) => {
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const router = useRouter();
const { colorMode, setColorMode } = useColorMode();
const { Loading } = useLoading();
const { loading, setScreenWidth, isPc } = useGlobalStore();
const { loading } = useGlobalStore();
const isChatPage = useMemo(
() => router.pathname === '/chat' && Object.values(router.query).join('').length !== 0,
@@ -36,19 +37,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
}
}, [colorMode, router.pathname, setColorMode]);
useEffect(() => {
const resize = throttle(() => {
setScreenWidth(document.documentElement.clientWidth);
}, 300);
resize();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, [setScreenWidth]);
return (
<>
<Box
@@ -87,3 +75,9 @@ const Layout = ({ children }: { children: JSX.Element }) => {
};
export default Layout;
Layout.getInitialProps = ({ req }: any) => {
return {
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};

View File

@@ -22,18 +22,13 @@ const Navbar = () => {
link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`,
activeLink: ['/chat']
},
{
label: 'AI助手',
icon: 'model',
link: `/model?modelId=${lastModelId}`,
activeLink: ['/model']
},
{
label: '知识库',
icon: 'kb',
link: `/kb`,
activeLink: ['/kb']
},
{
label: '共享',
icon: 'shareMarket',
@@ -130,24 +125,6 @@ const Navbar = () => {
</Tooltip>
))}
</Box>
<Box>
<Flex
mb={3}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
cursor={'pointer'}
w={'60px'}
h={'45px'}
color={'#9096a5'}
_hover={{
color: '#ffffff'
}}
onClick={() => window.open('https://github.com/c121914yu/FastGPT')}
>
<MyIcon name={'git'} width={'22px'} height={'22px'} />
</Flex>
</Box>
</Flex>
);
};

View File

@@ -1,40 +1,27 @@
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';
import { useCopyData, formatLinkTextToHtml } from '@/utils/tools';
import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import 'katex/dist/katex.min.css';
import styles from './index.module.scss';
import { codeLight } from './codeLight';
const Markdown = ({
source,
isChatting = false,
formatLink
}: {
source: string;
formatLink?: boolean;
isChatting?: boolean;
}) => {
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
const { copyData } = useCopyData();
const formatSource = useMemo(() => {
return formatLink ? formatLinkTextToHtml(source) : source;
}, [source, formatLink]);
return (
<ReactMarkdown
className={`markdown ${styles.markdown} ${
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
}`}
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeRaw, remarkGfm, rehypeKatex]}
rehypePlugins={[remarkGfm, rehypeKatex]}
components={{
pre: 'div',
code({ node, inline, className, children, ...props }) {
@@ -76,7 +63,7 @@ const Markdown = ({
}}
linkTarget="_blank"
>
{formatSource}
{source}
</ReactMarkdown>
);
};

View File

@@ -1,67 +0,0 @@
import React, { useState } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import type { BoxProps } from '@chakra-ui/react';
import MyIcon from '../Icon';
interface Props extends BoxProps {}
const SideBar = (e?: Props) => {
const {
w = ['100%', '0 0 250px', '0 0 280px', '0 0 310px', '0 0 340px'],
children,
...props
} = e || {};
const [foldSideBar, setFoldSideBar] = useState(false);
return (
<Box
position={'relative'}
flex={foldSideBar ? '0 0 0' : w}
w={['100%', 0]}
h={'100%'}
zIndex={1}
transition={'0.2s'}
_hover={{
'& > div': { visibility: 'visible', opacity: 1 }
}}
{...props}
>
<Flex
position={'absolute'}
right={0}
top={'50%'}
transform={'translate(50%,-50%)'}
alignItems={'center'}
justifyContent={'flex-end'}
pr={1}
w={'36px'}
h={'50px'}
borderRadius={'10px'}
bg={'rgba(0,0,0,0.5)'}
cursor={'pointer'}
transition={'0.2s'}
{...(foldSideBar
? {
opacity: 0.6
}
: {
visibility: 'hidden',
opacity: 0
})}
onClick={() => setFoldSideBar(!foldSideBar)}
>
<MyIcon
name={'back'}
transform={foldSideBar ? 'rotate(180deg)' : ''}
w={'14px'}
color={'white'}
/>
</Flex>
<Box position={'relative'} h={'100%'} overflow={foldSideBar ? 'hidden' : 'visible'}>
{children}
</Box>
</Box>
);
};
export default SideBar;

View File

@@ -23,7 +23,7 @@ const WxConcat = ({ onClose }: { onClose: () => void }) => {
<ModalBody textAlign={'center'}>
<Image
style={{ margin: 'auto' }}
src={'https://otnvvf-imgs.oss.laf.run/wx300.png'}
src={'/imgs/wx300.jpg'}
width={'200px'}
height={'200px'}
alt=""

View File

@@ -1,11 +0,0 @@
import type { KbItemType } from '@/types/plugin';
export const defaultKbDetail: KbItemType = {
_id: '',
userId: '',
updateTime: new Date(),
avatar: '/icon/logo.png',
name: '',
tags: '',
totalData: 0
};

View File

@@ -1,6 +1,6 @@
import { getSystemModelList } from '@/api/system';
import type { ShareChatEditType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/model';
export const embeddingModel = 'text-embedding-ada-002';
export type EmbeddingModelType = 'text-embedding-ada-002';
@@ -142,7 +142,7 @@ export const defaultModel: ModelSchema = {
status: ModelStatusEnum.pending,
updateTime: Date.now(),
chat: {
relatedKbs: [],
useKb: false,
searchMode: ModelVectorSearchModeEnum.hightSimilarity,
systemPrompt: '',
temperature: 0,
@@ -153,6 +153,13 @@ export const defaultModel: ModelSchema = {
isShareDetail: false,
intro: '',
collection: 0
},
security: {
domain: ['*'],
contextMaxLen: 1,
contentMaxLen: 1,
expiredTime: 9999,
maxLoadAmount: 1
}
};

View File

@@ -1,4 +0,0 @@
export enum SplitTextTypEnum {
'qa' = 'qa',
'subsection' = 'subsection'
}

View File

@@ -1,6 +1,6 @@
import { extendTheme, defineStyleConfig, ComponentStyleConfig } from '@chakra-ui/react';
// @ts-ignore
import { modalAnatomy, switchAnatomy, selectAnatomy, checkboxAnatomy } from '@chakra-ui/anatomy';
import { modalAnatomy, switchAnatomy, selectAnatomy } from '@chakra-ui/anatomy';
// @ts-ignore
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
@@ -11,8 +11,6 @@ const { definePartsStyle: switchPart, defineMultiStyleConfig: switchMultiStyle }
createMultiStyleConfigHelpers(switchAnatomy.keys);
const { definePartsStyle: selectPart, defineMultiStyleConfig: selectMultiStyle } =
createMultiStyleConfigHelpers(selectAnatomy.keys);
const { definePartsStyle: checkboxPart, defineMultiStyleConfig: checkboxMultiStyle } =
createMultiStyleConfigHelpers(checkboxAnatomy.keys);
// modal 弹窗
const ModalTheme = defineMultiStyleConfig({
@@ -173,9 +171,6 @@ export const theme = extendTheme({
fontWeight: 400,
height: '100%',
overflow: 'hidden'
},
a: {
color: 'myBlue.700'
}
}
},

View File

@@ -41,7 +41,6 @@ export const usePagination = <T = any,>({
});
console.log(error);
}
return null;
}
});

View File

@@ -4,9 +4,7 @@ function Error({ errStr }: { errStr: string }) {
Error.getInitialProps = ({ res, err }: { res: any; err: any }) => {
console.log(err);
return {
errStr: `部分系统不兼容,导致页面崩溃。如果可以,请联系作者,反馈下具体操作和页面。大部分是 苹果 的 safari 浏览器导致,可以尝试更换 chrome 浏览器。`
};
return { errStr: JSON.stringify(err) };
};
export default Error;

View File

@@ -54,7 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const prompts = [...content, prompt];
// 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) {
if (model.chat.useKb) {
const { code, searchPrompts } = await searchKb({
userOpenAiKey,
prompts,

View File

@@ -50,7 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) {
if (model.chat.useKb) {
const { code, searchPrompts } = await searchKb({
userOpenAiKey,
prompts,

View File

@@ -6,11 +6,11 @@ import { PgClient } from '@/service/pg';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { kbId } = req.query as {
kbId: string;
let { modelId } = req.query as {
modelId: string;
};
if (!kbId) {
if (!modelId) {
throw new Error('缺少参数');
}
@@ -21,11 +21,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 统计数据
const count = await PgClient.count('modelData', {
where: [['kb_id', kbId], 'AND', ['user_id', userId]]
where: [['model_id', modelId], 'AND', ['user_id', userId]]
});
// 从 pg 中获取所有数据
const pgData = await PgClient.select<{ q: string; a: string }>('modelData', {
where: [['kb_id', kbId], 'AND', ['user_id', userId]],
where: [['model_id', modelId], 'AND', ['user_id', userId]],
fields: ['q', 'a'],
order: [{ field: 'id', mode: 'DESC' }],
limit: count

View File

@@ -0,0 +1,34 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import axios from 'axios';
import { axiosConfig } from '@/service/utils/tools';
/**
* 读取网站的内容
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { url } = req.body as { url: string };
if (!url) {
throw new Error('缺少 url');
}
await connectToDatabase();
await authToken(req);
const data = await axios
.get(url, {
httpsAgent: axiosConfig().httpsAgent
})
.then((res) => res.data as string);
jsonRes(res, { data });
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -3,22 +3,27 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import type { PgKBDataItemType } from '@/types/pg';
import type { PgModelDataItemType } from '@/types/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let {
kbId,
modelId,
pageNum = 1,
pageSize = 10,
searchText = ''
} = req.body as {
kbId: string;
pageNum: number;
pageSize: number;
} = req.query as {
modelId: string;
pageNum: string;
pageSize: string;
searchText: string;
};
if (!kbId) {
pageNum = +pageNum;
pageSize = +pageSize;
if (!modelId) {
throw new Error('缺少参数');
}
@@ -27,14 +32,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { model } = await authModel({
userId,
modelId,
authOwner: false
});
const where: any = [
['user_id', userId],
'AND',
['kb_id', kbId],
...(model.share.isShareDetail ? [] : [['user_id', userId], 'AND']),
['model_id', modelId],
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
];
const searchRes = await PgClient.select<PgKBDataItemType>('modelData', {
const searchRes = await PgClient.select<PgModelDataItemType>('modelData', {
fields: ['id', 'q', 'a', 'status'],
where,
order: [{ field: 'id', mode: 'DESC' }],

View File

@@ -8,8 +8,8 @@ import { PgClient } from '@/service/pg';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { kbId } = req.query as { kbId: string };
if (!kbId) {
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
}
await connectToDatabase();
@@ -19,25 +19,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// split queue data
const data = await SplitData.find({
userId,
kbId,
modelId,
textList: { $exists: true, $not: { $size: 0 } }
});
// embedding queue data
const embeddingData = await PgClient.count('modelData', {
where: [
['user_id', userId],
'AND',
['kb_id', kbId],
'AND',
['status', ModelDataStatusEnum.waiting]
]
});
const where: any = [
['user_id', userId],
'AND',
['model_id', modelId],
'AND',
['status', ModelDataStatusEnum.waiting]
];
jsonRes(res, {
data: {
splitDataQueue: data.map((item) => item.textList).flat().length,
embeddingQueue: embeddingData
embeddingQueue: await PgClient.count('modelData', {
where
})
}
});
} catch (err) {

View File

@@ -1,25 +1,20 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { KbDataItemType } from '@/types/plugin';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { generateVector } from '@/service/events/generateVector';
import { ModelDataStatusEnum } from '@/constants/model';
import { PgClient } from '@/service/pg';
import { authKb } from '@/service/utils/auth';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
kbId,
data,
formatLineBreak = true
} = req.body as {
kbId: string;
formatLineBreak?: boolean;
data: { a: KbDataItemType['a']; q: KbDataItemType['q'] }[];
const { modelId, data } = req.body as {
modelId: string;
data: string[][];
};
if (!kbId || !Array.isArray(data)) {
if (!modelId || !Array.isArray(data)) {
throw new Error('缺少参数');
}
@@ -28,27 +23,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
await authKb({
// 验证是否是该用户的 model
await authModel({
userId,
kbId
modelId
});
// 过滤重复的内容
// 去重
const searchRes = await Promise.allSettled(
data.map(async ({ q, a = '' }) => {
data.map(async ([q, a = '']) => {
if (!q) {
return Promise.reject('q为空');
}
if (formatLineBreak) {
try {
q = q.replace(/\\n/g, '\n');
a = a.replace(/\\n/g, '\n');
}
// Exactly the same data, not push
try {
const count = await PgClient.count('modelData', {
where: [['user_id', userId], 'AND', ['kb_id', kbId], 'AND', ['q', q], 'AND', ['a', a]]
where: [
['user_id', userId],
'AND',
['model_id', modelId],
'AND',
['q', q],
'AND',
['a', a]
]
});
if (count > 0) {
return Promise.reject('已经存在');
@@ -62,25 +61,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
});
})
);
// 过滤重复的内容
const filterData = searchRes
.filter((item) => item.status === 'fulfilled')
.map<{ q: string; a: string }>((item: any) => item.value);
// 插入记录
// 插入 pg
const insertRes = await PgClient.insert('modelData', {
values: filterData.map((item) => [
{ key: 'user_id', value: userId },
{ key: 'kb_id', value: kbId },
{ key: 'model_id', value: modelId },
{ key: 'q', value: item.q },
{ key: 'a', value: item.a },
{ key: 'status', value: 'waiting' }
{ key: 'status', value: ModelDataStatusEnum.waiting }
])
});
generateVector();
jsonRes(res, {
message: `共插入 ${insertRes.rowCount} 条数据`,
data: insertRes.rowCount
});
} catch (err) {

View File

@@ -0,0 +1,54 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { ModelDataSchema } from '@/types/mongoSchema';
import { generateVector } from '@/service/events/generateVector';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId, data } = req.body as {
modelId: string;
data: { a: ModelDataSchema['a']; q: ModelDataSchema['q'] }[];
};
if (!modelId || !Array.isArray(data)) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(req);
await connectToDatabase();
// 验证是否是该用户的 model
await authModel({
userId,
modelId
});
// 插入记录
await PgClient.insert('modelData', {
values: data.map((item) => [
{ key: 'user_id', value: userId },
{ key: 'model_id', value: modelId },
{ key: 'q', value: item.q },
{ key: 'a', value: item.a },
{ key: 'status', value: 'waiting' }
])
});
generateVector();
jsonRes(res, {
data: 0
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -16,7 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 凭证校验
const userId = await authToken(req);
// 更新 pg 内容.仅修改a不需要更新向量。
// 更新 pg 内容
await PgClient.update('modelData', {
where: [['id', dataId], 'AND', ['user_id', userId]],
values: [

View File

@@ -1,22 +1,21 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData } from '@/service/mongo';
import { authKb, authToken } from '@/service/utils/auth';
import { authModel, authToken } from '@/service/utils/auth';
import { generateVector } from '@/service/events/generateVector';
import { generateQA } from '@/service/events/generateQA';
import { PgClient } from '@/service/pg';
import { SplitTextTypEnum } from '@/constants/plugin';
/* split text */
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chunks, kbId, prompt, mode } = req.body as {
kbId: string;
const { chunks, modelId, prompt, mode } = req.body as {
modelId: string;
chunks: string[];
prompt: string;
mode: `${SplitTextTypEnum}`;
mode: 'qa' | 'subsection';
};
if (!chunks || !kbId || !prompt) {
if (!chunks || !modelId || !prompt) {
throw new Error('参数错误');
}
await connectToDatabase();
@@ -24,28 +23,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const userId = await authToken(req);
// 验证是否是该用户的 model
await authKb({
kbId,
await authModel({
modelId,
userId
});
if (mode === SplitTextTypEnum.qa) {
if (mode === 'qa') {
// 批量QA拆分插入数据
await SplitData.create({
userId,
kbId,
modelId,
textList: chunks,
prompt
});
generateQA();
} else if (mode === SplitTextTypEnum.subsection) {
// 待优化,直接调用另一个接口
} else if (mode === 'subsection') {
// 插入记录
await PgClient.insert('modelData', {
values: chunks.map((item) => [
{ key: 'user_id', value: userId },
{ key: 'kb_id', value: kbId },
{ key: 'model_id', value: modelId },
{ key: 'q', value: item },
{ key: 'a', value: '' },
{ key: 'status', value: 'waiting' }

View File

@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, Model, connectToDatabase, Collection, ShareChat } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
@@ -24,6 +25,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
userId
});
// 删除 pg 中所有该模型的数据
await PgClient.delete('modelData', {
where: [['user_id', userId], 'AND', ['model_id', modelId]]
});
// 删除对应的聊天
await Chat.deleteMany({
modelId

View File

@@ -9,10 +9,10 @@ import { authModel } from '@/service/utils/auth';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, avatar, chat, share } = req.body as ModelUpdateParams;
const { name, avatar, chat, share, security } = req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
if (!name || !chat || !modelId) {
if (!name || !chat || !security || !modelId) {
throw new Error('参数错误');
}
@@ -38,7 +38,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
chat,
'share.isShare': share.isShare,
'share.isShareDetail': share.isShareDetail,
'share.intro': share.intro
'share.intro': share.intro,
security
}
);

View File

@@ -70,7 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) {
if (model.chat.useKb) {
const similarity = ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22;
const { code, searchPrompts } = await searchKb({

View File

@@ -1,35 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, tags } = req.body as {
name: string;
tags: string[];
};
if (!name) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(req);
await connectToDatabase();
const { _id } = await KB.create({
name,
userId,
tags
});
jsonRes(res, { data: _id });
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,43 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id } = req.query as {
id: string;
};
if (!id) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(req);
await connectToDatabase();
// delete mongo data
await KB.findOneAndDelete({
_id: id,
userId
});
// delete all pg data
// 删除 pg 中所有该模型的数据
await PgClient.delete('modelData', {
where: [['user_id', userId], 'AND', ['kb_id', id]]
});
// delete related model
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,42 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { KbItemType } from '@/types/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
// 凭证校验
const userId = await authToken(req);
await connectToDatabase();
const kbList = await KB.find({
userId
}).sort({ updateTime: -1 });
const data = await Promise.all(
kbList.map(async (item) => ({
_id: item._id,
avatar: item.avatar,
name: item.name,
userId: item.userId,
updateTime: item.updateTime,
tags: item.tags.join(' '),
totalData: await PgClient.count('modelData', {
where: [['user_id', userId], 'AND', ['kb_id', item._id]]
})
}))
);
jsonRes<KbItemType[]>(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,39 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authToken } from '@/service/utils/auth';
import type { KbUpdateParams } from '@/api/plugins/kb';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { id, name, tags, avatar } = req.body as KbUpdateParams;
if (!id || !name) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(req);
await connectToDatabase();
await KB.findOneAndUpdate(
{
_id: id,
userId
},
{
avatar,
name,
tags: tags.split(' ').filter((item) => item)
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -5,10 +5,8 @@ import Markdown from '@/components/Markdown';
import { LOGO_ICON } from '@/constants/chat';
const Empty = ({
showChatProblem,
model: { name, intro, avatar }
}: {
showChatProblem: boolean;
model: {
name: string;
intro: string;
@@ -45,18 +43,13 @@ const Empty = ({
<Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}
{showChatProblem && (
<>
{/* version intro */}
<Card p={4} mb={10}>
<Markdown source={versionIntro} />
</Card>
<Card p={4}>
<Markdown source={chatProblem} />
</Card>
</>
)}
{/* version intro */}
<Card p={4} mb={10}>
<Markdown source={versionIntro} />
</Card>
<Card p={4}>
<Markdown source={chatProblem} />
</Card>
</Box>
);
};

View File

@@ -20,22 +20,24 @@ import { formatTimeToChatTime } from '@/utils/tools';
import MyIcon from '@/components/Icon';
import type { HistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import { useScreen } from '@/hooks/useScreen';
import ModelList from './ModelList';
import { useGlobalStore } from '@/store/global';
import styles from '../index.module.scss';
const PcSliderBar = ({
isPcDevice,
onclickDelHistory,
onclickExportChat
}: {
isPcDevice: boolean;
onclickDelHistory: (historyId: string) => Promise<void>;
onclickExportChat: (type: ExportChatType) => void;
}) => {
const router = useRouter();
const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string };
const theme = useTheme();
const { isPc } = useGlobalStore();
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const ContextMenuRef = useRef(null);

View File

@@ -17,15 +17,17 @@ import { formatTimeToChatTime } from '@/utils/tools';
import MyIcon from '@/components/Icon';
import type { ShareChatHistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import styles from '../index.module.scss';
const PcSliderBar = ({
isPcDevice,
onclickDelHistory,
onclickExportChat,
onCloseSlider
}: {
isPcDevice: boolean;
onclickDelHistory: (historyId: string) => void;
onclickExportChat: (type: ExportChatType) => void;
onCloseSlider: () => void;
@@ -33,7 +35,7 @@ const PcSliderBar = ({
const router = useRouter();
const { shareId = '', historyId = '' } = router.query as { shareId: string; historyId: string };
const theme = useTheme();
const { isPc } = useGlobalStore();
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const ContextMenuRef = useRef(null);

View File

@@ -33,14 +33,15 @@ import {
useTheme
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
import { useCopyData, voiceBroadcast } from '@/utils/tools';
import { streamFetch } from '@/api/fetch';
import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { Types } from 'mongoose';
import Markdown from '@/components/Markdown';
import { LOGO_ICON } from '@/constants/chat';
import { ChatModelMap } from '@/constants/model';
import { useChatStore } from '@/store/chat';
@@ -49,23 +50,34 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
import { useUserStore } from '@/store/user';
import Loading from '@/components/Loading';
import Markdown from '@/components/Markdown';
import SideBar from '@/components/SideBar';
import Empty from './components/Empty';
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const History = dynamic(() => import('./components/History'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const Empty = dynamic(() => import('./components/Empty'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
import styles from './index.module.scss';
const textareaMinH = '22px';
const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const Chat = ({
modelId,
chatId,
isPcDevice
}: {
modelId: string;
chatId: string;
isPcDevice: boolean;
}) => {
const hasVoiceApi = !!window.speechSynthesis;
const router = useRouter();
const theme = useTheme();
@@ -78,6 +90,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const controller = useRef(new AbortController());
const isLeavePage = useRef(false);
const [inputVal, setInputVal] = useState(''); // user input prompt
const [showSystemPrompt, setShowSystemPrompt] = useState('');
const [messageContextMenuData, setMessageContextMenuData] = useState<{
// message messageContextMenuData
@@ -85,6 +98,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
top: number;
message: ChatSiteItemType;
}>();
const [foldSliderBar, setFoldSlideBar] = useState(false);
const {
lastChatModelId,
@@ -105,7 +119,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const { toast } = useToast();
const { copyData } = useCopyData();
const { isPc } = useGlobalStore();
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
@@ -154,8 +168,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
if (!TextareaDom.current) return;
TextareaDom.current.value = val;
setInputVal(val);
setTimeout(() => {
/* 回到最小高度 */
if (TextareaDom.current) {
@@ -276,7 +289,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
* 发送一个内容
*/
const sendPrompt = useCallback(async () => {
// get value
if (isChatting) {
toast({
title: '正在聊天中...请等待结束',
@@ -284,10 +296,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
});
return;
}
// get input value
const value = TextareaDom.current?.value || '';
const val = value.trim().replace(/\n\s*/g, '\n');
const storeInput = inputVal;
// 去除空行
const val = inputVal.trim().replace(/\n\s*/g, '\n');
if (!val) {
toast({
@@ -335,7 +346,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
isClosable: true
});
resetInputVal(value);
resetInputVal(storeInput);
setChatData((state) => ({
...state,
@@ -344,6 +355,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
}
}, [
isChatting,
inputVal,
chatData.history,
setChatData,
resetInputVal,
@@ -473,7 +485,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
navigator.vibrate?.(50); // 震动 50 毫秒
if (!isPc) {
if (!isPcDevice) {
PhoneContextShow.current = true;
}
@@ -485,7 +497,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
return false;
},
[isPc]
[isPcDevice]
);
// 获取对话信息
@@ -510,8 +522,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
status: 'finish'
}))
});
// have records.
if (res.history.length > 0) {
setTimeout(() => {
scrollToBottom('auto');
@@ -589,7 +599,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
AiDetail?: boolean;
}) => (
<MenuList fontSize={'sm'} minW={'100px !important'}>
<MenuItem onClick={() => onclickCopy(history.value)}></MenuItem>
{AiDetail && chatData.model.canUse && history.obj === 'AI' && (
<MenuItem
borderBottom={theme.borders.base}
@@ -598,6 +607,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
AI助手详情
</MenuItem>
)}
<MenuItem onClick={() => onclickCopy(history.value)}></MenuItem>
{hasVoiceApi && (
<MenuItem
borderBottom={theme.borders.base}
@@ -614,6 +624,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
chatData.model.canUse,
chatData.modelId,
delChatRecord,
hasVoiceApi,
onclickCopy,
router,
theme.borders.base
@@ -628,9 +639,61 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
>
{/* pc always show history. */}
{(isPc || !modelId) && (
<SideBar>
<History onclickDelHistory={onclickDelHistory} onclickExportChat={onclickExportChat} />
</SideBar>
<Box
position={'relative'}
flex={foldSliderBar ? '0 0 0' : [1, '0 0 250px', '0 0 280px', '0 0 310px', '0 0 340px']}
w={['100%', 0]}
h={'100%'}
zIndex={1}
transition={'0.2s'}
_hover={{
'& > div': { visibility: 'visible', opacity: 1 }
}}
>
<Flex
position={'absolute'}
right={0}
top={'50%'}
transform={'translate(50%,-50%)'}
alignItems={'center'}
justifyContent={'flex-end'}
pr={1}
w={'36px'}
h={'50px'}
borderRadius={'10px'}
bg={'rgba(0,0,0,0.5)'}
cursor={'pointer'}
transition={'0.2s'}
{...(foldSliderBar
? {
opacity: 0.6
}
: {
visibility: 'hidden',
opacity: 0
})}
onClick={() => setFoldSlideBar(!foldSliderBar)}
>
<MyIcon
name={'back'}
transform={foldSliderBar ? 'rotate(180deg)' : ''}
w={'14px'}
color={'white'}
/>
</Flex>
<Box
position={'relative'}
h={'100%'}
bg={'white'}
overflow={foldSliderBar ? 'hidden' : 'visible'}
>
<History
onclickDelHistory={onclickDelHistory}
onclickExportChat={onclickExportChat}
isPcDevice={isPcDevice}
/>
</Box>
</Box>
)}
{/* 聊天内容 */}
@@ -648,7 +711,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
justifyContent={'space-between'}
py={[3, 5]}
px={5}
borderBottom={'1px solid'}
borderBottom={'1px solid '}
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
color={useColorModeValue('myGray.900', 'white')}
>
@@ -724,10 +787,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
order: 1,
mr: ['6px', 2],
cursor: 'pointer',
onClick: () =>
isPc &&
chatData.model.canUse &&
router.push(`/model?modelId=${chatData.modelId}`)
onClick: () => isPc && router.push(`/model?modelId=${chatData.modelId}`)
}
: {
order: 3,
@@ -739,7 +799,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
className="avatar"
src={
item.obj === 'Human'
? userInfo?.avatar || '/icon/human.png'
? userInfo?.avatar
: chatData.model.avatar || LOGO_ICON
}
alt="avatar"
@@ -766,7 +826,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
<Markdown
source={item.value}
isChatting={isChatting && index === chatData.history.length - 1}
formatLink
/>
{item.systemPrompt && (
<Button
@@ -801,9 +860,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
</Flex>
</Flex>
))}
{chatData.history.length === 0 && (
<Empty model={chatData.model} showChatProblem={true} />
)}
{chatData.history.length === 0 && <Empty model={chatData.model} />}
</Box>
</Box>
{/* 发送区 */}
@@ -829,6 +886,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
}}
placeholder="提问"
resize={'none'}
value={inputVal}
rows={1}
height={'22px'}
lineHeight={'22px'}
@@ -841,12 +899,13 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
color={useColorModeValue('blackAlpha.700', 'white')}
onChange={(e) => {
const textarea = e.target;
setInputVal(textarea.value);
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
}}
onKeyDown={(e) => {
// 触发快捷发送
if (isPc && e.keyCode === 13 && !e.shiftKey) {
if (isPcDevice && e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
@@ -948,7 +1007,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
Chat.getInitialProps = ({ query, req }: any) => {
return {
modelId: query?.modelId || '',
chatId: query?.chatId || ''
chatId: query?.chatId || '',
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};

View File

@@ -31,14 +31,15 @@ import {
ModalHeader
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
import { useCopyData, voiceBroadcast } from '@/utils/tools';
import { streamFetch } from '@/api/fetch';
import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { Types } from 'mongoose';
import Markdown from '@/components/Markdown';
import { LOGO_ICON } from '@/constants/chat';
import { useChatStore } from '@/store/chat';
import { useLoading } from '@/hooks/useLoading';
@@ -46,20 +47,30 @@ import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
import { useUserStore } from '@/store/user';
import Loading from '@/components/Loading';
import Markdown from '@/components/Markdown';
import SideBar from '@/components/SideBar';
import Empty from './components/Empty';
const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const Empty = dynamic(() => import('./components/Empty'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
import styles from './index.module.scss';
const textareaMinH = '22px';
const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) => {
const Chat = ({
shareId,
historyId,
isPcDevice
}: {
shareId: string;
historyId: string;
isPcDevice: boolean;
}) => {
const hasVoiceApi = !!window.speechSynthesis;
const router = useRouter();
const theme = useTheme();
@@ -80,6 +91,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
top: number;
message: ChatSiteItemType;
}>();
const [foldSliderBar, setFoldSlideBar] = useState(false);
const {
password,
@@ -100,7 +112,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
const { toast } = useToast();
const { copyData } = useCopyData();
const { isPc } = useGlobalStore();
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
@@ -418,7 +430,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
navigator.vibrate?.(50); // 震动 50 毫秒
if (!isPc) {
if (!isPcDevice) {
PhoneContextShow.current = true;
}
@@ -430,7 +442,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
return false;
},
[isPc]
[isPcDevice]
);
// 获取对话信息
@@ -442,19 +454,16 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
password
});
const history = shareChatHistory.find((item) => item._id === historyId)?.chats || [];
setShareChatData({
...res,
history
history: shareChatHistory.find((item) => item._id === historyId)?.chats || []
});
onClosePassword();
history.length > 0 &&
setTimeout(() => {
scrollToBottom();
}, 500);
setTimeout(() => {
scrollToBottom();
}, 500);
} catch (e: any) {
toast({
status: 'error',
@@ -524,7 +533,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
<MenuItem onClick={() => delShareChatHistoryItemById(historyId, index)}></MenuItem>
</MenuList>
),
[delShareChatHistoryItemById, historyId, onclickCopy, theme.borders.base]
[delShareChatHistoryItemById, hasVoiceApi, historyId, onclickCopy, theme.borders.base]
);
return (
@@ -535,13 +544,57 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
>
{/* pc always show history. */}
{isPc && (
<SideBar>
<ShareHistory
onclickDelHistory={delShareHistoryById}
onclickExportChat={onclickExportChat}
onCloseSlider={onCloseSlider}
/>
</SideBar>
<Box
position={'relative'}
flex={foldSliderBar ? '0 0 0' : [1, '0 0 250px', '0 0 280px', '0 0 310px', '0 0 340px']}
w={['100%', 0]}
h={'100%'}
zIndex={1}
transition={'0.2s'}
_hover={{
'& > div': { visibility: 'visible', opacity: 1 }
}}
>
<Flex
position={'absolute'}
right={0}
top={'50%'}
transform={'translate(50%,-50%)'}
alignItems={'center'}
justifyContent={'flex-end'}
pr={1}
w={'36px'}
h={'50px'}
borderRadius={'10px'}
bg={'rgba(0,0,0,0.5)'}
cursor={'pointer'}
transition={'0.2s'}
{...(foldSliderBar
? {
opacity: 0.6
}
: {
visibility: 'hidden',
opacity: 0
})}
onClick={() => setFoldSlideBar(!foldSliderBar)}
>
<MyIcon
name={'back'}
transform={foldSliderBar ? 'rotate(180deg)' : ''}
w={'14px'}
color={'white'}
/>
</Flex>
<Box position={'relative'} h={'100%'} bg={'white'} overflow={'hidden'}>
<ShareHistory
onclickDelHistory={delShareHistoryById}
onclickExportChat={onclickExportChat}
onCloseSlider={onCloseSlider}
isPcDevice={isPcDevice}
/>
</Box>
</Box>
)}
{/* 聊天内容 */}
@@ -571,7 +624,13 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
onClick={onOpenSlider}
/>
)}
<Box lineHeight={1.2} textAlign={'center'} px={3} fontSize={['sm', 'md']}>
<Box
cursor={'pointer'}
lineHeight={1.2}
textAlign={'center'}
px={3}
fontSize={['sm', 'md']}
>
{shareChatData.model.name}
{shareChatData.history.length > 0 ? ` (${shareChatData.history.length})` : ''}
</Box>
@@ -631,7 +690,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
className="avatar"
src={
item.obj === 'Human'
? userInfo?.avatar || '/icon/human.png'
? userInfo?.avatar
: shareChatData.model.avatar || LOGO_ICON
}
alt="avatar"
@@ -658,7 +717,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
<Markdown
source={item.value}
isChatting={isChatting && index === shareChatData.history.length - 1}
formatLink
/>
{item.systemPrompt && (
<Button
@@ -693,9 +751,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
</Flex>
</Flex>
))}
{shareChatData.history.length === 0 && (
<Empty model={shareChatData.model} showChatProblem={false} />
)}
{shareChatData.history.length === 0 && <Empty model={shareChatData.model} />}
</Box>
</Box>
{/* 发送区 */}
@@ -739,7 +795,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
}}
onKeyDown={(e) => {
// 触发快捷发送
if (isPc && e.keyCode === 13 && !e.shiftKey) {
if (isPcDevice && e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
@@ -796,6 +852,7 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
onclickDelHistory={delShareHistoryById}
onclickExportChat={onclickExportChat}
onCloseSlider={onCloseSlider}
isPcDevice={isPcDevice}
/>
</DrawerContent>
</Drawer>
@@ -865,7 +922,8 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
Chat.getInitialProps = ({ query, req }: any) => {
return {
shareId: query?.shareId || '',
historyId: query?.historyId || ''
historyId: query?.historyId || '',
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};

View File

@@ -4,8 +4,8 @@ import Markdown from '@/components/Markdown';
import { useMarkdown } from '@/hooks/useMarkdown';
import { getFilling } from '@/api/system';
import { useQuery } from '@tanstack/react-query';
import { useScreen } from '@/hooks/useScreen';
import { useRouter } from 'next/router';
import { useGlobalStore } from '@/store/global';
import styles from './index.module.scss';
@@ -13,7 +13,7 @@ const Home = () => {
const router = useRouter();
const { inviterId } = router.query as { inviterId: string };
const { data } = useMarkdown({ url: '/intro.md' });
const { isPc } = useGlobalStore();
const { isPc } = useScreen();
useEffect(() => {
if (inviterId) {

View File

@@ -1,305 +0,0 @@
import React, { useCallback, useState, useRef } from 'react';
import {
Box,
TableContainer,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
IconButton,
Flex,
Button,
useDisclosure,
Menu,
MenuButton,
MenuList,
MenuItem,
Input,
Tooltip
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { BoxProps } from '@chakra-ui/react';
import type { KbDataItemType } from '@/types/plugin';
import { ModelDataStatusMap } from '@/constants/model';
import { usePagination } from '@/hooks/usePagination';
import {
getKbDataList,
getExportDataList,
delOneKbDataByDataId,
getTrainingData
} from '@/api/plugins/kb';
import { DeleteIcon, RepeatIcon, EditIcon } from '@chakra-ui/icons';
import { useLoading } from '@/hooks/useLoading';
import { fileDownload } from '@/utils/file';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useToast } from '@/hooks/useToast';
import Papa from 'papaparse';
import dynamic from 'next/dynamic';
import InputModal, { FormData as InputDataType } from './InputDataModal';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
const DataCard = ({ kbId }: { kbId: string }) => {
const lastSearch = useRef('');
const tdStyles = useRef<BoxProps>({
fontSize: 'xs',
minW: '150px',
maxW: '500px',
maxH: '250px',
whiteSpace: 'pre-wrap',
overflowY: 'auto'
});
const [searchText, setSearchText] = useState('');
const { Loading, setIsLoading } = useLoading();
const { toast } = useToast();
const {
data: modelDataList,
isLoading,
Pagination,
total,
getData,
pageNum
} = usePagination<KbDataItemType>({
api: getKbDataList,
pageSize: 10,
params: {
kbId,
searchText
},
defaultRequest: false
});
useQuery(['getKbData', kbId], () => {
getData(1);
return null;
});
const [editInputData, setEditInputData] = useState<InputDataType>();
const {
isOpen: isOpenSelectFileModal,
onOpen: onOpenSelectFileModal,
onClose: onCloseSelectFileModal
} = useDisclosure();
const {
isOpen: isOpenSelectCsvModal,
onOpen: onOpenSelectCsvModal,
onClose: onCloseSelectCsvModal
} = useDisclosure();
const { data: { splitDataQueue = 0, embeddingQueue = 0 } = {}, refetch } = useQuery(
['getModelSplitDataList'],
() => getTrainingData(kbId),
{
onError(err) {
console.log(err);
}
}
);
const refetchData = useCallback(
(num = 1) => {
getData(num);
refetch();
return null;
},
[getData, refetch]
);
// interval get data
useQuery(['refetchData'], () => refetchData(pageNum), {
refetchInterval: 5000,
enabled: splitDataQueue > 0 || embeddingQueue > 0
});
// get al data and export csv
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
mutationFn: () => getExportDataList(kbId),
onSuccess(res) {
try {
setIsLoading(true);
const text = Papa.unparse({
fields: ['question', 'answer'],
data: res
});
fileDownload({
text,
type: 'text/csv',
filename: 'data.csv'
});
} catch (error) {
error;
}
setIsLoading(false);
},
onError(err: any) {
toast({
title: typeof err === 'string' ? err : err?.message || '导出异常',
status: 'error'
});
console.log(err);
}
});
return (
<Box position={'relative'}>
<Flex>
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
: {total}
</Box>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={[2, 4]}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</Flex>
<Flex mt={4}>
{(splitDataQueue > 0 || embeddingQueue > 0) && (
<Box fontSize={'xs'}>
{splitDataQueue > 0 ? `${splitDataQueue}条数据正在拆分,` : ''}
{embeddingQueue > 0 ? `${embeddingQueue}条数据正在生成索引,` : ''}
...
</Box>
)}
<Box flex={1} />
<Input
maxW={'240px'}
size={'sm'}
value={searchText}
placeholder="搜索相关问题和答案,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}
}}
/>
</Flex>
<TableContainer mt={4} minH={'200px'}>
<Table>
<Thead>
<Tr>
<Th>
<Tooltip
label={
'对话时,会将用户的问题和知识库的 "匹配知识点" 进行比较,找到最相似的前 n 条记录,将这些记录的 "匹配知识点"+"补充知识点" 作为 chatgpt 的系统提示词。'
}
>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{modelDataList.map((item) => (
<Tr key={item.id}>
<Td>
<Box {...tdStyles.current}>{item.q}</Box>
</Td>
<Td>
<Box {...tdStyles.current}>{item.a || '-'}</Box>
</Td>
<Td>{ModelDataStatusMap[item.status]}</Td>
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneKbDataByDataId(item.id);
refetchData(pageNum);
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Flex mt={2} justifyContent={'flex-end'}>
<Pagination />
</Flex>
<Loading loading={isLoading} fixed={false} />
{editInputData !== undefined && (
<InputModal
kbId={kbId}
defaultValues={editInputData}
onClose={() => setEditInputData(undefined)}
onSuccess={refetchData}
/>
)}
{isOpenSelectFileModal && (
<SelectFileModal kbId={kbId} onClose={onCloseSelectFileModal} onSuccess={refetchData} />
)}
{isOpenSelectCsvModal && (
<SelectCsvModal kbId={kbId} onClose={onCloseSelectCsvModal} onSuccess={refetchData} />
)}
</Box>
);
};
export default DataCard;

View File

@@ -1,245 +0,0 @@
import React, { useCallback, useState, useRef } from 'react';
import { useRouter } from 'next/router';
import {
Card,
Box,
Flex,
Button,
Tooltip,
Image,
FormControl,
Input,
Tag,
IconButton
} from '@chakra-ui/react';
import { QuestionOutlineIcon, DeleteIcon } from '@chakra-ui/icons';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user';
import { delKbById, putKbById } from '@/api/plugins/kb';
import { useLoading } from '@/hooks/useLoading';
import { KbItemType } from '@/types/plugin';
import { useSelectFile } from '@/hooks/useSelectFile';
import { useConfirm } from '@/hooks/useConfirm';
import { compressImg } from '@/utils/file';
import DataCard from './DataCard';
const Detail = ({ kbId }: { kbId: string }) => {
const { toast } = useToast();
const router = useRouter();
const InputRef = useRef<HTMLInputElement>(null);
const { setLastKbId, KbDetail, getKbDetail, loadKbList, myKbList } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [btnLoading, setBtnLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const { getValues, formState, setValue, reset, register, handleSubmit } = useForm<KbItemType>({
defaultValues: KbDetail
});
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该知识库?数据将无法恢复,请确认!'
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { isLoading } = useQuery([kbId, myKbList], () => getKbDetail(kbId), {
onSuccess(res) {
kbId && setLastKbId(kbId);
if (res) {
reset(res);
if (InputRef.current) {
InputRef.current.value = res.tags;
}
}
},
onError(err: any) {
toast({
title: err?.message || '获取AI助手异常',
status: 'error'
});
setLastKbId('');
router.replace('/model');
}
});
/* 点击删除 */
const onclickDelKb = useCallback(async () => {
setIsLoading(true);
try {
await delKbById(kbId);
toast({
title: '删除成功',
status: 'success'
});
router.replace(`/kb?kbId=${myKbList.find((item) => item._id !== kbId)?._id || ''}`);
await loadKbList(true);
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setIsLoading(false);
}, [setIsLoading, kbId, toast, router, myKbList, loadKbList]);
const saveSubmitSuccess = useCallback(
async (data: KbItemType) => {
setBtnLoading(true);
try {
await putKbById({
id: kbId,
...data
});
toast({
title: '更新成功',
status: 'success'
});
loadKbList(true);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setBtnLoading(false);
},
[kbId, loadKbList, toast]
);
const saveSubmitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return '提交表单错误';
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(formState.errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [formState.errors, toast]);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const base64 = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', base64);
loadKbList(true);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : '头像选择异常',
status: 'warning'
});
}
},
[loadKbList, setValue, toast]
);
return (
<Box h={'100%'} p={5} overflow={'overlay'} position={'relative'}>
<Card p={6}>
<Flex>
<Box fontWeight={'bold'} fontSize={'2xl'} flex={1}>
</Box>
{KbDetail._id && (
<>
<Button
isLoading={btnLoading}
mr={3}
onClick={handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
<IconButton
isLoading={btnLoading}
icon={<DeleteIcon />}
aria-label={''}
variant={'solid'}
colorScheme={'red'}
onClick={openConfirm(onclickDelKb)}
/>
</>
)}
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 60px'} w={0}>
</Box>
<Image
src={getValues('avatar') || '/icon/logo.png'}
alt={'avatar'}
w={['28px', '36px']}
h={['28px', '36px']}
objectFit={'cover'}
cursor={'pointer'}
title={'点击切换头像'}
onClick={onOpenSelectFile}
/>
</Flex>
<FormControl mt={5}>
<Flex alignItems={'center'} maxW={'350px'}>
<Box flex={'0 0 60px'} w={0}>
</Box>
<Input
{...register('name', {
required: '知识库名称不能为空'
})}
/>
</Flex>
</FormControl>
<Box>
<Flex mt={5} alignItems={'center'} maxW={'350px'} flexWrap={'wrap'}>
<Box flex={'0 0 60px'} w={0}>
<Tooltip label={'仅用于记忆,用空格隔开多个标签'}>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Box>
<Input
flex={1}
ref={InputRef}
placeholder={'标签,使用空格分割。'}
onChange={(e) => {
setValue('tags', e.target.value);
setRefresh(!refresh);
}}
/>
<Box pl={'60px'} mt={2} w="100%">
{getValues('tags')
.split(' ')
.filter((item) => item)
.map((item, i) => (
<Tag mr={2} mb={2} key={i} variant={'outline'} colorScheme={'blue'}>
{item}
</Tag>
))}
</Box>
</Flex>
</Box>
</Card>
<Card p={6} mt={5}>
<DataCard kbId={kbId} />
</Card>
<File onSelect={onSelectFile} />
<ConfirmChild />
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default Detail;

View File

@@ -1,155 +0,0 @@
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Flex, useTheme, Input, IconButton, Tooltip, Image, Tag } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { useRouter } from 'next/router';
import { postCreateKb } from '@/api/plugins/kb';
import { useLoading } from '@/hooks/useLoading';
import { useToast } from '@/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user';
import MyIcon from '@/components/Icon';
const KbList = ({ kbId }: { kbId: string }) => {
const theme = useTheme();
const router = useRouter();
const { toast } = useToast();
const { Loading, setIsLoading } = useLoading();
const { myKbList, loadKbList } = useUserStore();
const [searchText, setSearchText] = useState('');
const kbs = useMemo(
() => myKbList.filter((item) => new RegExp(searchText, 'ig').test(item.name + item.tags)),
[myKbList, searchText]
);
/* 加载模型 */
const { isLoading } = useQuery(['loadModels'], () => loadKbList(false));
const handleCreateModel = useCallback(async () => {
setIsLoading(true);
try {
const name = `知识库${myKbList.length + 1}`;
const id = await postCreateKb({ name });
await loadKbList(true);
toast({
title: '创建成功',
status: 'success'
});
router.replace(`/kb?kbId=${id}`);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err.message || '出现了意外',
status: 'error'
});
}
setIsLoading(false);
}, [loadKbList, myKbList.length, router, setIsLoading, toast]);
return (
<Flex
position={'relative'}
flexDirection={'column'}
w={'100%'}
h={'100%'}
bg={'white'}
borderRight={['', theme.borders.base]}
>
<Flex w={'90%'} my={5} mx={'auto'}>
<Flex flex={1} mr={2} position={'relative'} alignItems={'center'}>
<Input
h={'32px'}
placeholder="搜索知识库"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
{searchText && (
<MyIcon
zIndex={10}
position={'absolute'}
right={3}
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.500'}
cursor={'pointer'}
onClick={() => setSearchText('')}
/>
)}
</Flex>
<Tooltip label={'新建一个知识库'}>
<IconButton
h={'32px'}
icon={<AddIcon />}
aria-label={''}
variant={'outline'}
onClick={handleCreateModel}
/>
</Tooltip>
</Flex>
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
{kbs.map((item) => (
<Flex
key={item._id}
position={'relative'}
alignItems={['flex-start', 'center']}
p={3}
mb={[2, 0]}
cursor={'pointer'}
transition={'background-color .2s ease-in'}
borderLeft={['', '5px solid transparent']}
_hover={{
backgroundColor: ['', '#dee0e3']
}}
{...(kbId === item._id
? {
backgroundColor: '#eff0f1',
borderLeftColor: 'myBlue.600 !important'
}
: {})}
onClick={() => {
if (item._id === kbId) return;
router.push(`/kb?kbId=${item._id}`);
}}
>
<Image
src={item.avatar || '/icon/logo.png'}
alt=""
w={'34px'}
maxH={'50px'}
objectFit={'contain'}
/>
<Box flex={'1 0 0'} w={0} ml={3}>
<Box className="textEllipsis" color={'myGray.1000'}>
{item.name}
</Box>
{/* tags */}
<Box className="textEllipsis" color={'myGray.400'} mt={1} fontSize={'sm'}>
{!item.tags ? (
<>{item.tags || '你还没设置标签~'}</>
) : (
item.tags.split(' ').map((item, i) => (
<Tag key={i} mr={2} mb={2} variant={'outline'} colorScheme={'blue'} size={'sm'}>
{item}
</Tag>
))
)}
</Box>
</Box>
</Flex>
))}
{!isLoading && myKbList.length === 0 && (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
~
</Box>
</Flex>
)}
</Box>
<Loading loading={isLoading} fixed={false} />
</Flex>
);
};
export default KbList;

View File

@@ -1,43 +0,0 @@
import React, { useEffect } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useGlobalStore } from '@/store/global';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import SideBar from '@/components/SideBar';
import KbList from './components/KbList';
import KbDetail from './components/Detail';
const Kb = ({ kbId }: { kbId: string }) => {
const router = useRouter();
const { isPc } = useGlobalStore();
const { lastKbId } = useUserStore();
// redirect
useEffect(() => {
if (isPc && !kbId && lastKbId) {
router.replace(`/kb?kbId=${lastKbId}`);
}
}, [isPc, kbId, lastKbId, router]);
return (
<Flex h={'100%'} position={'relative'} overflow={'hidden'}>
{/* 模型列表 */}
{(isPc || !kbId) && (
<SideBar w={['100%', '0 0 250px', '0 0 270px', '0 0 290px']}>
<KbList kbId={kbId} />
</SideBar>
)}
<Box flex={'1 0 0'} w={0} h={'100%'} position={'relative'}>
{kbId && <KbDetail kbId={kbId} />}
</Box>
</Flex>
);
};
export default Kb;
Kb.getInitialProps = ({ query, req }: any) => {
return {
kbId: query?.kbId || ''
};
};

View File

@@ -5,6 +5,7 @@ import { PageTypeEnum } from '../../../constants/user';
import { postFindPassword } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
interface Props {
@@ -21,6 +22,7 @@ interface RegisterType {
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
handleSubmit,
@@ -79,7 +81,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl mt={5} isInvalid={!!errors.username}>
<Input
placeholder="邮箱/手机号"
size={['md', 'lg']}
size={mediaLgMd}
{...register('username', {
required: '邮箱/手机号不能为空',
pattern: {
@@ -98,7 +100,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
flex={1}
placeholder="验证码"
size={['md', 'lg']}
size={mediaLgMd}
{...register('code', {
required: '验证码不能为空'
})}
@@ -107,7 +109,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
ml={5}
w={'145px'}
maxW={'50%'}
size={['md', 'lg']}
size={mediaLgMd}
onClick={onclickSendCode}
isDisabled={codeCountDown > 0}
isLoading={codeSending}
@@ -123,7 +125,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
type={'password'}
placeholder="新密码"
size={['md', 'lg']}
size={mediaLgMd}
{...register('password', {
required: '密码不能为空',
minLength: {
@@ -144,7 +146,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
type={'password'}
placeholder="确认密码"
size={['md', 'lg']}
size={mediaLgMd}
{...register('password2', {
validate: (val) => (getValues('password') === val ? true : '两次密码不一致')
})}
@@ -168,7 +170,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
type="submit"
mt={5}
w={'100%'}
size={['md', 'lg']}
size={mediaLgMd}
colorScheme="blue"
isLoading={requesting}
>

View File

@@ -5,6 +5,7 @@ import { PageTypeEnum } from '@/constants/user';
import { postLogin } from '@/api/user';
import type { ResLogin } from '@/api/response/user';
import { useToast } from '@/hooks/useToast';
import { useScreen } from '@/hooks/useScreen';
interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>;
@@ -18,6 +19,7 @@ interface LoginFormType {
const LoginForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
handleSubmit,
@@ -60,7 +62,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl mt={8} isInvalid={!!errors.username}>
<Input
placeholder="邮箱/手机号"
size={['md', 'lg']}
size={mediaLgMd}
{...register('username', {
required: '邮箱/手机号不能为空',
pattern: {
@@ -77,7 +79,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl mt={8} isInvalid={!!errors.password}>
<Input
type={'password'}
size={['md', 'lg']}
size={mediaLgMd}
placeholder="密码"
{...register('password', {
required: '密码不能为空',
@@ -117,7 +119,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
type="submit"
mt={8}
w={'100%'}
size={['md', 'lg']}
size={mediaLgMd}
colorScheme="blue"
isLoading={requesting}
>

View File

@@ -5,6 +5,7 @@ import { PageTypeEnum } from '@/constants/user';
import { postRegister } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
import { useRouter } from 'next/router';
import { postCreateModel } from '@/api/model';
@@ -24,6 +25,7 @@ interface RegisterType {
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const { inviterId = '' } = useRouter().query as { inviterId: string };
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
handleSubmit,
@@ -87,7 +89,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<FormControl mt={5} isInvalid={!!errors.username}>
<Input
placeholder="邮箱/手机号"
size={['md', 'lg']}
size={mediaLgMd}
{...register('username', {
required: '邮箱/手机号不能为空',
pattern: {
@@ -105,7 +107,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Flex>
<Input
flex={1}
size={['md', 'lg']}
size={mediaLgMd}
placeholder="验证码"
{...register('code', {
required: '验证码不能为空'
@@ -115,7 +117,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
ml={5}
w={'145px'}
maxW={'50%'}
size={['md', 'lg']}
size={mediaLgMd}
onClick={onclickSendCode}
isDisabled={codeCountDown > 0}
isLoading={codeSending}
@@ -131,7 +133,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
type={'password'}
placeholder="密码"
size={['md', 'lg']}
size={mediaLgMd}
{...register('password', {
required: '密码不能为空',
minLength: {
@@ -152,7 +154,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Input
type={'password'}
placeholder="确认密码"
size={['md', 'lg']}
size={mediaLgMd}
{...register('password2', {
validate: (val) => (getValues('password') === val ? true : '两次密码不一致')
})}
@@ -176,7 +178,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
type="submit"
mt={5}
w={'100%'}
size={['md', 'lg']}
size={mediaLgMd}
colorScheme="blue"
isLoading={requesting}
>

View File

@@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react';
import styles from './index.module.scss';
import { Box, Flex, Image } from '@chakra-ui/react';
import { PageTypeEnum } from '@/constants/user';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import type { ResLogin } from '@/api/response/user';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
@@ -12,10 +12,10 @@ import dynamic from 'next/dynamic';
const RegisterForm = dynamic(() => import('./components/RegisterForm'));
const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm'));
const Login = () => {
const Login = ({ isPcDevice }: { isPcDevice: boolean }) => {
const router = useRouter();
const { lastRoute = '' } = router.query as { lastRoute: string };
const { isPc } = useGlobalStore();
const { isPc } = useScreen({ defaultIsPc: isPcDevice });
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo, setLastModelId, loadMyModels } = useUserStore();
const { setLastChatId, setLastChatModelId, loadHistory } = useChatStore();
@@ -114,3 +114,9 @@ const Login = () => {
};
export default Login;
Login.getInitialProps = ({ query, req }: any) => {
return {
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};

View File

@@ -126,7 +126,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
{...(modelId === item._id
? {
backgroundColor: '#eff0f1',
borderLeftColor: 'myBlue.600 !important'
borderLeftColor: 'myBlue.600'
}
: {})}
onClick={() => {

View File

@@ -11,15 +11,17 @@ import {
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { postKbDataFromList, putKbDataById } from '@/api/plugins/kb';
import { postModelDataInput, putModelDataById } from '@/api/model';
import { useToast } from '@/hooks/useToast';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
export type FormData = { dataId?: string; a: string; q: string };
const InputDataModal = ({
onClose,
onSuccess,
kbId,
modelId,
defaultValues = {
a: '',
q: ''
@@ -27,7 +29,7 @@ const InputDataModal = ({
}: {
onClose: () => void;
onSuccess: () => void;
kbId: string;
modelId: string;
defaultValues?: FormData;
}) => {
const [importing, setImporting] = useState(false);
@@ -52,8 +54,8 @@ const InputDataModal = ({
setImporting(true);
try {
const res = await postKbDataFromList({
kbId,
const res = await postModelDataInput({
modelId: modelId,
data: [
{
a: e.a,
@@ -63,8 +65,8 @@ const InputDataModal = ({
});
toast({
title: res === 0 ? '可能已存在完全一致的数据' : '导入数据成功,需要一段时间训练',
status: 'success'
title: res === 0 ? '导入数据成功,需要一段时间训练' : '数据导入异常',
status: res === 0 ? 'success' : 'warning'
});
reset({
a: '',
@@ -80,7 +82,7 @@ const InputDataModal = ({
}
setImporting(false);
},
[kbId, onSuccess, reset, toast]
[modelId, onSuccess, reset, toast]
);
const updateData = useCallback(
@@ -88,7 +90,7 @@ const InputDataModal = ({
if (!e.dataId) return;
if (e.a !== defaultValues.a || e.q !== defaultValues.q) {
await putKbDataById({
await putModelDataById({
dataId: e.dataId,
a: e.a,
q: e.q === defaultValues.q ? '' : e.q

View File

@@ -0,0 +1,310 @@
import React, { useCallback, useState, useRef, useEffect } from 'react';
import {
Box,
TableContainer,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
IconButton,
Flex,
Button,
useDisclosure,
Menu,
MenuButton,
MenuList,
MenuItem,
Input,
Tooltip
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { BoxProps } from '@chakra-ui/react';
import type { ModelDataItemType } from '@/types/model';
import { ModelDataStatusMap } from '@/constants/model';
import { usePagination } from '@/hooks/usePagination';
import {
getModelDataList,
delOneModelData,
getModelSplitDataListLen,
getExportDataList
} from '@/api/model';
import { DeleteIcon, RepeatIcon, EditIcon } from '@chakra-ui/icons';
import { useLoading } from '@/hooks/useLoading';
import { fileDownload } from '@/utils/file';
import dynamic from 'next/dynamic';
import { useMutation, useQuery } from '@tanstack/react-query';
import Papa from 'papaparse';
import InputModal, { FormData as InputDataType } from './InputDataModal';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean }) => {
const { Loading, setIsLoading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
const tdStyles = useRef<BoxProps>({
fontSize: 'xs',
minW: '150px',
maxW: '500px',
maxH: '250px',
whiteSpace: 'pre-wrap',
overflowY: 'auto'
});
const {
data: modelDataList,
isLoading,
Pagination,
total,
getData,
pageNum
} = usePagination<ModelDataItemType>({
api: getModelDataList,
pageSize: 10,
params: {
modelId,
searchText
},
defaultRequest: false
});
useEffect(() => {
getData(1);
}, [modelId, getData]);
const [editInputData, setEditInputData] = useState<InputDataType>();
const {
isOpen: isOpenSelectFileModal,
onOpen: onOpenSelectFileModal,
onClose: onCloseSelectFileModal
} = useDisclosure();
const {
isOpen: isOpenSelectCsvModal,
onOpen: onOpenSelectCsvModal,
onClose: onCloseSelectCsvModal
} = useDisclosure();
const { data: { splitDataQueue = 0, embeddingQueue = 0 } = {}, refetch } = useQuery(
['getModelSplitDataList'],
() => getModelSplitDataListLen(modelId),
{
onError(err) {
console.log(err);
}
}
);
const refetchData = useCallback(
(num = 1) => {
getData(num);
refetch();
return null;
},
[getData, refetch]
);
useQuery(['refetchData'], () => refetchData(pageNum), {
refetchInterval: 5000,
enabled: splitDataQueue > 0 || embeddingQueue > 0
});
// 获取所有的数据,并导出 json
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
mutationFn: () => getExportDataList(modelId),
onSuccess(res) {
try {
setIsLoading(true);
const text = Papa.unparse({
fields: ['question', 'answer'],
data: res
});
fileDownload({
text,
type: 'text/csv',
filename: 'data.csv'
});
} catch (error) {
error;
}
setIsLoading(false);
},
onError(err) {
console.log(err);
}
});
return (
<Box position={'relative'}>
<Flex>
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
: {total}
</Box>
{isOwner && (
<>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
size={'sm'}
onClick={() => refetchData(pageNum)}
/>
<Button
variant={'outline'}
mr={2}
size={'sm'}
isLoading={isLoadingExport}
title={'换行数据导出时,会进行格式转换'}
onClick={() => onclickExport()}
>
</Button>
<Menu autoSelect={false}>
<MenuButton as={Button} size={'sm'}>
</MenuButton>
<MenuList>
<MenuItem
onClick={() =>
setEditInputData({
a: '',
q: ''
})
}
>
</MenuItem>
<MenuItem onClick={onOpenSelectFileModal}>/</MenuItem>
<MenuItem onClick={onOpenSelectCsvModal}>csv </MenuItem>
</MenuList>
</Menu>
</>
)}
</Flex>
<Flex mt={4}>
{isOwner && (splitDataQueue > 0 || embeddingQueue > 0) && (
<Box fontSize={'xs'}>
{splitDataQueue > 0 ? `${splitDataQueue}条数据正在拆分,` : ''}
{embeddingQueue > 0 ? `${embeddingQueue}条数据正在生成索引,` : ''}
...
</Box>
)}
<Box flex={1} />
<Input
maxW={'240px'}
size={'sm'}
value={searchText}
placeholder="搜索相关问题和答案,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
getData(1);
lastSearch.current = searchText;
}}
onKeyDown={(e) => {
if (searchText === lastSearch.current) return;
if (e.key === 'Enter') {
getData(1);
lastSearch.current = searchText;
}
}}
/>
</Flex>
<Box mt={4}>
<TableContainer minH={'500px'}>
<Table variant={'simple'} w={'100%'}>
<Thead>
<Tr>
<Th>
<Tooltip
label={
'对话时,会将用户的问题和知识库的 "匹配知识点" 进行比较,找到最相似的前 n 条记录,将这些记录的 "匹配知识点"+"补充知识点" 作为 chatgpt 的系统提示词。'
}
>
<QuestionOutlineIcon ml={1} />
</Tooltip>
</Th>
<Th></Th>
<Th></Th>
{isOwner && <Th></Th>}
</Tr>
</Thead>
<Tbody>
{modelDataList.map((item) => (
<Tr key={item.id}>
<Td>
<Box {...tdStyles.current}>{item.q}</Box>
</Td>
<Td>
<Box {...tdStyles.current}>{item.a || '-'}</Box>
</Td>
<Td>{ModelDataStatusMap[item.status]}</Td>
{isOwner && (
<Td>
<IconButton
mr={5}
icon={<EditIcon />}
variant={'outline'}
aria-label={'delete'}
size={'sm'}
onClick={() =>
setEditInputData({
dataId: item.id,
q: item.q,
a: item.a
})
}
/>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
await delOneModelData(item.id);
refetchData(pageNum);
}}
/>
</Td>
)}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Flex mt={2} justifyContent={'flex-end'}>
<Pagination />
</Flex>
</Box>
<Loading loading={isLoading} fixed={false} />
{editInputData !== undefined && (
<InputModal
modelId={modelId}
defaultValues={editInputData}
onClose={() => setEditInputData(undefined)}
onSuccess={refetchData}
/>
)}
{isOpenSelectFileModal && (
<SelectFileModal
modelId={modelId}
onClose={onCloseSelectFileModal}
onSuccess={refetchData}
/>
)}
{isOpenSelectCsvModal && (
<SelectCsvModal modelId={modelId} onClose={onCloseSelectCsvModal} onSuccess={refetchData} />
)}
</Box>
);
};
export default ModelDataCard;

View File

@@ -32,9 +32,11 @@ import {
Th,
Td,
TableContainer,
Checkbox
IconButton
} from '@chakra-ui/react';
import { DeleteIcon } from '@chakra-ui/icons';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
import { useForm, UseFormReturn } from 'react-hook-form';
import { ChatModelMap, ModelVectorSearchModeMap, getChatModelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
@@ -47,12 +49,9 @@ import { getShareChatList, createShareChat, delShareChatById } from '@/api/chat'
import { useRouter } from 'next/router';
import { defaultShareChat } from '@/constants/model';
import type { ShareChatEditType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { formatTimeToChatTime, useCopyData } from '@/utils/tools';
import MyIcon from '@/components/Icon';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import type { KbItemType } from '@/types/plugin';
const ModelEditForm = ({
formHooks,
@@ -63,11 +62,10 @@ const ModelEditForm = ({
isOwner: boolean;
handleDelModel: () => void;
}) => {
const { modelId } = useRouter().query as { modelId: string };
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const { modelId } = useRouter().query as { modelId: string };
const { setLoading } = useGlobalStore();
const { loadKbList } = useUserStore();
const [refresh, setRefresh] = useState(false);
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该AI助手?'
@@ -88,11 +86,6 @@ const ModelEditForm = ({
onOpen: onOpenCreateShareChat,
onClose: onCloseCreateShareChat
} = useDisclosure();
const {
isOpen: isOpenKbSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
@@ -160,41 +153,11 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
]
);
// format share used token
const formatTokens = (tokens: number) => {
if (tokens < 10000) return tokens;
return `${(tokens / 10000).toFixed(2)}`;
};
// init kb select list
const { data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
const RenderSelectedKbList = useCallback(() => {
const kbs = getValues('chat.relatedKbs').map((id) => kbList.find((kb) => kb._id === id));
return (
<>
{kbs.map((item) =>
item ? (
<Card key={item._id} p={3} mt={3}>
<Flex alignItems={'center'}>
<Image
src={item.avatar}
fallbackSrc="/icon/logo.png"
w={'20px'}
h={'20px'}
alt=""
></Image>
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
</Card>
) : null
)}
</>
);
}, [getValues, kbList]);
return (
<>
{/* basic info */}
@@ -329,7 +292,18 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
</Slider>
</Flex>
</FormControl>
{getValues('chat.relatedKbs').length > 0 && (
<Flex mt={4} alignItems={'center'}>
<Box mr={4}></Box>
<Switch
isDisabled={!isOwner}
isChecked={getValues('chat.useKb')}
onChange={() => {
setValue('chat.useKb', !getValues('chat.useKb'));
setRefresh(!refresh);
}}
/>
</Flex>
{getValues('chat.useKb') && (
<Flex mt={4} alignItems={'center'}>
<Box mr={4} whiteSpace={'nowrap'}>
&emsp;
@@ -365,9 +339,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
<Box fontWeight={'bold'}></Box>
<Box>
<Flex mt={5} alignItems={'center'}>
<Box mr={1} fontSize={['sm', 'md']}>
:
</Box>
<Box mr={1}>:</Box>
<Tooltip label="开启模型分享后,你的模型将会出现在共享市场,可供 FastGpt 所有用户使用。用户使用时不会消耗你的 tokens而是消耗使用者的 tokens。">
<QuestionOutlineIcon mr={3} />
</Tooltip>
@@ -378,8 +350,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
setRefresh(!refresh);
}}
/>
<Box ml={12} mr={1} fontSize={['sm', 'md']}>
<Box ml={12} mr={1}>
:
</Box>
<Tooltip label="开启分享详情后,其他用户可以查看该模型的特有数据:温度、提示词和数据集。">
@@ -405,22 +376,8 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
</Box>
</Box>
</Card>
<Card p={4}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}></Box>
<Button
size={'sm'}
variant={'outline'}
colorScheme={'myBlue'}
onClick={onOpenKbSelect}
>
</Button>
</Flex>
<RenderSelectedKbList />
</Card>
{/* shareChat */}
<Card p={4} gridColumnStart={1} gridColumnEnd={[2, 3]}>
<Card p={4}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'}>
@@ -453,7 +410,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
<Th></Th>
<Th>tokens消耗</Th>
<Th>使</Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
@@ -582,52 +539,6 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
</ModalFooter>
</ModalContent>
</Modal>
{/* select kb modal */}
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
{kbList.map((item) => (
<Card key={item._id} p={3} mb={3}>
<Checkbox
isChecked={getValues('chat.relatedKbs').includes(item._id)}
onChange={(e) => {
const ids = getValues('chat.relatedKbs');
// toggle to true
if (e.target.checked) {
setValue('chat.relatedKbs', ids.concat(item._id));
} else {
const i = ids.findIndex((id) => id === item._id);
ids.splice(i, 1);
setValue('chat.relatedKbs', ids);
}
setRefresh(!refresh);
}}
>
<Flex alignItems={'center'}>
<Image
src={item.avatar}
fallbackSrc="/icon/logo.png"
w={'20px'}
h={'20px'}
alt=""
></Image>
<Box ml={3} fontWeight={'bold'}>
{item.name}
</Box>
</Flex>
</Checkbox>
</Card>
))}
</ModalBody>
<ModalFooter>
<Button onClick={onCloseKbSelect}>,</Button>
</ModalFooter>
</ModalContent>
</Modal>
<File onSelect={onSelectFile} />
<ConfirmChild />
</>

View File

@@ -15,7 +15,7 @@ import { useSelectFile } from '@/hooks/useSelectFile';
import { useConfirm } from '@/hooks/useConfirm';
import { readCsvContent } from '@/utils/file';
import { useMutation } from '@tanstack/react-query';
import { postKbDataFromList } from '@/api/plugins/kb';
import { postModelDataCsvData } from '@/api/model';
import Markdown from '@/components/Markdown';
import { useMarkdown } from '@/hooks/useMarkdown';
import { fileDownload } from '@/utils/file';
@@ -25,16 +25,16 @@ const csvTemplate = `question,answer\n"什么是 laf","laf 是一个云函数开
const SelectJsonModal = ({
onClose,
onSuccess,
kbId
modelId
}: {
onClose: () => void;
onSuccess: () => void;
kbId: string;
modelId: string;
}) => {
const [selecting, setSelecting] = useState(false);
const { toast } = useToast();
const { File, onOpen } = useSelectFile({ fileType: '.csv', multiple: false });
const [fileData, setFileData] = useState<{ q: string; a: string }[]>([]);
const [fileData, setFileData] = useState<string[][]>([]);
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认导入该数据集?'
});
@@ -48,12 +48,7 @@ const SelectJsonModal = ({
if (header[0] !== 'question' || header[1] !== 'answer') {
throw new Error('csv 文件格式有误');
}
setFileData(
data.map((item) => ({
q: item[0] || '',
a: item[1] || ''
}))
);
setFileData(data);
} catch (error: any) {
console.log(error);
toast({
@@ -68,13 +63,8 @@ const SelectJsonModal = ({
const { mutate, isLoading } = useMutation({
mutationFn: async () => {
if (!fileData || fileData.length === 0) return;
const res = await postKbDataFromList({
kbId,
data: fileData
});
if (!fileData) return;
const res = await postModelDataCsvData(modelId, fileData);
toast({
title: `导入数据成功,最终导入: ${res || 0} 条数据。需要一段时间训练`,
status: 'success',
@@ -130,10 +120,10 @@ const SelectJsonModal = ({
{fileData.map((item, index) => (
<Box key={index}>
<Box>
Q{index + 1}. {item.q}
Q{index + 1}. {item[0]}
</Box>
<Box>
A{index + 1}. {item.a}
A{index + 1}. {item[1]}
</Box>
</Box>
))}

View File

@@ -17,10 +17,10 @@ import { useSelectFile } from '@/hooks/useSelectFile';
import { useConfirm } from '@/hooks/useConfirm';
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/file';
import { useMutation } from '@tanstack/react-query';
import { postSplitData } from '@/api/plugins/kb';
import { postModelDataSplitData } from '@/api/model';
import { formatPrice } from '@/utils/user';
import Radio from '@/components/Radio';
import { splitText_token } from '@/utils/file';
import { SplitTextTypEnum } from '@/constants/plugin';
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
@@ -42,17 +42,17 @@ const modeMap = {
const SelectFileModal = ({
onClose,
onSuccess,
kbId
modelId
}: {
onClose: () => void;
onSuccess: () => void;
kbId: string;
modelId: string;
}) => {
const [btnLoading, setBtnLoading] = useState(false);
const { toast } = useToast();
const [prompt, setPrompt] = useState('');
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
const [mode, setMode] = useState<`${SplitTextTypEnum}`>(SplitTextTypEnum.subsection);
const [mode, setMode] = useState<'qa' | 'subsection'>('qa');
const [fileTextArr, setFileTextArr] = useState<string[]>(['']);
const [splitRes, setSplitRes] = useState<{ tokens: number; chunks: string[] }>({
tokens: 0,
@@ -107,8 +107,8 @@ const SelectFileModal = ({
mutationFn: async () => {
if (splitRes.chunks.length === 0) return;
await postSplitData({
kbId,
await postModelDataSplitData({
modelId,
chunks: splitRes.chunks,
prompt: `下面是"${prompt || '一段长文本'}"`,
mode

View File

@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import {
Box,
Flex,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
Input,
Textarea
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useConfirm } from '@/hooks/useConfirm';
import { useMutation } from '@tanstack/react-query';
import { postModelDataSplitData, getWebContent } from '@/api/model';
import { formatPrice } from '@/utils/user';
const SelectUrlModal = ({
onClose,
onSuccess,
modelId
}: {
onClose: () => void;
onSuccess: () => void;
modelId: string;
}) => {
const { toast } = useToast();
const [webUrl, setWebUrl] = useState('');
const [webText, setWebText] = useState('');
const [prompt, setPrompt] = useState(''); // 提示词
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,任务讲被终止。'
});
const { mutate: onclickImport, isLoading: isImporting } = useMutation({
mutationFn: async () => {
if (!webText) return;
await postModelDataSplitData({
modelId,
chunks: [],
prompt: `下面是"${prompt || '一段长文本'}"`,
mode: 'qa'
});
toast({
title: '导入数据成功,需要一段拆解和训练',
status: 'success'
});
onClose();
onSuccess();
},
onError(error) {
console.log(error);
toast({
title: '导入数据失败',
status: 'error'
});
}
});
const { mutate: onclickFetchingUrl, isLoading: isFetching } = useMutation({
mutationFn: async () => {
if (!webUrl) return;
const res = await getWebContent(webUrl);
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(res, 'text/html');
const data = htmlDoc?.body?.innerText || '';
if (!data) {
throw new Error('获取不到数据');
}
setWebText(data.replace(/\s+/g, ' '));
},
onError(error) {
console.log(error);
toast({
status: 'error',
title: '获取网站内容失败'
});
}
});
return (
<Modal isOpen={true} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent maxW={'min(900px, 90vw)'} m={0} position={'relative'} h={'90vh'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody
display={'flex'}
flexDirection={'column'}
p={4}
h={'100%'}
alignItems={'center'}
justifyContent={'center'}
fontSize={'sm'}
>
<Box mt={2} maxW={['100%', '70%']}>
Gpt会对文本进行
QA tokens
</Box>
<Flex w={'100%'} alignItems={'center'} my={4}>
<Box flex={'0 0 70px'}></Box>
<Input
mx={2}
placeholder="需要获取内容的地址。例如https://fastgpt.ahapocket.cn"
value={webUrl}
onChange={(e) => setWebUrl(e.target.value)}
size={'sm'}
/>
<Button isLoading={isFetching} onClick={() => onclickFetchingUrl()}>
</Button>
</Flex>
<Flex w={'100%'} alignItems={'center'} my={4}>
<Box flex={'0 0 70px'} mr={2}>
</Box>
<Input
placeholder="内容提示词。例如: Laf的介绍/关于gpt4的论文/一段长文本"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
size={'sm'}
/>
</Flex>
<Textarea
flex={'1 0 0'}
h={0}
w={'100%'}
placeholder="网站的内容"
maxLength={-1}
resize={'none'}
fontSize={'xs'}
whiteSpace={'pre-wrap'}
value={webText}
onChange={(e) => setWebText(e.target.value)}
/>
</ModalBody>
<Flex px={6} pt={2} pb={4}>
<Box flex={1}></Box>
<Button variant={'outline'} mr={3} onClick={onClose}>
</Button>
<Button
isLoading={isImporting}
isDisabled={webText === ''}
onClick={openConfirm(onclickImport)}
>
</Button>
</Flex>
</ModalContent>
<ConfirmChild />
</Modal>
);
};
export default SelectUrlModal;

View File

@@ -2,13 +2,15 @@ import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import { delModelById, putModelById } from '@/api/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Grid } from '@chakra-ui/react';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { formatModelStatus } from '@/constants/model';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user';
import { useLoading } from '@/hooks/useLoading';
import ModelEditForm from './components/ModelEditForm';
import ModelDataCard from './components/ModelDataCard';
const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
const { toast } = useToast();
@@ -17,7 +19,7 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
const { Loading, setIsLoading } = useLoading();
const [btnLoading, setBtnLoading] = useState(false);
const formHooks = useForm({
const formHooks = useForm<ModelSchema>({
defaultValues: modelDetail
});
@@ -43,11 +45,6 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
[modelDetail.userId, userInfo?._id]
);
const canRead = useMemo(
() => isOwner || isLoading || modelDetail.share.isShareDetail,
[isLoading, isOwner, modelDetail.share.isShareDetail]
);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!modelDetail) return;
@@ -83,9 +80,13 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
name: data.name,
avatar: data.avatar || '/icon/logo.png',
chat: data.chat,
share: data.share
share: data.share,
security: data.security
});
toast({
title: '更新成功',
status: 'success'
});
refreshModel.updateModelDetail(data);
} catch (err: any) {
toast({
@@ -115,18 +116,20 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
});
}, [formHooks.formState.errors, toast]);
const saveUpdateModel = useCallback(
() => formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)(),
[formHooks, saveSubmitError, saveSubmitSuccess]
);
useEffect(() => {
return () => {
saveUpdateModel();
};
}, []);
router.prefetch('/chat');
return canRead ? (
window.onbeforeunload = (e) => {
e.preventDefault();
e.returnValue = '内容已修改,确认离开页面吗?';
};
return () => {
window.onbeforeunload = null;
};
}, [router]);
return (
<Box h={'100%'} p={5} overflow={'overlay'} position={'relative'}>
{/* 头部 */}
<Card px={6} py={3}>
@@ -135,6 +138,13 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
<Box fontSize={'xl'} fontWeight={'bold'}>
{modelDetail.name}
</Box>
<Tag
ml={2}
variant="solid"
colorScheme={formatModelStatus[modelDetail.status].colorTheme}
>
{formatModelStatus[modelDetail.status].text}
</Tag>
<Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}>
@@ -143,18 +153,7 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
<Button
isLoading={btnLoading}
ml={4}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
@@ -166,6 +165,9 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{modelDetail.name}
</Box>
<Tag ml={2} colorScheme={formatModelStatus[modelDetail.status].colorTheme}>
{formatModelStatus[modelDetail.status].text}
</Tag>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'outline'} size={'sm'} onClick={handlePreviewChat}>
@@ -176,18 +178,7 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
ml={4}
size={'sm'}
isLoading={btnLoading}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
@@ -198,13 +189,13 @@ const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
</Card>
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} isOwner={isOwner} />
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<ModelDataCard modelId={modelId} isOwner={isOwner} />
</Card>
</Grid>
<Loading loading={isLoading} fixed={false} />
</Box>
) : (
<Box h={'100%'} p={5}>
</Box>
);
};

View File

@@ -1,21 +1,22 @@
import React, { useEffect } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useScreen } from '@/hooks/useScreen';
import { useRouter } from 'next/router';
import ModelList from './components/ModelList';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
import { useGlobalStore } from '@/store/global';
import Loading from '@/components/Loading';
import SideBar from '@/components/SideBar';
const ModelDetail = dynamic(() => import('./components/detail/index'), {
loading: () => <Loading fixed={false} />,
ssr: false
});
const Model = ({ modelId }: { modelId: string }) => {
const Model = ({ modelId, isPcDevice }: { modelId: string; isPcDevice: boolean }) => {
const router = useRouter();
const { isPc } = useGlobalStore();
const { isPc } = useScreen({
defaultIsPc: isPcDevice
});
const { lastModelId } = useUserStore();
// redirect modelId
@@ -26,12 +27,12 @@ const Model = ({ modelId }: { modelId: string }) => {
}, [isPc, lastModelId, modelId, router]);
return (
<Flex h={'100%'} position={'relative'} overflow={'hidden'}>
<Flex h={'100%'} position={'relative'}>
{/* 模型列表 */}
{(isPc || !modelId) && (
<SideBar w={['100%', '0 0 250px', '0 0 270px', '0 0 290px']}>
<Box w={['100%', '250px']}>
<ModelList modelId={modelId} />
</SideBar>
</Box>
)}
<Box flex={1} h={'100%'} position={'relative'}>
{modelId && <ModelDetail modelId={modelId} isPc={isPc} />}
@@ -44,6 +45,7 @@ export default Model;
Model.getInitialProps = ({ query, req }: any) => {
return {
modelId: query?.modelId || ''
modelId: query?.modelId || '',
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};

View File

@@ -18,7 +18,6 @@ const ShareModelList = ({
<>
{models.map((model) => (
<Flex
w={'100%'}
flexDirection={'column'}
key={model._id}
p={4}

View File

@@ -5,11 +5,6 @@ import MyIcon from '@/components/Icon';
import { useRouter } from 'next/router';
const list = [
{
icon: 'kb',
label: '我的知识库',
link: '/kb'
},
{
icon: 'shareMarket',
label: 'AI助手市场',
@@ -24,11 +19,6 @@ const list = [
icon: 'develop',
label: '开发',
link: '/openapi'
},
{
icon: 'git',
label: 'Git项目地址',
link: 'https://github.com/c121914yu/FastGPT'
}
];

View File

@@ -37,8 +37,7 @@ export const proxyError: Record<string, boolean> = {
export enum ERROR_ENUM {
unAuthorization = 'unAuthorization',
insufficientQuota = 'insufficientQuota',
unAuthModel = 'unAuthModel',
unAuthKb = 'unAuthKb'
unAuthModel = 'unAuthModel'
}
export const ERROR_RESPONSE: Record<
any,
@@ -66,11 +65,5 @@ export const ERROR_RESPONSE: Record<
statusText: ERROR_ENUM.unAuthModel,
message: '无权使用该模型',
data: null
},
[ERROR_ENUM.unAuthKb]: {
code: 512,
statusText: ERROR_ENUM.unAuthKb,
message: '无权使用该知识库',
data: null
}
};

View File

@@ -5,7 +5,7 @@ import { pushSplitDataBill } from '@/service/events/pushBill';
import { generateVector } from './generateVector';
import { openaiError2 } from '../errorCode';
import { PgClient } from '@/service/pg';
import { SplitDataSchema } from '@/types/mongoSchema';
import { ModelSplitDataSchema } from '@/types/mongoSchema';
import { modelServiceToolMap } from '../utils/chat';
import { ChatRoleEnum } from '@/constants/chat';
import { getErrMessage } from '../utils/tools';
@@ -32,7 +32,7 @@ export async function generateQA(next = false): Promise<any> {
{ $sample: { size: 1 } }
]);
const dataItem: SplitDataSchema = data[0];
const dataItem: ModelSplitDataSchema = data[0];
if (!dataItem) {
console.log('没有需要生成 QA 的数据');
@@ -127,15 +127,14 @@ A2:
const resultList = successResponse.map((item) => item.result).flat();
await Promise.allSettled([
// 删掉后5个数据
SplitData.findByIdAndUpdate(dataItem._id, {
textList: dataItem.textList.slice(0, -5)
}),
}), // 删掉后5个数据
// 生成的内容插入 pg
PgClient.insert('modelData', {
values: resultList.map((item) => [
{ key: 'user_id', value: dataItem.userId },
{ key: 'kb_id', value: dataItem.kbId },
{ key: 'model_id', value: dataItem.modelId },
{ key: 'q', value: item.q },
{ key: 'a', value: item.a },
{ key: 'status', value: 'waiting' }

View File

@@ -1,28 +0,0 @@
import { Schema, model, models, Model } from 'mongoose';
import { kbSchema as SchemaType } from '@/types/mongoSchema';
const kbSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
updateTime: {
type: Date,
default: () => new Date()
},
avatar: {
type: String,
default: '/icon/logo.png'
},
name: {
type: String,
required: true
},
tags: {
type: [String],
default: []
}
});
export const KB: Model<SchemaType> = models['kb'] || model('kb', kbSchema);

View File

@@ -31,10 +31,10 @@ const ModelSchema = new Schema({
default: () => new Date()
},
chat: {
relatedKbs: {
type: [Schema.Types.ObjectId],
ref: 'kb',
default: []
useKb: {
// use knowledge base to search
type: Boolean,
default: false
},
searchMode: {
// knowledge base search mode
@@ -79,6 +79,33 @@ const ModelSchema = new Schema({
type: Number,
default: 0
}
},
security: {
type: {
domain: {
type: [String],
default: ['*']
},
contextMaxLen: {
type: Number,
default: 20
},
contentMaxLen: {
type: Number,
default: 4000
},
expiredTime: {
type: Number,
default: 1,
set: (val: number) => val * (60 * 60 * 1000)
},
maxLoadAmount: {
// 负数代表不限制
type: Number,
default: -1
}
},
default: {}
}
});

View File

@@ -1,6 +1,6 @@
/* 模型的知识库 */
import { Schema, model, models, Model as MongoModel } from 'mongoose';
import { SplitDataSchema as SplitDataType } from '@/types/mongoSchema';
import { ModelSplitDataSchema as SplitDataType } from '@/types/mongoSchema';
const SplitDataSchema = new Schema({
userId: {
@@ -13,9 +13,9 @@ const SplitDataSchema = new Schema({
type: String,
required: true
},
kbId: {
modelId: {
type: Schema.Types.ObjectId,
ref: 'kb',
ref: 'model',
required: true
},
textList: {

View File

@@ -52,4 +52,3 @@ export * from './models/openapi';
export * from './models/promotionRecord';
export * from './models/collection';
export * from './models/shareChat';
export * from './models/kb';

View File

@@ -48,7 +48,7 @@ export const searchKb = async ({
where: [
['status', ModelDataStatusEnum.ready],
'AND',
`kb_id IN (${model.chat.relatedKbs.map((item) => `'${item}'`).join(',')})`,
['model_id', model._id],
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest } from 'next';
import jwt from 'jsonwebtoken';
import cookie from 'cookie';
import { Chat, Model, OpenApi, User, ShareChat, KB } from '../mongo';
import { Chat, Model, OpenApi, User, ShareChat } from '../mongo';
import type { ModelSchema } from '@/types/mongoSchema';
import type { ChatItemSimpleType } from '@/types/chat';
import mongoose from 'mongoose';
@@ -34,13 +34,6 @@ export const authToken = (req: NextApiRequest): Promise<string> => {
});
};
export const getOpenAiKey = () => {
// 纯字符串类型
const keys = process.env.OPENAIKEY?.split(',') || [];
const i = Math.floor(Math.random() * keys.length);
return keys[i] || (process.env.OPENAIKEY as string);
};
/* 获取 api 请求的 key */
export const getApiKey = async ({
model,
@@ -59,7 +52,7 @@ export const getApiKey = async ({
const keyMap = {
[OpenAiChatEnum.GPT35]: {
userOpenAiKey: user.openaiKey || '',
systemAuthKey: getOpenAiKey() as string
systemAuthKey: process.env.OPENAIKEY as string
},
[OpenAiChatEnum.GPT4]: {
userOpenAiKey: user.openaiKey || '',
@@ -136,18 +129,6 @@ export const authModel = async ({
return { model, showModelDetail: model.share.isShareDetail || userId === String(model.userId) };
};
// 知识库操作权限
export const authKb = async ({ kbId, userId }: { kbId: string; userId: string }) => {
const kb = await KB.findOne({
_id: kbId,
userId
});
if (kb) {
return kb;
}
return Promise.reject(ERROR_ENUM.unAuthKb);
};
// 获取对话校验
export const authChat = async ({
modelId,

View File

@@ -25,9 +25,9 @@ export const lafClaudChat = async ({
.filter((item) => item.obj === 'System')
.map((item) => item.value)
.join('\n');
const systemPromptText = systemPrompt ? `你本次知识:${systemPrompt}\n下面是我的问题:` : '';
const systemPromptText = systemPrompt ? `你本次知识:${systemPrompt}\n` : '';
const prompt = `${systemPromptText}'${messages[messages.length - 1].value}'`;
const prompt = `${systemPromptText}我的问题是:'${messages[messages.length - 1].value}'`;
const lafResponse = await axios.post(
process.env.CLAUDE_BASE_URL || '',

View File

@@ -7,7 +7,6 @@ import { adaptChatItem_openAI } from '@/utils/chat/openai';
import { modelToolMap } from '@/utils/chat';
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
import { ChatRoleEnum } from '@/constants/chat';
import { getOpenAiKey } from '../auth';
export const getOpenAIApi = (apiKey: string) => {
const configuration = new Configuration({
@@ -28,7 +27,7 @@ export const openaiCreateEmbedding = async ({
userId: string;
textArr: string[];
}) => {
const systemAuthKey = getOpenAiKey();
const systemAuthKey = process.env.OPENAIKEY as string;
// 获取 chatAPI
const chatAPI = getOpenAIApi(userOpenAiKey || systemAuthKey);

View File

@@ -5,9 +5,6 @@ import { immer } from 'zustand/middleware/immer';
type State = {
loading: boolean;
setLoading: (val: boolean) => null;
screenWidth: number;
setScreenWidth: (val: number) => void;
isPc: boolean;
};
export const useGlobalStore = create<State>()(
@@ -19,15 +16,7 @@ export const useGlobalStore = create<State>()(
state.loading = val;
});
return null;
},
screenWidth: 600,
setScreenWidth(val: number) {
set((state) => {
state.screenWidth = val;
state.isPc = val < 900 ? false : true;
});
},
isPc: false
}
}))
)
);

View File

@@ -2,22 +2,18 @@ import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { UserType, UserUpdateParams } from '@/types/user';
import type { ModelSchema } from '@/types/mongoSchema';
import { getMyModels, getModelById } from '@/api/model';
import { formatPrice } from '@/utils/user';
import { getTokenLogin } from '@/api/user';
import { defaultModel } from '@/constants/model';
import { ModelListItemType } from '@/types/model';
import { KbItemType } from '@/types/plugin';
import { getKbList } from '@/api/plugins/kb';
import { defaultKbDetail } from '@/constants/kb';
import type { ModelSchema } from '@/types/mongoSchema';
type State = {
userInfo: UserType | null;
initUserInfo: () => Promise<null>;
setUserInfo: (user: UserType | null) => void;
updateUserInfo: (user: UserUpdateParams) => void;
// model
lastModelId: string;
setLastModelId: (id: string) => void;
myModels: ModelListItemType[];
@@ -30,13 +26,6 @@ type State = {
updateModelDetail(model: ModelSchema): void;
removeModelDetail(modelId: string): void;
};
// kb
lastKbId: string;
setLastKbId: (id: string) => void;
myKbList: KbItemType[];
loadKbList: (init?: boolean) => Promise<KbItemType[]>;
KbDetail: KbItemType;
getKbDetail: (id: string) => KbItemType;
};
export const useUserStore = create<State>()(
@@ -114,38 +103,12 @@ export const useUserStore = create<State>()(
}
get().loadMyModels(true);
}
},
lastKbId: '',
setLastKbId(id: string) {
set((state) => {
state.lastKbId = id;
});
},
myKbList: [],
async loadKbList(init = false) {
if (get().myKbList.length > 0 && !init) return get().myKbList;
const res = await getKbList();
set((state) => {
state.myKbList = res;
});
return res;
},
KbDetail: defaultKbDetail,
getKbDetail(id: string) {
const data = get().myKbList.find((item) => item._id === id) || defaultKbDetail;
set((state) => {
state.KbDetail = data;
});
return data;
}
})),
{
name: 'userStore',
partialize: (state) => ({
lastModelId: state.lastModelId,
lastKbId: state.lastKbId
lastModelId: state.lastModelId
})
}
)

13
src/types/model.d.ts vendored
View File

@@ -1,6 +1,5 @@
import { ModelStatusEnum } from '@/constants/model';
import type { ModelSchema, kbSchema } from './mongoSchema';
import { ChatModelType, ModelVectorSearchModeEnum } from '@/constants/model';
import type { ModelSchema } from './mongoSchema';
export type ModelListItemType = {
_id: string;
@@ -14,6 +13,16 @@ export interface ModelUpdateParams {
avatar: string;
chat: ModelSchema['chat'];
share: ModelSchema['share'];
security: ModelSchema['security'];
}
export interface ModelDataItemType {
id: string;
status: 'waiting' | 'ready';
q: string; // 提问词
a: string; // 原文
modelId: string;
userId: string;
}
export interface ShareModelItem {

View File

@@ -38,7 +38,7 @@ export interface ModelSchema {
status: `${ModelStatusEnum}`;
updateTime: number;
chat: {
relatedKbs: string[];
useKb: boolean;
searchMode: `${ModelVectorSearchModeEnum}`;
systemPrompt: string;
temperature: number;
@@ -50,6 +50,13 @@ export interface ModelSchema {
intro: string;
collection: number;
};
security: {
domain: string[];
contextMaxLen: number;
contentMaxLen: number;
expiredTime: number;
maxLoadAmount: number;
};
}
export interface ModelPopulate extends ModelSchema {
@@ -62,11 +69,19 @@ export interface CollectionSchema {
}
export type ModelDataType = 0 | 1;
export interface ModelDataSchema {
_id: string;
modelId: string;
userId: string;
a: string;
q: string;
status: ModelDataType;
}
export interface SplitDataSchema {
export interface ModelSplitDataSchema {
_id: string;
userId: string;
kbId: string;
modelId: string;
prompt: string;
errorText: string;
textList: string[];
@@ -133,12 +148,3 @@ export interface ShareChatSchema {
maxContext: number;
lastTime: Date;
}
export interface kbSchema {
_id: string;
userId: string;
updateTime: Date;
avatar: string;
name: string;
tags: string[];
}

3
src/types/pg.d.ts vendored
View File

@@ -1,11 +1,10 @@
import { ModelDataStatusEnum } from '@/constants/model';
export interface PgKBDataItemType {
export interface PgModelDataItemType {
id: string;
q: string;
a: string;
status: `${ModelDataStatusEnum}`;
model_id: string;
user_id: string;
kb_id: string;
}

15
src/types/plugin.d.ts vendored
View File

@@ -1,15 +0,0 @@
import type { kbSchema } from './mongoSchema';
/* kb type */
export interface KbItemType extends kbSchema {
totalData: number;
tags: string;
}
export interface KbDataItemType {
id: string;
status: 'waiting' | 'ready';
q: string; // 提问词
a: string; // 原文
kbId: string;
userId: string;
}

View File

@@ -89,7 +89,6 @@ export const formatTimeToChatTime = (time: Date) => {
return target.format('YYYY/M/D');
};
export const hasVoiceApi = typeof window !== 'undefined' && 'speechSynthesis' in window;
/**
* voice broadcast
*/
@@ -114,9 +113,3 @@ export const voiceBroadcast = ({ text }: { text: string }) => {
cancel: () => window.speechSynthesis?.cancel()
};
};
export const formatLinkTextToHtml = (text: string) => {
const httpReg =
/(http|https|ftp):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?/gi;
return text.replace(httpReg, '<a href="$&" target="_blank">$&</a>');
};