Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7f0db0811 | ||
|
|
c16d59671f |
@@ -29,8 +29,8 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
|
||||
|
||||
## 🏘️ 交流群
|
||||
|
||||
添加 wx 进入:
|
||||

|
||||
wx: fastgpt123
|
||||

|
||||
|
||||
## 👀 其他
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
BIN
docs/imgs/wx300.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -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
215
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
@@ -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 为内核的云操作系统发行版,可以…… |
|
||||
|
||||
@@ -29,6 +29,6 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
|
||||
如果群满了,可加个小助手,定时拉
|
||||
wx 号: fastgpt123
|
||||
|
||||
| 交流群 | 小助手 |
|
||||
| ------------------------------------------------- | ---------------------------------------------- |
|
||||
|  |  |
|
||||
| 交流群 | 小助手 |
|
||||
| ----------------------- | -------------------- |
|
||||
|  |  |
|
||||
|
||||
BIN
public/imgs/wx300.jpg
Normal file
BIN
public/imgs/wx300.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -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}`);
|
||||
|
||||
/* 共享市场 */
|
||||
/**
|
||||
* 获取共享市场模型
|
||||
|
||||
@@ -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);
|
||||
@@ -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 |
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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=""
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum SplitTextTypEnum {
|
||||
'qa' = 'qa',
|
||||
'subsection' = 'subsection'
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,7 +41,6 @@ export const usePagination = <T = any,>({
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
34
src/pages/api/model/data/fetchingUrlData.ts
Normal file
34
src/pages/api/model/data/fetchingUrlData.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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' }],
|
||||
@@ -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) {
|
||||
@@ -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) {
|
||||
54
src/pages/api/model/data/pushModelDataInput.ts
Normal file
54
src/pages/api/model/data/pushModelDataInput.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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: [
|
||||
@@ -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' }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 || ''
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -126,7 +126,7 @@ const ModelList = ({ modelId }: { modelId: string }) => {
|
||||
{...(modelId === item._id
|
||||
? {
|
||||
backgroundColor: '#eff0f1',
|
||||
borderLeftColor: 'myBlue.600 !important'
|
||||
borderLeftColor: 'myBlue.600'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
|
||||
@@ -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
|
||||
310
src/pages/model/components/detail/components/ModelDataCard.tsx
Normal file
310
src/pages/model/components/detail/components/ModelDataCard.tsx
Normal 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;
|
||||
@@ -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'}>
|
||||
搜索模式 
|
||||
@@ -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 />
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
@@ -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
|
||||
162
src/pages/model/components/detail/components/SelectUrlModal.tsx
Normal file
162
src/pages/model/components/detail/components/SelectUrlModal.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ const ShareModelList = ({
|
||||
<>
|
||||
{models.map((model) => (
|
||||
<Flex
|
||||
w={'100%'}
|
||||
flexDirection={'column'}
|
||||
key={model._id}
|
||||
p={4}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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);
|
||||
@@ -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: {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -52,4 +52,3 @@ export * from './models/openapi';
|
||||
export * from './models/promotionRecord';
|
||||
export * from './models/collection';
|
||||
export * from './models/shareChat';
|
||||
export * from './models/kb';
|
||||
|
||||
@@ -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}`
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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
13
src/types/model.d.ts
vendored
@@ -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 {
|
||||
|
||||
30
src/types/mongoSchema.d.ts
vendored
30
src/types/mongoSchema.d.ts
vendored
@@ -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
3
src/types/pg.d.ts
vendored
@@ -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
15
src/types/plugin.d.ts
vendored
@@ -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;
|
||||
}
|
||||
@@ -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\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/gi;
|
||||
return text.replace(httpReg, '<a href="$&" target="_blank">$&</a>');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user