Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be69cfb966 | ||
|
|
6244f6c1fb | ||
|
|
e12f97a73b | ||
|
|
7f96c4ff9b | ||
|
|
138b607ac7 | ||
|
|
b204c55bd1 | ||
|
|
17cbfa05d3 | ||
|
|
a7b9940d7a | ||
|
|
fd8135f50c | ||
|
|
9f96593136 | ||
|
|
ed9e72ec9a | ||
|
|
7fb76cde0b | ||
|
|
38d49ea05f | ||
|
|
7db87c2d09 | ||
|
|
453f3be8ce | ||
|
|
f023f63103 | ||
|
|
65da4653bc | ||
|
|
26888e855b | ||
|
|
e132c622a6 | ||
|
|
17364e9da3 | ||
|
|
e5fe670a6e | ||
|
|
7807b26707 | ||
|
|
16775430ea | ||
|
|
dd5217d8a5 | ||
|
|
9f8d696bbe | ||
|
|
bf81d23de4 | ||
|
|
52a752dab5 | ||
|
|
78903baefa | ||
|
|
45ad3ba22a | ||
|
|
c03a7db633 | ||
|
|
2cc32d1806 |
@@ -1,6 +1,6 @@
|
|||||||
AXIOS_PROXY_HOST=127.0.0.1
|
AXIOS_PROXY_HOST=127.0.0.1
|
||||||
AXIOS_PROXY_PORT=33210
|
AXIOS_PROXY_PORT=33210
|
||||||
MONGODB_UR=
|
MONGODB_URI=
|
||||||
MY_MAIL=
|
MY_MAIL=
|
||||||
MAILE_CODE=
|
MAILE_CODE=
|
||||||
TOKEN_KEY=
|
TOKEN_KEY=
|
||||||
4
.gitignore
vendored
@@ -34,6 +34,6 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
public/trainData/
|
/public/trainData/
|
||||||
.vscode/
|
/.vscode/
|
||||||
platform.json
|
platform.json
|
||||||
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.formatOnType": true,
|
|
||||||
"editor.formatOnSave": true ,
|
|
||||||
"prettier.tabWidth": 2
|
|
||||||
}
|
|
||||||
@@ -58,7 +58,7 @@ ENV PORT 3000
|
|||||||
ENV MAX_USER ''
|
ENV MAX_USER ''
|
||||||
ENV AXIOS_PROXY_HOST ''
|
ENV AXIOS_PROXY_HOST ''
|
||||||
ENV AXIOS_PROXY_PORT ''
|
ENV AXIOS_PROXY_PORT ''
|
||||||
ENV MONGODB_UR ''
|
ENV MONGODB_URI ''
|
||||||
ENV MY_MAIL ''
|
ENV MY_MAIL ''
|
||||||
ENV MAILE_CODE ''
|
ENV MAILE_CODE ''
|
||||||
ENV TOKEN_KEY ''
|
ENV TOKEN_KEY ''
|
||||||
|
|||||||
6
Makefile
@@ -1,4 +1,4 @@
|
|||||||
SERVICE_NAME=doc-gpt
|
SERVICE_NAME=fast-gpt
|
||||||
# Image URL to use all building/pushing image targets
|
# Image URL to use all building/pushing image targets
|
||||||
IMG ?= $(SERVICE_NAME):latest
|
IMG ?= $(SERVICE_NAME):latest
|
||||||
|
|
||||||
@@ -34,13 +34,13 @@ run: ## Run a dev service from host.
|
|||||||
|
|
||||||
.PHONY: docker-build
|
.PHONY: docker-build
|
||||||
docker-build: ## Build docker image with the desktop-frontend.
|
docker-build: ## Build docker image with the desktop-frontend.
|
||||||
docker build -t c121914yu/doc-gpt:latest .
|
docker build -t c121914yu/fast-gpt:latest .
|
||||||
|
|
||||||
##@ Deployment
|
##@ Deployment
|
||||||
|
|
||||||
.PHONY: docker-run
|
.PHONY: docker-run
|
||||||
docker-run: ## Push docker image.
|
docker-run: ## Push docker image.
|
||||||
docker run -d -p 8008:3000 --name doc-gpt -v /web_project/yjl/doc-gpt/logs:/app/.next/logs c121914yu/doc-gpt:latest
|
docker run -d -p 8008:3000 --name fast-gpt -v /web_project/yjl/fast-gpt/logs:/app/.next/logs c121914yu/fast-gpt:latest
|
||||||
|
|
||||||
#TODO: add support of docker push
|
#TODO: add support of docker push
|
||||||
|
|
||||||
|
|||||||
128
README.md
@@ -1,4 +1,6 @@
|
|||||||
# Doc GPT
|
# Fast GPT
|
||||||
|
|
||||||
|
Fast GPT 允许你是用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
|
||||||
|
|
||||||
## 初始化
|
## 初始化
|
||||||
复制 .env.template 成 .env.local ,填写核心参数
|
复制 .env.template 成 .env.local ,填写核心参数
|
||||||
@@ -6,68 +8,96 @@
|
|||||||
```
|
```
|
||||||
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
|
||||||
AXIOS_PROXY_PORT=代理端口
|
AXIOS_PROXY_PORT=代理端口
|
||||||
MONGODB_UR=mongo数据库地址
|
MONGODB_URI=mongo数据库地址(例如:mongodb://username:password@ip:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false)
|
||||||
MY_MAIL=发送验证码邮箱
|
MY_MAIL=发送验证码邮箱
|
||||||
MAILE_CODE=邮箱秘钥
|
MAILE_CODE=邮箱秘钥(代理里设置的是QQ邮箱,不知道怎么找这个 code 的,可以百度搜"nodemailer发送邮件")
|
||||||
TOKEN_KEY=随便填一个,用于生成和校验token
|
TOKEN_KEY=随便填一个,用于生成和校验token
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 部署
|
|
||||||
```bash
|
|
||||||
# 本地 docker 打包
|
|
||||||
docker build -t imageName .
|
|
||||||
docker push imageName
|
|
||||||
|
|
||||||
# 服务器拉取部署
|
|
||||||
docker pull imageName
|
|
||||||
docker stop doc-gpt || true
|
|
||||||
docker rm doc-gpt || true
|
|
||||||
# 运行时才把参数写入
|
|
||||||
docker run -d --network=host --name doc-gpt -e AXIOS_PROXY_HOST= -e AXIOS_PROXY_PORT= -e MAILE_CODE= -e TOKEN_KEY= -e MONGODB_UR= imageName
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
# 介绍页
|
## 部署
|
||||||
|
|
||||||
## 欢迎使用 Doc GPT
|
### docker 模式
|
||||||
|
请准备好 docker, mongo,代理, 和nginx。 镜像走本机的代理,所以用 network=host,port 改成代理的端口,clash 一般都是 7890。
|
||||||
|
|
||||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
#### docker 打包
|
||||||
|
```bash
|
||||||
|
docker build -t imageName:tag .
|
||||||
|
docker push imageName:tag
|
||||||
|
```
|
||||||
|
|
||||||
1. 使用邮箱注册账号。
|
#### 服务器拉取镜像和运行
|
||||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
```bash
|
||||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
# 服务器拉取部署, imageName 替换成镜像名
|
||||||
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
|
docker pull imageName:tag
|
||||||
|
docker stop fast-gpt || true
|
||||||
|
docker rm fast-gpt || true
|
||||||
|
docker run -d --network=host --name fast-gpt \
|
||||||
|
-e AXIOS_PROXY_HOST=127.0.0.1 \
|
||||||
|
-e AXIOS_PROXY_PORT=7890 \
|
||||||
|
-e MY_MAIL=your email\
|
||||||
|
-e MAILE_CODE=your email code \
|
||||||
|
-e TOKEN_KEY=任意一个内容 \
|
||||||
|
-e MONGODB_URI="mongodb://user:password@127.0.0.0:27017/?authSource=admin&readPreference=primary&appname=MongoDB%20Compass&ssl=false" \
|
||||||
|
imageName:tag
|
||||||
|
```
|
||||||
|
|
||||||
### 模型配置
|
#### 软件教程:docker 安装
|
||||||
|
```bash
|
||||||
|
# 安装docker
|
||||||
|
curl -sSL https://get.daocloud.io/docker | sh
|
||||||
|
sudo systemctl start docker
|
||||||
|
```
|
||||||
|
|
||||||
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
|
#### 软件教程:mongo 安装
|
||||||
|
```bash
|
||||||
|
docker pull mongo:6.0.4
|
||||||
|
docker stop mongo
|
||||||
|
docker rm mongo
|
||||||
|
docker run -d --name mongo \
|
||||||
|
-e MONGO_INITDB_ROOT_USERNAME= \
|
||||||
|
-e MONGO_INITDB_ROOT_PASSWORD= \
|
||||||
|
-v /root/service/mongo:/data/db \
|
||||||
|
mongo:6.0.4
|
||||||
|
|
||||||
|
# 检查 mongo 运行情况, 有成功的 logs 代表访问成功
|
||||||
|
docker logs mongo
|
||||||
|
```
|
||||||
|
#### 软件教程: clash 代理
|
||||||
|
```bash
|
||||||
|
# 下载包
|
||||||
|
curl https://glados.rocks/tools/clash-linux.zip -o clash.zip
|
||||||
|
# 解压
|
||||||
|
unzip clash.zip
|
||||||
|
# 下载终端配置⽂件(改成自己配置文件路径)
|
||||||
|
curl https://update.glados-config.com/clash/98980/8f30944/70870/glados-terminal.yaml > config.yaml
|
||||||
|
# 赋予运行权限
|
||||||
|
chmod +x ./clash-linux-amd64-v1.10.0
|
||||||
|
# 记得配置端口变量:
|
||||||
|
export ALL_PROXY=socks5://127.0.0.1:7891
|
||||||
|
export http_proxy=http://127.0.0.1:7890
|
||||||
|
export https_proxy=http://127.0.0.1:7890
|
||||||
|
export HTTP_PROXY=http://127.0.0.1:7890
|
||||||
|
export HTTPS_PROXY=http://127.0.0.1:7890
|
||||||
|
|
||||||
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
|
# 运行脚本: 删除clash - 到 clash 目录 - 删除缓存 - 执行运行
|
||||||
|
# 会生成一个 nohup.out 文件,可以看到 clash 的 logs
|
||||||
|
OLD_PROCESS=$(pgrep clash)
|
||||||
|
if [ ! -z "$OLD_PROCESS" ]; then
|
||||||
|
echo "Killing old process: $OLD_PROCESS"
|
||||||
|
kill $OLD_PROCESS
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
cd **/clash
|
||||||
|
rm -f ./nohup.out || true
|
||||||
|
rm -f ./cache.db || true
|
||||||
|
nohup ./clash-linux-amd64-v1.10.0 -d ./ &
|
||||||
|
echo "Restart clash"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 软件教程:Nginx
|
||||||
3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
|
...没写,这个百度吧。
|
||||||
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
|
|
||||||
|
|
||||||
4. **过期时间**:生成对话框后,这个对话框多久过期。
|
|
||||||
|
|
||||||
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
|
|
||||||
|
|
||||||
### 对话框介绍
|
|
||||||
|
|
||||||
1. 每个对话框以 windowId 作为标识。
|
|
||||||
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
|
|
||||||
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
|
|
||||||
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如:
|
|
||||||
|
|
||||||
* 当前网页链接:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
|
|
||||||
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
|
||||||
|
|
||||||
### 其他问题
|
|
||||||
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
|
|
||||||

|
|
||||||
17
package.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "docgpt",
|
"name": "fastgpt",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -13,17 +13,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.17",
|
"@chakra-ui/icons": "^2.0.17",
|
||||||
"@chakra-ui/react": "^2.5.1",
|
"@chakra-ui/react": "^2.5.1",
|
||||||
"@chakra-ui/system": "^2.5.1",
|
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@next/font": "13.1.6",
|
"@next/font": "13.1.6",
|
||||||
"@reduxjs/toolkit": "^1.9.3",
|
|
||||||
"@tanstack/react-query": "^4.24.10",
|
"@tanstack/react-query": "^4.24.10",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
"axios": "^1.3.3",
|
"axios": "^1.3.3",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"eslint": "8.34.0",
|
"eventsource-parser": "^0.1.0",
|
||||||
"eslint-config-next": "13.1.6",
|
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
"framer-motion": "^9.0.6",
|
"framer-motion": "^9.0.6",
|
||||||
"hyperdown": "^2.4.29",
|
"hyperdown": "^2.4.29",
|
||||||
@@ -32,17 +30,19 @@
|
|||||||
"mongoose": "^6.10.0",
|
"mongoose": "^6.10.0",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.9.1",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
"openai": "^3.2.1",
|
"openai": "^3.2.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.43.1",
|
"react-hook-form": "^7.43.1",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"rehype-katex": "^6.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
"sass": "^1.58.3",
|
"sass": "^1.58.3",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"typescript": "4.9.5",
|
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"zustand": "^4.3.5"
|
"zustand": "^4.3.5"
|
||||||
},
|
},
|
||||||
@@ -56,9 +56,12 @@
|
|||||||
"@types/react-syntax-highlighter": "^15.5.6",
|
"@types/react-syntax-highlighter": "^15.5.6",
|
||||||
"@types/tunnel": "^0.0.3",
|
"@types/tunnel": "^0.0.3",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
|
"eslint": "8.34.0",
|
||||||
|
"eslint-config-next": "13.1.6",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.1.2",
|
"lint-staged": "^13.1.2",
|
||||||
"prettier": "^2.8.4"
|
"prettier": "^2.8.4",
|
||||||
|
"typescript": "4.9.5"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"./src/**/*.{ts,tsx,scss}": "npm run format"
|
"./src/**/*.{ts,tsx,scss}": "npm run format"
|
||||||
|
|||||||
725
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
BIN
public/icon/logo.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17" height="12" viewBox="0 0 17 12" fill="none"><g opacity="1" transform="translate(0.70001220703125 0.2001953125) rotate(0 7.5 5.5)"><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0 5) rotate(0 7.5 0.5)" d="M0,0.5L15,0.5 " /><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0 0) rotate(0 7.5 0.5)" d="M0,0.5L15,0.5 " /><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7 10) rotate(0 4 0.5)" d="M0,0.5L8,0.5 " /></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 728 B |
|
Before Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,5 +1,6 @@
|
|||||||
import { GET, POST, DELETE } from './request';
|
import { GET, POST, DELETE } from './request';
|
||||||
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
|
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一个聊天框的ID
|
* 获取一个聊天框的ID
|
||||||
@@ -35,28 +36,10 @@ export const postGPT3SendPrompt = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预发 prompt 进行存储
|
* 存储一轮对话
|
||||||
*/
|
*/
|
||||||
export const postChatGptPrompt = ({
|
export const postSaveChat = (data: { windowId: string; prompts: ChatItemType[] }) =>
|
||||||
prompt,
|
POST('/chat/saveChat', data);
|
||||||
windowId,
|
|
||||||
chatId
|
|
||||||
}: {
|
|
||||||
prompt: ChatSiteItemType;
|
|
||||||
windowId: string;
|
|
||||||
chatId: string;
|
|
||||||
}) =>
|
|
||||||
POST<string>(`/chat/preChat`, {
|
|
||||||
windowId,
|
|
||||||
prompt: {
|
|
||||||
obj: prompt.obj,
|
|
||||||
value: prompt.value
|
|
||||||
},
|
|
||||||
chatId
|
|
||||||
});
|
|
||||||
/* 获取 Chat 的 Event 对象,进行持续通信 */
|
|
||||||
export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
|
|
||||||
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}`);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除最后一句
|
* 删除最后一句
|
||||||
|
|||||||
47
src/api/fetch.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
interface StreamFetchProps {
|
||||||
|
url: string;
|
||||||
|
data: any;
|
||||||
|
onMessage: (text: string) => void;
|
||||||
|
}
|
||||||
|
export const streamFetch = ({ url, data, onMessage }: StreamFetchProps) =>
|
||||||
|
new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
const reader = res.body?.getReader();
|
||||||
|
if (!reader) return;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let responseText = '';
|
||||||
|
|
||||||
|
const read = async () => {
|
||||||
|
const { done, value } = await reader?.read();
|
||||||
|
if (done) {
|
||||||
|
if (res.status === 200) {
|
||||||
|
resolve(responseText);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const parseError = JSON.parse(responseText);
|
||||||
|
reject(parseError?.message || '请求异常');
|
||||||
|
} catch (err) {
|
||||||
|
reject('请求异常');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = decoder.decode(value).replace(/<br\/>/g, '\n');
|
||||||
|
res.status === 200 && onMessage(text);
|
||||||
|
responseText += text;
|
||||||
|
read();
|
||||||
|
};
|
||||||
|
read();
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log(err, '====');
|
||||||
|
reject(typeof err === 'string' ? err : err?.message || '请求异常');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -34,7 +34,7 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) {
|
|||||||
*/
|
*/
|
||||||
function checkRes(data: ResponseDataType) {
|
function checkRes(data: ResponseDataType) {
|
||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
console.log(data, 'data is empty');
|
console.log('error->', data, 'data is empty');
|
||||||
return Promise.reject('服务器异常');
|
return Promise.reject('服务器异常');
|
||||||
} else if (data.code < 200 || data.code >= 400) {
|
} else if (data.code < 200 || data.code >= 400) {
|
||||||
return Promise.reject(data.message);
|
return Promise.reject(data.message);
|
||||||
@@ -46,24 +46,23 @@ function checkRes(data: ResponseDataType) {
|
|||||||
* 响应错误
|
* 响应错误
|
||||||
*/
|
*/
|
||||||
function responseError(err: any) {
|
function responseError(err: any) {
|
||||||
console.error('请求错误', err);
|
console.log('error->', '请求错误', err);
|
||||||
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
return Promise.reject('未知错误');
|
return Promise.reject({ message: '未知错误' });
|
||||||
}
|
}
|
||||||
if (typeof err === 'string') {
|
if (typeof err === 'string') {
|
||||||
return Promise.reject(err);
|
return Promise.reject({ message: err });
|
||||||
}
|
}
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
// 有报错响应
|
// 有报错响应
|
||||||
const res = err.response;
|
const res = err.response;
|
||||||
/* token过期,判断请求token与本地是否相同,若不同需要重发 */
|
|
||||||
if (res.data.code in TOKEN_ERROR_CODE) {
|
if (res.data.code in TOKEN_ERROR_CODE) {
|
||||||
clearToken();
|
clearToken();
|
||||||
return Promise.reject('token过期,重新登录');
|
return Promise.reject({ message: 'token过期,重新登录' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject('未知错误');
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 创建请求实例 */
|
/* 创建请求实例 */
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useGlobalStore } from '@/store/global';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
const unAuthPage: { [key: string]: boolean } = {
|
const unAuthPage: { [key: string]: boolean } = {
|
||||||
|
'/': true,
|
||||||
'/login': true,
|
'/login': true,
|
||||||
'/chat': true
|
'/chat': true
|
||||||
};
|
};
|
||||||
@@ -24,10 +25,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
useQuery(
|
useQuery(
|
||||||
[router.pathname, userInfo],
|
[router.pathname, userInfo],
|
||||||
() => {
|
() => {
|
||||||
setLoading(true);
|
|
||||||
if (unAuthPage[router.pathname] === true || userInfo) {
|
if (unAuthPage[router.pathname] === true || userInfo) {
|
||||||
return setLoading(false);
|
return setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
|
setLoading(true);
|
||||||
return getTokenLogin();
|
return getTokenLogin();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -38,7 +39,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.log(error);
|
console.log('error->', error);
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
toast();
|
toast();
|
||||||
},
|
},
|
||||||
@@ -48,7 +49,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return userInfo || unAuthPage[router.pathname] === true ? <>{children}</> : null;
|
return userInfo || unAuthPage[router.pathname] === true ? children : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Auth;
|
export default Auth;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import Link from 'next/link';
|
||||||
import Navbar from './navbar';
|
import Navbar from './navbar';
|
||||||
import NavbarPhone from './navbarPhone';
|
import NavbarPhone from './navbarPhone';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -43,18 +44,16 @@ const navbarList = [
|
|||||||
const Layout = ({ children }: { children: JSX.Element }) => {
|
const Layout = ({ children }: { children: JSX.Element }) => {
|
||||||
const { isPc } = useScreen();
|
const { isPc } = useScreen();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { Loading } = useLoading({
|
const { Loading } = useLoading({ defaultLoading: true });
|
||||||
defaultLoading: true
|
|
||||||
});
|
|
||||||
const { loading } = useGlobalStore();
|
const { loading } = useGlobalStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!unShowLayoutRoute[router.pathname] ? (
|
{!unShowLayoutRoute[router.pathname] ? (
|
||||||
<Box minHeight={'100vh'} backgroundColor={'gray.100'}>
|
<Box h={'100%'} backgroundColor={'gray.100'} overflow={'auto'}>
|
||||||
{isPc ? (
|
{isPc ? (
|
||||||
<>
|
<>
|
||||||
<Box h={'100vh'} position={'fixed'} left={0} top={0} w={'80px'}>
|
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'80px'}>
|
||||||
<Navbar navbarList={navbarList} />
|
<Navbar navbarList={navbarList} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box ml={'80px'} p={7}>
|
<Box ml={'80px'} p={7}>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Box, Flex } from '@chakra-ui/react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
import styles from './style.module.scss';
|
|
||||||
|
|
||||||
export enum NavbarTypeEnum {
|
export enum NavbarTypeEnum {
|
||||||
normal = 'normal',
|
normal = 'normal',
|
||||||
@@ -35,7 +34,7 @@ const Navbar = ({
|
|||||||
>
|
>
|
||||||
{/* logo */}
|
{/* logo */}
|
||||||
<Box pb={4}>
|
<Box pb={4}>
|
||||||
<Image src={'/logo.svg'} width={50} height={100} alt=""></Image>
|
<Image src={'/icon/logo.png'} width={'35'} height={'35'} alt=""></Image>
|
||||||
</Box>
|
</Box>
|
||||||
{/* 导航列表 */}
|
{/* 导航列表 */}
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
@@ -47,6 +46,7 @@ const Navbar = ({
|
|||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
justifyContent={'center'}
|
justifyContent={'center'}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
!item.activeLink.includes(router.pathname) &&
|
||||||
router.push(item.link, undefined, {
|
router.push(item.link, undefined, {
|
||||||
shallow: true
|
shallow: true
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,15 +45,15 @@ const NavbarPhone = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
|
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent maxWidth={'60vw'}>
|
<DrawerContent maxWidth={'50vw'}>
|
||||||
<DrawerBody p={4}>
|
<DrawerBody p={4}>
|
||||||
<Box pb={4}>
|
<Box py={4}>
|
||||||
<Image src={'/logo.svg'} w={'100%'} h={'70px'} pt={2} alt=""></Image>
|
<Image src={'/icon/logo.png'} margin={'auto'} w={'35'} h={'35'} alt=""></Image>
|
||||||
</Box>
|
</Box>
|
||||||
{navbarList.map((item) => (
|
{navbarList.map((item) => (
|
||||||
<Flex
|
<Flex
|
||||||
key={item.label}
|
key={item.label}
|
||||||
mb={4}
|
mb={5}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
justifyContent={'center'}
|
justifyContent={'center'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -61,8 +61,7 @@ const NavbarPhone = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
fontSize={'sm'}
|
h={'60px'}
|
||||||
h={'65px'}
|
|
||||||
borderRadius={'md'}
|
borderRadius={'md'}
|
||||||
{...(item.activeLink.includes(router.pathname)
|
{...(item.activeLink.includes(router.pathname)
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import React from 'react';
|
|||||||
export const codeLight: { [key: string]: React.CSSProperties } = {
|
export const codeLight: { [key: string]: React.CSSProperties } = {
|
||||||
'code[class*=language-]': {
|
'code[class*=language-]': {
|
||||||
color: '#d4d4d4',
|
color: '#d4d4d4',
|
||||||
fontSize: '13px',
|
|
||||||
textShadow: 'none',
|
textShadow: 'none',
|
||||||
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
|
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
whiteSpace: 'pre',
|
whiteSpace: 'pre',
|
||||||
@@ -21,9 +19,7 @@ export const codeLight: { [key: string]: React.CSSProperties } = {
|
|||||||
},
|
},
|
||||||
'pre[class*=language-]': {
|
'pre[class*=language-]': {
|
||||||
color: '#d4d4d4',
|
color: '#d4d4d4',
|
||||||
fontSize: '13px',
|
|
||||||
textShadow: 'none',
|
textShadow: 'none',
|
||||||
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
|
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
whiteSpace: 'pre',
|
whiteSpace: 'pre',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
animation: blink 0.6s infinite;
|
animation: blink 0.6s infinite;
|
||||||
}
|
}
|
||||||
.animation {
|
.animation {
|
||||||
:last-child::after {
|
> :last-child::after {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
content: '';
|
content: '';
|
||||||
width: 4px;
|
width: 4px;
|
||||||
@@ -27,96 +27,398 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown > *:first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
.markdown > *:last-child {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
.markdown a.absent {
|
||||||
|
color: #cc0000;
|
||||||
|
}
|
||||||
|
.markdown a.anchor {
|
||||||
|
bottom: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
left: 0;
|
||||||
|
margin-left: -30px;
|
||||||
|
padding-left: 30px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.markdown h1,
|
||||||
|
.markdown h2,
|
||||||
|
.markdown h3,
|
||||||
|
.markdown h4,
|
||||||
|
.markdown h5,
|
||||||
|
.markdown h6 {
|
||||||
|
cursor: text;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.markdown h1 .mini-icon-link,
|
||||||
|
.markdown h2 .mini-icon-link,
|
||||||
|
.markdown h3 .mini-icon-link,
|
||||||
|
.markdown h4 .mini-icon-link,
|
||||||
|
.markdown h5 .mini-icon-link,
|
||||||
|
.markdown h6 .mini-icon-link {
|
||||||
|
color: #000000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.markdown h1:hover a.anchor,
|
||||||
|
.markdown h2:hover a.anchor,
|
||||||
|
.markdown h3:hover a.anchor,
|
||||||
|
.markdown h4:hover a.anchor,
|
||||||
|
.markdown h5:hover a.anchor,
|
||||||
|
.markdown h6:hover a.anchor {
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: -22px;
|
||||||
|
padding-left: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
top: 15%;
|
||||||
|
}
|
||||||
|
.markdown h1:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h2:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h3:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h4:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h5:hover a.anchor .mini-icon-link,
|
||||||
|
.markdown h6:hover a.anchor .mini-icon-link {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.markdown h1 tt,
|
||||||
|
.markdown h1 code,
|
||||||
|
.markdown h2 tt,
|
||||||
|
.markdown h2 code,
|
||||||
|
.markdown h3 tt,
|
||||||
|
.markdown h3 code,
|
||||||
|
.markdown h4 tt,
|
||||||
|
.markdown h4 code,
|
||||||
|
.markdown h5 tt,
|
||||||
|
.markdown h5 code,
|
||||||
|
.markdown h6 tt,
|
||||||
|
.markdown h6 code {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.markdown h1 {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.markdown h2 {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.markdown h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.markdown h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.markdown h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.markdown h6 {
|
||||||
|
color: #777777;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.markdown p,
|
||||||
|
.markdown blockquote,
|
||||||
|
.markdown ul,
|
||||||
|
.markdown ol,
|
||||||
|
.markdown dl,
|
||||||
|
.markdown table,
|
||||||
|
.markdown pre {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.markdown hr {
|
||||||
|
background: url('https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-350cca8f57223ebd53603021b2e670f4f319f1b7.png')
|
||||||
|
repeat-x scroll 0 0 transparent;
|
||||||
|
border: 0 none;
|
||||||
|
color: #cccccc;
|
||||||
|
height: 4px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown > h2:first-child,
|
||||||
|
.markdown > h1:first-child,
|
||||||
|
.markdown > h1:first-child + h2,
|
||||||
|
.markdown > h3:first-child,
|
||||||
|
.markdown > h4:first-child,
|
||||||
|
.markdown > h5:first-child,
|
||||||
|
.markdown > h6:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.markdown a:first-child h1,
|
||||||
|
.markdown a:first-child h2,
|
||||||
|
.markdown a:first-child h3,
|
||||||
|
.markdown a:first-child h4,
|
||||||
|
.markdown a:first-child h5,
|
||||||
|
.markdown a:first-child h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.markdown h1 + p,
|
||||||
|
.markdown h2 + p,
|
||||||
|
.markdown h3 + p,
|
||||||
|
.markdown h4 + p,
|
||||||
|
.markdown h5 + p,
|
||||||
|
.markdown h6 + p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown li p.first {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.markdown ul,
|
||||||
|
.markdown ol {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
.markdown ul.no-list,
|
||||||
|
.markdown ol.no-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown ul li > *:first-child,
|
||||||
|
.markdown ol li > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown ul ul,
|
||||||
|
.markdown ul ol,
|
||||||
|
.markdown ol ol,
|
||||||
|
.markdown ol ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown dl {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dt {
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 15px 0 5px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dt:first-child {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dt > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dt > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dd {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
.markdown dl dd > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown dl dd > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown blockquote {
|
||||||
|
border-left: 4px solid #dddddd;
|
||||||
|
color: #777777;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
.markdown blockquote > *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.markdown blockquote > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown table th {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.markdown table th,
|
||||||
|
.markdown table td {
|
||||||
|
padding: 6px 13px;
|
||||||
|
}
|
||||||
|
.markdown table tr {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.markdown table tr:nth-child(2n) {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.markdown img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.markdown span.frame {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.frame > span {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 7px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.markdown span.frame span img {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.markdown span.frame span span {
|
||||||
|
clear: both;
|
||||||
|
color: #333333;
|
||||||
|
display: block;
|
||||||
|
padding: 5px 0 0;
|
||||||
|
}
|
||||||
|
.markdown span.align-center {
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.align-center > span {
|
||||||
|
display: block;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.markdown span.align-center span img {
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.markdown span.align-right {
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.align-right > span {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.markdown span.align-right span img {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.markdown span.float-left {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
margin-right: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.float-left span {
|
||||||
|
margin: 13px 0 0;
|
||||||
|
}
|
||||||
|
.markdown span.float-right {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
margin-left: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.markdown span.float-right > span {
|
||||||
|
display: block;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.markdown code,
|
||||||
|
.markdown tt {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
border-radius: 3px 3px 3px 3px;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
.markdown pre > code {
|
||||||
|
background: none repeat scroll 0 0 transparent;
|
||||||
|
border: medium none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.markdown .highlight pre,
|
||||||
|
.markdown pre {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border-radius: 3px 3px 3px 3px;
|
||||||
|
font-size: max(0.9em, 14px);
|
||||||
|
line-height: 19px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
.markdown pre code,
|
||||||
|
.markdown pre tt {
|
||||||
|
background-color: transparent;
|
||||||
|
border: medium none;
|
||||||
|
}
|
||||||
.markdown {
|
.markdown {
|
||||||
/* 标题样式 */
|
text-align: justify;
|
||||||
h1 {
|
word-break: break-all;
|
||||||
font-size: 1.8rem;
|
overflow-y: hidden;
|
||||||
}
|
tab-size: 4;
|
||||||
|
word-spacing: normal;
|
||||||
h2 {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
font-size: 0.83rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 列表样式 */
|
|
||||||
ol,
|
|
||||||
ul {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style: inside;
|
|
||||||
}
|
|
||||||
ol {
|
|
||||||
list-style: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 链接样式 */
|
|
||||||
a {
|
|
||||||
color: #0077cc;
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px solid #0077cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #005580;
|
|
||||||
border-bottom-color: #005580;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图片样式 */
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 200px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 强调样式 */
|
|
||||||
em,
|
|
||||||
i {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong,
|
|
||||||
b {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 代码样式 */
|
|
||||||
code {
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 10px 15px;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
background-color: #222 !important;
|
background-color: #222 !important;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code {
|
pre code {
|
||||||
display: block;
|
background-color: #222 !important;
|
||||||
border: none;
|
|
||||||
background-color: #222;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
a {
|
||||||
line-height: 1.7;
|
text-decoration: underline;
|
||||||
|
color: var(--chakra-colors-blue-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0px;
|
||||||
|
|
||||||
|
thead tr:first-child th {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-color: #ccc;
|
||||||
|
background-color: rgba(236, 236, 241, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top-right-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-color: #ccc;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-right-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:last-child {
|
||||||
|
overflow: hidden;
|
||||||
|
td {
|
||||||
|
&:first-child {
|
||||||
|
border-bottom-left-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
import React, { useMemo, memo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import styles from './index.module.scss';
|
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { codeLight } from './codeLight';
|
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
import { Box, Flex } from '@chakra-ui/react';
|
||||||
import { useCopyData } from '@/utils/tools';
|
import { useCopyData } from '@/utils/tools';
|
||||||
import Icon from '@/components/Icon';
|
import Icon from '@/components/Icon';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import remarkMath from 'remark-math';
|
||||||
|
import rehypeKatex from 'rehype-katex';
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
import { codeLight } from './codeLight';
|
||||||
|
|
||||||
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
|
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
|
||||||
// const formatSource = useMemo(() => source.replace(/\n/g, '\n'), [source]);
|
const formatSource = useMemo(() => source, [source]);
|
||||||
const { copyData } = useCopyData();
|
const { copyData } = useCopyData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
className={`${styles.markdown} ${
|
className={`${styles.markdown} ${
|
||||||
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
|
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
|
||||||
}`}
|
}`}
|
||||||
rehypePlugins={[remarkGfm]}
|
remarkPlugins={[remarkMath]}
|
||||||
skipHtml={true}
|
rehypePlugins={[remarkGfm, rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
p: 'div',
|
|
||||||
pre: 'div',
|
pre: 'div',
|
||||||
code({ node, inline, className, children, ...props }) {
|
code({ node, inline, className, children, ...props }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
const match = /language-(\w+)/.exec(className || '');
|
||||||
const code = String(children).replace(/\n$/, '');
|
const code = String(children);
|
||||||
|
|
||||||
return (
|
return !inline || match ? (
|
||||||
<Box my={3} borderRadius={'md'} overflow={'hidden'}>
|
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
|
||||||
<Flex py={2} px={5} backgroundColor={'#323641'} color={'#fff'} fontSize={'sm'}>
|
<Flex
|
||||||
|
py={2}
|
||||||
|
px={5}
|
||||||
|
backgroundColor={'#323641'}
|
||||||
|
color={'#fff'}
|
||||||
|
fontSize={'sm'}
|
||||||
|
userSelect={'none'}
|
||||||
|
>
|
||||||
<Box flex={1}>{match?.[1]}</Box>
|
<Box flex={1}>{match?.[1]}</Box>
|
||||||
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
||||||
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
|
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
|
||||||
@@ -36,18 +47,23 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
|
|||||||
</Flex>
|
</Flex>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
style={codeLight as any}
|
style={codeLight as any}
|
||||||
showLineNumbers
|
|
||||||
language={match?.[1]}
|
language={match?.[1]}
|
||||||
|
PreTag="pre"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{code}
|
{code}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</Box>
|
</Box>
|
||||||
|
) : (
|
||||||
|
<code className={className} {...props}>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
linkTarget="_blank"
|
||||||
>
|
>
|
||||||
{source}
|
{formatSource}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ export enum EmailTypeEnum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const introPage = `
|
export const introPage = `
|
||||||
## 欢迎使用 Doc GPT
|
## 欢迎使用 Fast GPT
|
||||||
|
|
||||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
[Git 仓库](https://github.com/c121914yu/FastGPT)
|
||||||
|
|
||||||
|
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
||||||
1. 使用邮箱注册账号。
|
1. 使用邮箱注册账号。
|
||||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
||||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||||
@@ -39,6 +40,5 @@ export const introPage = `
|
|||||||
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
||||||
|
|
||||||
### 其他问题
|
### 其他问题
|
||||||
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
|
还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。
|
||||||

|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -20,24 +20,27 @@ const Button = defineStyleConfig({
|
|||||||
baseStyle: {},
|
baseStyle: {},
|
||||||
sizes: {
|
sizes: {
|
||||||
sm: {
|
sm: {
|
||||||
fontSize: 'sm',
|
fontSize: 'xs',
|
||||||
px: 3,
|
px: 3,
|
||||||
py: 0,
|
py: 0,
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
height: '26px'
|
height: '26px',
|
||||||
|
lineHeight: '26px'
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
fontSize: 'md',
|
fontSize: 'sm',
|
||||||
px: 6,
|
px: 6,
|
||||||
py: 0,
|
py: 0,
|
||||||
height: '34px',
|
height: '34px',
|
||||||
|
lineHeight: '34px',
|
||||||
fontWeight: 'normal'
|
fontWeight: 'normal'
|
||||||
},
|
},
|
||||||
lg: {
|
lg: {
|
||||||
fontSize: 'lg',
|
fontSize: 'md',
|
||||||
px: 8,
|
px: 8,
|
||||||
py: 0,
|
py: 0,
|
||||||
height: '42px',
|
height: '42px',
|
||||||
|
lineHeight: '42px',
|
||||||
fontWeight: 'normal'
|
fontWeight: 'normal'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -58,13 +61,12 @@ export const theme = extendTheme({
|
|||||||
global: {
|
global: {
|
||||||
'html, body': {
|
'html, body': {
|
||||||
color: 'blackAlpha.800',
|
color: 'blackAlpha.800',
|
||||||
fontSize: '14px'
|
height: '100%',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
overflowY: 'hidden'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fonts: {
|
|
||||||
body: 'system-ui, sans-serif'
|
|
||||||
},
|
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
xs: '0.8rem',
|
xs: '0.8rem',
|
||||||
sm: '0.9rem',
|
sm: '0.9rem',
|
||||||
@@ -80,6 +82,9 @@ export const theme = extendTheme({
|
|||||||
'8xl': '6rem',
|
'8xl': '6rem',
|
||||||
'9xl': '8rem'
|
'9xl': '8rem'
|
||||||
},
|
},
|
||||||
|
fonts: {
|
||||||
|
body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
Modal: ModalTheme,
|
Modal: ModalTheme,
|
||||||
Button
|
Button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogBody,
|
AlertDialogBody,
|
||||||
@@ -17,45 +17,51 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
|
|||||||
const cancelCb = useRef<any>();
|
const cancelCb = useRef<any>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
openConfirm: (confirm?: any, cancel?: any) => {
|
openConfirm: useCallback(
|
||||||
onOpen();
|
(confirm?: any, cancel?: any) => {
|
||||||
confirmCb.current = confirm;
|
onOpen();
|
||||||
cancelCb.current = cancel;
|
confirmCb.current = confirm;
|
||||||
},
|
cancelCb.current = cancel;
|
||||||
ConfirmChild: () => (
|
},
|
||||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
[onOpen]
|
||||||
<AlertDialogOverlay>
|
),
|
||||||
<AlertDialogContent>
|
ConfirmChild: useCallback(
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
() => (
|
||||||
{title}
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
</AlertDialogHeader>
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
{title}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogBody>{content}</AlertDialogBody>
|
<AlertDialogBody>{content}</AlertDialogBody>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button
|
<Button
|
||||||
colorScheme={'gray'}
|
colorScheme={'gray'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
typeof cancelCb.current === 'function' && cancelCb.current();
|
typeof cancelCb.current === 'function' && cancelCb.current();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
ml={3}
|
ml={4}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClose();
|
onClose();
|
||||||
typeof confirmCb.current === 'function' && confirmCb.current();
|
typeof confirmCb.current === 'function' && confirmCb.current();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确认
|
确认
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialogOverlay>
|
</AlertDialogOverlay>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
),
|
||||||
|
[content, isOpen, onClose, title]
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { Spinner, Flex } from '@chakra-ui/react';
|
import { Spinner, Flex } from '@chakra-ui/react';
|
||||||
|
|
||||||
export const useLoading = (props?: { defaultLoading: boolean }) => {
|
export const useLoading = (props?: { defaultLoading: boolean }) => {
|
||||||
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
|
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
|
||||||
|
|
||||||
const Loading = ({
|
const Loading = useCallback(
|
||||||
loading,
|
({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
|
||||||
fixed = true
|
return isLoading || loading ? (
|
||||||
}: {
|
<Flex
|
||||||
loading?: boolean;
|
position={fixed ? 'fixed' : 'absolute'}
|
||||||
fixed?: boolean;
|
zIndex={100}
|
||||||
}): JSX.Element | null => {
|
backgroundColor={'rgba(255,255,255,0.5)'}
|
||||||
return isLoading || loading ? (
|
top={0}
|
||||||
<Flex
|
left={0}
|
||||||
position={fixed ? 'fixed' : 'absolute'}
|
right={0}
|
||||||
zIndex={100}
|
bottom={0}
|
||||||
backgroundColor={'rgba(255,255,255,0.5)'}
|
alignItems={'center'}
|
||||||
top={0}
|
justifyContent={'center'}
|
||||||
left={0}
|
>
|
||||||
right={0}
|
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
|
||||||
bottom={0}
|
</Flex>
|
||||||
alignItems={'center'}
|
) : null;
|
||||||
justifyContent={'center'}
|
},
|
||||||
>
|
[isLoading]
|
||||||
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
|
);
|
||||||
</Flex>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export function useScreen() {
|
|||||||
isPc,
|
isPc,
|
||||||
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [isPc]),
|
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [isPc]),
|
||||||
mediaMdSm: useMemo(() => (isPc ? 'md' : 'sm'), [isPc]),
|
mediaMdSm: useMemo(() => (isPc ? 'md' : 'sm'), [isPc]),
|
||||||
media: (pc: number | string, phone: number | string) => (isPc ? pc : phone)
|
media: (pc: any, phone: any) => (isPc ? pc : phone)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { sendCodeToEmail } from '@/api/user';
|
import { sendCodeToEmail } from '@/api/user';
|
||||||
import { EmailTypeEnum } from '@/constants/common';
|
import { EmailTypeEnum } from '@/constants/common';
|
||||||
import { useToast } from '@chakra-ui/react';
|
|
||||||
let timer: any;
|
let timer: any;
|
||||||
|
import { useToast } from './useToast';
|
||||||
|
|
||||||
export const useSendCode = () => {
|
export const useSendCode = () => {
|
||||||
const toast = useToast({
|
const { toast } = useToast();
|
||||||
position: 'top',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
const [codeSending, setCodeSending] = useState(false);
|
const [codeSending, setCodeSending] = useState(false);
|
||||||
const [codeCountDown, setCodeCountDown] = useState(0);
|
const [codeCountDown, setCodeCountDown] = useState(0);
|
||||||
const sendCodeText = useMemo(() => {
|
const sendCodeText = useMemo(() => {
|
||||||
@@ -43,13 +40,11 @@ export const useSendCode = () => {
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
typeof error === 'string' &&
|
toast({
|
||||||
toast({
|
title: error.message || '发送验证码异常',
|
||||||
title: error,
|
status: 'error'
|
||||||
status: 'error',
|
});
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setCodeSending(false);
|
setCodeSending(false);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
import type { AppProps, NextWebVitalsMetric } from 'next/app';
|
import type { AppProps, NextWebVitalsMetric } from 'next/app';
|
||||||
|
import Script from 'next/script';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { ChakraProvider } from '@chakra-ui/react';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
import Layout from '@/components/Layout';
|
import Layout from '@/components/Layout';
|
||||||
import { theme } from '@/constants/theme';
|
import { theme } from '@/constants/theme';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import NProgress from 'nprogress'; //nprogress module
|
||||||
|
import Router from 'next/router';
|
||||||
|
import 'nprogress/nprogress.css';
|
||||||
import '../styles/reset.scss';
|
import '../styles/reset.scss';
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
//Binding events.
|
||||||
// Create a client
|
Router.events.on('routeChangeStart', () => NProgress.start());
|
||||||
const queryClient = new QueryClient({
|
Router.events.on('routeChangeComplete', () => NProgress.done());
|
||||||
defaultOptions: {
|
Router.events.on('routeChangeError', () => NProgress.done());
|
||||||
queries: {
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
retry: false,
|
|
||||||
cacheTime: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false,
|
||||||
|
cacheTime: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Doc GPT</title>
|
<title>Fast GPT</title>
|
||||||
<meta name="description" content="Generated by Doc GPT" />
|
<meta name="description" content="Generated by Fast GPT" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<script src="/iconfont.js" async></script>
|
|
||||||
</Head>
|
</Head>
|
||||||
|
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
|
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
|
||||||
|
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||||
import type { ModelType } from '@/types/model';
|
import type { ModelType } from '@/types/model';
|
||||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
|
import { jsonRes } from '@/service/response';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
res.writeHead(200, {
|
const { chatId, windowId, prompt } = req.body as {
|
||||||
Connection: 'keep-alive',
|
prompt: ChatItemType;
|
||||||
'Content-Encoding': 'none',
|
windowId: string;
|
||||||
'Cache-Control': 'no-cache',
|
chatId: string;
|
||||||
'Content-Type': 'text/event-stream'
|
};
|
||||||
});
|
|
||||||
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!windowId || !chatId) {
|
if (!windowId || !chatId || !prompt) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,15 +28,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const model: ModelType = chat.modelId;
|
const model: ModelType = chat.modelId;
|
||||||
|
|
||||||
const map = {
|
|
||||||
Human: ChatCompletionRequestMessageRoleEnum.User,
|
|
||||||
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
|
||||||
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
|
||||||
};
|
|
||||||
// 读取对话内容
|
// 读取对话内容
|
||||||
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
|
||||||
|
prompts.push(prompt);
|
||||||
|
|
||||||
// 长度过滤
|
// 上下文长度过滤
|
||||||
const maxContext = model.security.contextMaxLen;
|
const maxContext = model.security.contextMaxLen;
|
||||||
const filterPrompts =
|
const filterPrompts =
|
||||||
prompts.length > maxContext + 2
|
prompts.length > maxContext + 2
|
||||||
@@ -44,15 +40,26 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
: prompts.slice(0, prompts.length);
|
: prompts.slice(0, prompts.length);
|
||||||
|
|
||||||
// 格式化文本内容
|
// 格式化文本内容
|
||||||
|
const map = {
|
||||||
|
Human: ChatCompletionRequestMessageRoleEnum.User,
|
||||||
|
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
|
||||||
|
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
|
||||||
|
};
|
||||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||||
(item: ChatItemType) => ({
|
(item: ChatItemType) => ({
|
||||||
role: map[item.obj],
|
role: map[item.obj],
|
||||||
content: item.value
|
content: item.value
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
// 第一句话,强调代码类型
|
||||||
|
formatPrompts.unshift({
|
||||||
|
role: ChatCompletionRequestMessageRoleEnum.System,
|
||||||
|
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
|
||||||
|
});
|
||||||
|
|
||||||
// 获取 chatAPI
|
// 获取 chatAPI
|
||||||
const chatAPI = getOpenAIApi(userApiKey);
|
const chatAPI = getOpenAIApi(userApiKey);
|
||||||
|
// 发出请求
|
||||||
const chatResponse = await chatAPI.createChatCompletion(
|
const chatResponse = await chatAPI.createChatCompletion(
|
||||||
{
|
{
|
||||||
model: model.service.chatModel,
|
model: model.service.chatModel,
|
||||||
@@ -61,50 +68,51 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
messages: formatPrompts,
|
messages: formatPrompts,
|
||||||
stream: true
|
stream: true
|
||||||
},
|
},
|
||||||
openaiProxy
|
{
|
||||||
|
timeout: 20000,
|
||||||
|
responseType: 'stream',
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
formatPrompts.reduce((sum, item) => sum + item.content.length, 0),
|
||||||
|
'response success'
|
||||||
);
|
);
|
||||||
|
|
||||||
// 截取字符串内容
|
// 创建响应流
|
||||||
const reg = /{"content"(.*)"}/g;
|
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
|
||||||
// @ts-ignore
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
const match = chatResponse.data.match(reg);
|
res.setHeader('X-Accel-Buffering', 'no');
|
||||||
let AIResponse = '';
|
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||||
if (match) {
|
|
||||||
match.forEach((item: string, i: number) => {
|
const pass = new PassThrough();
|
||||||
try {
|
pass.pipe(res);
|
||||||
const json = JSON.parse(item);
|
|
||||||
// 开头的换行忽略
|
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
|
||||||
if (i === 0 && json.content?.startsWith('\n')) return;
|
if (event.type !== 'event') return;
|
||||||
AIResponse += json.content;
|
const data = event.data;
|
||||||
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
|
if (data === '[DONE]') return;
|
||||||
content && res.write(`data: ${content}\n\n`);
|
try {
|
||||||
} catch (err) {
|
const json = JSON.parse(data);
|
||||||
err;
|
const content: string = json?.choices?.[0].delta.content || '';
|
||||||
}
|
if (!content) return;
|
||||||
});
|
// console.log('content:', content)
|
||||||
|
pass.push(content.replace(/\n/g, '<br/>'));
|
||||||
|
} catch (error) {
|
||||||
|
error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for await (const chunk of chatResponse.data as any) {
|
||||||
|
const parser = createParser(onParse);
|
||||||
|
parser.feed(decodeURIComponent(chunk));
|
||||||
}
|
}
|
||||||
res.write(`data: [DONE]\n\n`);
|
pass.push(null);
|
||||||
|
|
||||||
// 存入库
|
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
|
||||||
$push: {
|
|
||||||
content: {
|
|
||||||
obj: 'AI',
|
|
||||||
value: AIResponse
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.log(err?.response?.data || err);
|
res.status(500);
|
||||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
jsonRes(res, {
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
code: 500,
|
||||||
$pop: { content: 1 },
|
error: err
|
||||||
updateTime: Date.now()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { connectToDatabase, Chat } from '@/service/mongo';
|
|||||||
import type { ModelType } from '@/types/model';
|
import type { ModelType } from '@/types/model';
|
||||||
import { getOpenAIApi } from '@/service/utils/chat';
|
import { getOpenAIApi } from '@/service/utils/chat';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
|
|
||||||
/* 发送提示词 */
|
/* 发送提示词 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -66,7 +66,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
presence_penalty: 0.6,
|
presence_penalty: 0.6,
|
||||||
stop: ['###']
|
stop: ['###']
|
||||||
},
|
},
|
||||||
openaiProxy
|
{
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const responseMessage = response.data.choices[0]?.text;
|
const responseMessage = response.data.choices[0]?.text;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 安全校验
|
// 安全校验
|
||||||
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
|
if (!chat || chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
|
||||||
throw new Error('聊天框已过期');
|
throw new Error('聊天框已过期');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
code: 500,
|
code: 500,
|
||||||
error: err
|
error: err
|
||||||
|
|||||||
@@ -2,34 +2,31 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { ChatItemType } from '@/types/chat';
|
import { ChatItemType } from '@/types/chat';
|
||||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||||
import type { ModelType } from '@/types/model';
|
|
||||||
import { authChat } from '@/service/utils/chat';
|
|
||||||
|
|
||||||
/* 聊天预请求,存储聊天内容 */
|
/* 聊天内容存存储 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const { windowId, prompt, chatId } = req.body as {
|
const { windowId, prompts } = req.body as {
|
||||||
windowId: string;
|
windowId: string;
|
||||||
prompt: ChatItemType;
|
prompts: ChatItemType[];
|
||||||
chatId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!windowId || !prompt || !chatId) {
|
if (!windowId || !prompts) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
const { chat } = await authChat(chatId);
|
// 存入库
|
||||||
|
|
||||||
// 长度校验
|
|
||||||
const model: ModelType = chat.modelId;
|
|
||||||
if (prompt.value.length > model.security.contentMaxLen) {
|
|
||||||
throw new Error('输入内容超长');
|
|
||||||
}
|
|
||||||
|
|
||||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||||
$push: { content: prompt },
|
$push: {
|
||||||
|
content: {
|
||||||
|
$each: prompts.map((item) => ({
|
||||||
|
obj: item.obj,
|
||||||
|
value: item.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
updateTime: Date.now()
|
updateTime: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
|
|||||||
import { TrainingStatusEnum } from '@/constants/model';
|
import { TrainingStatusEnum } from '@/constants/model';
|
||||||
import { getOpenAIApi } from '@/service/utils/chat';
|
import { getOpenAIApi } from '@/service/utils/chat';
|
||||||
import { TrainingItemType } from '@/types/training';
|
import { TrainingItemType } from '@/types/training';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
|
|
||||||
/* 获取我的模型 */
|
/* 获取我的模型 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
@@ -47,12 +47,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
if (training) {
|
if (training) {
|
||||||
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||||
// 获取训练记录
|
// 获取训练记录
|
||||||
const tuneRecord = await openai.retrieveFineTune(training.tuneId, openaiProxy);
|
const tuneRecord = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
|
||||||
|
|
||||||
// 删除训练文件
|
// 删除训练文件
|
||||||
openai.deleteFile(tuneRecord.data.training_files[0].id, openaiProxy);
|
openai.deleteFile(tuneRecord.data.training_files[0].id, { httpsAgent });
|
||||||
// 取消训练
|
// 取消训练
|
||||||
openai.cancelFineTune(training.tuneId, openaiProxy);
|
openai.cancelFineTune(training.tuneId, { httpsAgent });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除对应训练记录
|
// 删除对应训练记录
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import fs from 'fs';
|
|||||||
import type { ModelType } from '@/types/model';
|
import type { ModelType } from '@/types/model';
|
||||||
import type { OpenAIApi } from 'openai';
|
import type { OpenAIApi } from 'openai';
|
||||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
|
|
||||||
// 关闭next默认的bodyParser处理方式
|
// 关闭next默认的bodyParser处理方式
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { ModelType } from '@/types/model';
|
|||||||
import { TrainingItemType } from '@/types/training';
|
import { TrainingItemType } from '@/types/training';
|
||||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||||
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
|
|
||||||
/* 更新训练状态 */
|
/* 更新训练状态 */
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -46,11 +46,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
|
||||||
|
|
||||||
// 获取 openai 的训练情况
|
// 获取 openai 的训练情况
|
||||||
const { data } = await openai.retrieveFineTune(training.tuneId, openaiProxy);
|
const { data } = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
|
||||||
|
|
||||||
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
||||||
// 删除训练文件
|
// 删除训练文件
|
||||||
openai.deleteFile(data.training_files[0].id, openaiProxy);
|
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||||
|
|
||||||
// 更新模型
|
// 更新模型
|
||||||
await Model.findByIdAndUpdate(modelId, {
|
await Model.findByIdAndUpdate(modelId, {
|
||||||
@@ -74,7 +74,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
if (data.status === OpenAiTuneStatusEnum.cancelled) {
|
if (data.status === OpenAiTuneStatusEnum.cancelled) {
|
||||||
// 删除训练文件
|
// 删除训练文件
|
||||||
openai.deleteFile(data.training_files[0].id, openaiProxy);
|
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||||
|
|
||||||
// 更新模型
|
// 更新模型
|
||||||
await Model.findByIdAndUpdate(modelId, {
|
await Model.findByIdAndUpdate(modelId, {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import fs from 'fs';
|
|||||||
import type { ModelType } from '@/types/model';
|
import type { ModelType } from '@/types/model';
|
||||||
import type { OpenAIApi } from 'openai';
|
import type { OpenAIApi } from 'openai';
|
||||||
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
|
|
||||||
// 关闭next默认的bodyParser处理方式
|
// 关闭next默认的bodyParser处理方式
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -73,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fs.createReadStream(file.filepath),
|
fs.createReadStream(file.filepath),
|
||||||
'fine-tune',
|
'fine-tune',
|
||||||
openaiProxy
|
{ httpsAgent }
|
||||||
);
|
);
|
||||||
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
|
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
model: trainingType,
|
model: trainingType,
|
||||||
suffix: model.name
|
suffix: model.name
|
||||||
},
|
},
|
||||||
openaiProxy
|
{ httpsAgent }
|
||||||
);
|
);
|
||||||
|
|
||||||
trainId = trainRes.data.id; // 记录训练 ID
|
trainId = trainRes.data.id; // 记录训练 ID
|
||||||
@@ -114,9 +114,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (openai) {
|
if (openai) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
uploadFileId && openai.deleteFile(uploadFileId, openaiProxy);
|
uploadFileId && openai.deleteFile(uploadFileId, { httpsAgent });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
trainId && openai.cancelFineTune(trainId, openaiProxy);
|
trainId && openai.cancelFineTune(trainId, { httpsAgent });
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonRes(res, {
|
jsonRes(res, {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
|
||||||
if (req.method !== 'GET') return;
|
|
||||||
|
|
||||||
res.writeHead(200, {
|
|
||||||
Connection: 'keep-alive',
|
|
||||||
'Content-Encoding': 'none',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Content-Type': 'text/event-stream'
|
|
||||||
});
|
|
||||||
|
|
||||||
let val = 0;
|
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
console.log('发送消息', val);
|
|
||||||
res.write(`data: ${val++}\n\n`);
|
|
||||||
if (val > 30) {
|
|
||||||
clearInterval(timer);
|
|
||||||
res.write(`data: [DONE]\n\n`);
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import { getOpenAIApi } from '@/service/utils/chat';
|
|||||||
import { getUserOpenaiKey } from '@/service/utils/tools';
|
import { getUserOpenaiKey } from '@/service/utils/tools';
|
||||||
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
|
||||||
import { sendTrainSucceed } from '@/service/utils/sendEmail';
|
import { sendTrainSucceed } from '@/service/utils/sendEmail';
|
||||||
import { openaiProxy } from '@/service/utils/tools';
|
import { httpsAgent } from '@/service/utils/tools';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
@@ -23,10 +23,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const response = await Promise.all(
|
const response = await Promise.all(
|
||||||
trainingRecords.map(async (item) => {
|
trainingRecords.map(async (item) => {
|
||||||
const { data } = await openai.retrieveFineTune(item.tuneId, openaiProxy);
|
const { data } = await openai.retrieveFineTune(item.tuneId, { httpsAgent });
|
||||||
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
if (data.status === OpenAiTuneStatusEnum.succeeded) {
|
||||||
// 删除训练文件
|
// 删除训练文件
|
||||||
openai.deleteFile(data.training_files[0].id, openaiProxy);
|
openai.deleteFile(data.training_files[0].id, { httpsAgent });
|
||||||
|
|
||||||
const model = await Model.findById(item.modelId).populate({
|
const model = await Model.findById(item.modelId).populate({
|
||||||
path: 'userId',
|
path: 'userId',
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
if (type === EmailTypeEnum.register) {
|
if (type === EmailTypeEnum.register) {
|
||||||
const maxCount = process.env.MAX_USER ? +process.env.MAX_USER : Infinity;
|
const maxCount = process.env.MAX_USER ? +process.env.MAX_USER : Infinity;
|
||||||
const userCount = await User.count();
|
const userCount = await User.count();
|
||||||
|
|
||||||
if (userCount >= maxCount) {
|
if (userCount >= maxCount) {
|
||||||
throw new Error('当前注册用户已满,请等待名额~');
|
throw new Error('当前注册用户已满,请等待名额~');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const { authorization } = req.headers;
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少登录凭证');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = await authToken(authorization);
|
const userId = await authToken(authorization);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
const { authorization } = req.headers;
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
if (!authorization) {
|
if (!authorization) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('无权操作');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = await authToken(authorization);
|
const userId = await authToken(authorization);
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
import React, { useCallback, useState, useRef, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import {
|
import { getInitChatSiteInfo, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat';
|
||||||
getInitChatSiteInfo,
|
|
||||||
postGPT3SendPrompt,
|
|
||||||
getChatGPTSendEvent,
|
|
||||||
postChatGptPrompt,
|
|
||||||
delLastMessage
|
|
||||||
} from '@/api/chat';
|
|
||||||
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
|
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
|
||||||
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import Icon from '@/components/Icon';
|
import Icon from '@/components/Icon';
|
||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
import Markdown from '@/components/Markdown';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
|
||||||
import { OpenAiModelEnum } from '@/constants/model';
|
import { OpenAiModelEnum } from '@/constants/model';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
import { streamFetch } from '@/api/fetch';
|
||||||
|
|
||||||
const Chat = () => {
|
const Markdown = dynamic(() => import('@/components/Markdown'));
|
||||||
|
|
||||||
|
const textareaMinH = '22px';
|
||||||
|
|
||||||
|
const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { media } = useScreen();
|
const { isPc, media } = useScreen();
|
||||||
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
|
|
||||||
const ChatBox = useRef<HTMLDivElement>(null);
|
const ChatBox = useRef<HTMLDivElement>(null);
|
||||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
@@ -32,11 +30,10 @@ const Chat = () => {
|
|||||||
|
|
||||||
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
|
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
|
||||||
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
|
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
|
||||||
const { Loading } = useLoading();
|
const { setLoading } = useGlobalStore();
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
// 滚动到底部
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ChatBox.current &&
|
ChatBox.current &&
|
||||||
ChatBox.current.scrollTo({
|
ChatBox.current.scrollTo({
|
||||||
@@ -47,28 +44,56 @@ const Chat = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 初始化聊天框
|
// 初始化聊天框
|
||||||
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), {
|
useQuery(
|
||||||
cacheTime: 5 * 60 * 1000,
|
['initData'],
|
||||||
onSuccess(res) {
|
() => {
|
||||||
if (!res) return;
|
setLoading(true);
|
||||||
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
|
return getInitChatSiteInfo(chatId, windowId);
|
||||||
|
|
||||||
setChatSiteData(res.chatSite);
|
|
||||||
setChatList(
|
|
||||||
res.history.map((item) => ({
|
|
||||||
...item,
|
|
||||||
status: 'finish'
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
scrollToBottom();
|
|
||||||
},
|
},
|
||||||
onError() {
|
{
|
||||||
toast({
|
onSuccess(res) {
|
||||||
title: '初始化异常',
|
// 可能没有 windowId,给它设置一下
|
||||||
status: 'error'
|
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
|
||||||
});
|
|
||||||
|
setChatSiteData(res.chatSite);
|
||||||
|
setChatList(
|
||||||
|
res.history.map((item) => ({
|
||||||
|
...item,
|
||||||
|
status: 'finish'
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
scrollToBottom();
|
||||||
|
},
|
||||||
|
onError(e: any) {
|
||||||
|
toast({
|
||||||
|
title: e?.message || '初始化异常,请检查地址',
|
||||||
|
status: 'error',
|
||||||
|
isClosable: true,
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled() {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
|
// 重置输入内容
|
||||||
|
const resetInputVal = useCallback((val: string) => {
|
||||||
|
setInputVal(val);
|
||||||
|
setTimeout(() => {
|
||||||
|
/* 回到最小高度 */
|
||||||
|
if (TextareaDom.current) {
|
||||||
|
TextareaDom.current.style.height =
|
||||||
|
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 重载对话
|
||||||
|
const resetChat = useCallback(() => {
|
||||||
|
window.open(`/chat?chatId=${chatId}`, '_self');
|
||||||
|
}, [chatId]);
|
||||||
|
|
||||||
// gpt3 方法
|
// gpt3 方法
|
||||||
const gpt3ChatPrompt = useCallback(
|
const gpt3ChatPrompt = useCallback(
|
||||||
@@ -98,49 +123,64 @@ const Chat = () => {
|
|||||||
const chatGPTPrompt = useCallback(
|
const chatGPTPrompt = useCallback(
|
||||||
async (newChatList: ChatSiteItemType[]) => {
|
async (newChatList: ChatSiteItemType[]) => {
|
||||||
if (!windowId) return;
|
if (!windowId) return;
|
||||||
/* 预请求,把消息存入库 */
|
const prompt = {
|
||||||
await postChatGptPrompt({
|
obj: newChatList[newChatList.length - 1].obj,
|
||||||
windowId,
|
value: newChatList[newChatList.length - 1].value
|
||||||
prompt: newChatList[newChatList.length - 1],
|
};
|
||||||
chatId
|
// 流请求,获取数据
|
||||||
|
const res = await streamFetch({
|
||||||
|
url: '/api/chat/chatGpt',
|
||||||
|
data: {
|
||||||
|
windowId,
|
||||||
|
prompt,
|
||||||
|
chatId
|
||||||
|
},
|
||||||
|
onMessage: (text: string) => {
|
||||||
|
setChatList((state) =>
|
||||||
|
state.map((item, index) => {
|
||||||
|
if (index !== state.length - 1) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: item.value + text
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// 保存对话信息
|
||||||
const event = getChatGPTSendEvent(chatId, windowId);
|
try {
|
||||||
event.onmessage = ({ data }) => {
|
await postSaveChat({
|
||||||
if (data === '[DONE]') {
|
windowId,
|
||||||
event.close();
|
prompts: [
|
||||||
setChatList((state) =>
|
prompt,
|
||||||
state.map((item, index) => {
|
{
|
||||||
if (index !== state.length - 1) return item;
|
obj: 'AI',
|
||||||
return {
|
value: res as string
|
||||||
...item,
|
}
|
||||||
status: 'finish'
|
]
|
||||||
};
|
});
|
||||||
})
|
} catch (err) {
|
||||||
);
|
toast({
|
||||||
resolve('');
|
title: '存储对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
|
||||||
} else if (data) {
|
status: 'warning',
|
||||||
const msg = data.replace(/<br\/>/g, '\n');
|
duration: 3000,
|
||||||
setChatList((state) =>
|
isClosable: true
|
||||||
state.map((item, index) => {
|
});
|
||||||
if (index !== state.length - 1) return item;
|
}
|
||||||
return {
|
|
||||||
...item,
|
// 设置完成状态
|
||||||
value: item.value + msg
|
setChatList((state) =>
|
||||||
};
|
state.map((item, index) => {
|
||||||
})
|
if (index !== state.length - 1) return item;
|
||||||
);
|
return {
|
||||||
}
|
...item,
|
||||||
};
|
status: 'finish'
|
||||||
event.onerror = (err) => {
|
};
|
||||||
console.error(err, '===');
|
})
|
||||||
event.close();
|
);
|
||||||
reject('对话出现错误');
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[chatId, windowId]
|
[chatId, toast, windowId]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,15 +214,8 @@ const Chat = () => {
|
|||||||
|
|
||||||
// 插入内容
|
// 插入内容
|
||||||
setChatList(newChatList);
|
setChatList(newChatList);
|
||||||
setInputVal('');
|
resetInputVal('');
|
||||||
// 滚动到底部
|
scrollToBottom();
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
|
|
||||||
if (TextareaDom.current) {
|
|
||||||
TextareaDom.current.style.height = 22 + 'px';
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
const fnMap: { [key: string]: any } = {
|
const fnMap: { [key: string]: any } = {
|
||||||
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
|
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
|
||||||
@@ -200,15 +233,15 @@ const Chat = () => {
|
|||||||
if (typeof fnMap[chatSiteData.chatModel] === 'function') {
|
if (typeof fnMap[chatSiteData.chatModel] === 'function') {
|
||||||
await fnMap[chatSiteData.chatModel](requestPrompt);
|
await fnMap[chatSiteData.chatModel](requestPrompt);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
title: typeof err === 'string' ? err : '聊天已过期',
|
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||||
status: 'warning',
|
status: 'warning',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true
|
isClosable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
setInputVal(storeInput);
|
resetInputVal(storeInput);
|
||||||
|
|
||||||
setChatList(newChatList.slice(0, newChatList.length - 2));
|
setChatList(newChatList.slice(0, newChatList.length - 2));
|
||||||
}
|
}
|
||||||
@@ -219,6 +252,7 @@ const Chat = () => {
|
|||||||
gpt3ChatPrompt,
|
gpt3ChatPrompt,
|
||||||
inputVal,
|
inputVal,
|
||||||
isChatting,
|
isChatting,
|
||||||
|
resetInputVal,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
toast
|
toast
|
||||||
]);
|
]);
|
||||||
@@ -227,22 +261,16 @@ const Chat = () => {
|
|||||||
const reEdit = useCallback(async () => {
|
const reEdit = useCallback(async () => {
|
||||||
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
|
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
|
||||||
// 删除数据库最后一句
|
// 删除数据库最后一句
|
||||||
delLastMessage(windowId);
|
await delLastMessage(windowId);
|
||||||
const val = chatList[chatList.length - 1].value;
|
const val = chatList[chatList.length - 1].value;
|
||||||
|
|
||||||
setInputVal(val);
|
resetInputVal(val);
|
||||||
|
|
||||||
setChatList(chatList.slice(0, -1));
|
setChatList(chatList.slice(0, -1));
|
||||||
|
}, [chatList, resetInputVal, windowId]);
|
||||||
setTimeout(() => {
|
|
||||||
if (TextareaDom.current) {
|
|
||||||
TextareaDom.current.style.height = val.split('\n').length * 22 + 'px';
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, [chatList, windowId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}>
|
<Flex height={'100%'} flexDirection={'column'}>
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<Flex
|
<Flex
|
||||||
px={4}
|
px={4}
|
||||||
@@ -253,14 +281,9 @@ const Chat = () => {
|
|||||||
zIndex={1}
|
zIndex={1}
|
||||||
>
|
>
|
||||||
<Box flex={1}>{chatSiteData?.name}</Box>
|
<Box flex={1}>{chatSiteData?.name}</Box>
|
||||||
{/* 重置按键 */}
|
|
||||||
<Box cursor={'pointer'} onClick={() => router.replace(`/chat?chatId=${chatId}`)}>
|
|
||||||
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
|
|
||||||
</Box>
|
|
||||||
{/* 滚动到底部按键 */}
|
{/* 滚动到底部按键 */}
|
||||||
{/* 滚动到底部 */}
|
|
||||||
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
|
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
|
||||||
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
|
<Box mr={10} cursor={'pointer'} onClick={scrollToBottom}>
|
||||||
<Icon
|
<Icon
|
||||||
name={'icon-xiangxiazhankai-xianxingyuankuang'}
|
name={'icon-xiangxiazhankai-xianxingyuankuang'}
|
||||||
width={25}
|
width={25}
|
||||||
@@ -269,6 +292,10 @@ const Chat = () => {
|
|||||||
></Icon>
|
></Icon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{/* 重置按键 */}
|
||||||
|
<Button size={'sm'} colorScheme={'gray'} onClick={resetChat}>
|
||||||
|
新对话
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* 聊天内容 */}
|
{/* 聊天内容 */}
|
||||||
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}>
|
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}>
|
||||||
@@ -276,44 +303,55 @@ const Chat = () => {
|
|||||||
<Box
|
<Box
|
||||||
key={index}
|
key={index}
|
||||||
py={media(9, 6)}
|
py={media(9, 6)}
|
||||||
px={media(4, 3)}
|
px={media(4, 2)}
|
||||||
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
|
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
|
||||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
||||||
>
|
>
|
||||||
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
|
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
|
||||||
<Box mr={4}>
|
<Box mr={media(4, 1)}>
|
||||||
<Image
|
<Image
|
||||||
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'}
|
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
|
||||||
alt="/imgs/modelAvatar.png"
|
alt="/icon/logo.png"
|
||||||
width={30}
|
width={media(30, 20)}
|
||||||
height={30}
|
height={media(30, 20)}
|
||||||
></Image>
|
|
||||||
</Box>
|
|
||||||
<Box flex={'1 0 0'} w={0} overflowX={'auto'}>
|
|
||||||
<Markdown
|
|
||||||
source={item.value}
|
|
||||||
isChatting={isChatting && index === chatList.length - 1}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box flex={'1 0 0'} w={0} overflow={'hidden'}>
|
||||||
|
{item.obj === 'AI' ? (
|
||||||
|
<Markdown
|
||||||
|
source={item.value}
|
||||||
|
isChatting={isChatting && index === chatList.length - 1}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
{/* 空内容提示 */}
|
||||||
|
{/* {
|
||||||
|
chatList.length === 0 && (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
内容太长
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
} */}
|
||||||
<Box
|
<Box
|
||||||
m={media('20px auto', '0 auto')}
|
m={media('20px auto', '0 auto')}
|
||||||
w={media('100vw', '100%')}
|
w={media('100vw', '100%')}
|
||||||
maxW={'800px'}
|
maxW={media('800px', 'auto')}
|
||||||
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
|
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
|
||||||
|
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
|
||||||
>
|
>
|
||||||
{lastWordHuman ? (
|
{lastWordHuman ? (
|
||||||
<Box textAlign={'center'}>
|
<Box textAlign={'center'}>
|
||||||
<Box color={'red'}>对话出现了异常</Box>
|
<Box color={'red'}>对话出现了异常</Box>
|
||||||
<Flex py={5} justifyContent={'center'}>
|
<Flex py={5} justifyContent={'center'}>
|
||||||
<Button
|
<Button mr={20} onClick={resetChat} colorScheme={'green'}>
|
||||||
mr={20}
|
|
||||||
onClick={() => router.replace(`/chat?chatId=${chatId}`)}
|
|
||||||
colorScheme={'green'}
|
|
||||||
>
|
|
||||||
重开对话
|
重开对话
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={reEdit}>重新编辑最后一句</Button>
|
<Button onClick={reEdit}>重新编辑最后一句</Button>
|
||||||
@@ -349,12 +387,12 @@ const Chat = () => {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const textarea = e.target;
|
const textarea = e.target;
|
||||||
setInputVal(textarea.value);
|
setInputVal(textarea.value);
|
||||||
|
textarea.style.height = textareaMinH;
|
||||||
textarea.style.height = textarea.value.split('\n').length * 22 + 'px';
|
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
// 触发快捷发送
|
// 触发快捷发送
|
||||||
if (e.keyCode === 13 && !e.shiftKey) {
|
if (isPc && e.keyCode === 13 && !e.shiftKey) {
|
||||||
sendPrompt();
|
sendPrompt();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -382,9 +420,17 @@ const Chat = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Loading loading={!chatSiteData} />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Chat;
|
export default Chat;
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: any) {
|
||||||
|
const chatId = context.query?.chatId || '';
|
||||||
|
const windowId = context.query?.windowId || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { chatId, windowId }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { Card } from '@chakra-ui/react';
|
||||||
import { Card, Text, Box, Heading, Flex } from '@chakra-ui/react';
|
|
||||||
import Markdown from '@/components/Markdown';
|
import Markdown from '@/components/Markdown';
|
||||||
import { introPage } from '@/constants/common';
|
import { introPage } from '@/constants/common';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card p={5} lineHeight={2}>
|
<Card p={5} lineHeight={2}>
|
||||||
<Markdown source={introPage} isChatting={false} />
|
<Markdown source={introPage} isChatting={false} />
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import React, { useState, Dispatch, useCallback } from 'react';
|
import React, { useState, Dispatch, useCallback } from 'react';
|
||||||
import {
|
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
|
||||||
FormControl,
|
|
||||||
Box,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
FormErrorMessage,
|
|
||||||
useToast,
|
|
||||||
Flex
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { PageTypeEnum } from '../../../constants/user';
|
import { PageTypeEnum } from '../../../constants/user';
|
||||||
import { postFindPassword } from '@/api/user';
|
import { postFindPassword } from '@/api/user';
|
||||||
import { useSendCode } from '@/hooks/useSendCode';
|
import { useSendCode } from '@/hooks/useSendCode';
|
||||||
import type { ResLogin } from '@/api/response/user';
|
import type { ResLogin } from '@/api/response/user';
|
||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||||
@@ -28,7 +21,7 @@ interface RegisterType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||||
const toast = useToast();
|
const { toast } = useToast();
|
||||||
const { mediaLgMd } = useScreen();
|
const { mediaLgMd } = useScreen();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -66,16 +59,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
|||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: `密码已找回`,
|
title: `密码已找回`,
|
||||||
status: 'success',
|
status: 'success'
|
||||||
position: 'top'
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: error.message || '修改密码异常',
|
||||||
|
status: 'error'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
typeof error === 'string' &&
|
|
||||||
toast({
|
|
||||||
title: error,
|
|
||||||
status: 'error',
|
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setRequesting(false);
|
setRequesting(false);
|
||||||
},
|
},
|
||||||
@@ -85,7 +75,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||||
找回 DocGPT 账号
|
找回 FastGPT 账号
|
||||||
</Box>
|
</Box>
|
||||||
<form onSubmit={handleSubmit(onclickFindPassword)}>
|
<form onSubmit={handleSubmit(onclickFindPassword)}>
|
||||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||||
|
|||||||
@@ -42,13 +42,11 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
|||||||
title: '登录成功',
|
title: '登录成功',
|
||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
typeof error === 'string' &&
|
toast({
|
||||||
toast({
|
title: error.message || '登录异常',
|
||||||
title: error,
|
status: 'error'
|
||||||
status: 'error',
|
});
|
||||||
position: 'top'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setRequesting(false);
|
setRequesting(false);
|
||||||
},
|
},
|
||||||
@@ -58,7 +56,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||||
登录 DocGPT
|
登录 FastGPT
|
||||||
</Box>
|
</Box>
|
||||||
<form onSubmit={handleSubmit(onclickLogin)}>
|
<form onSubmit={handleSubmit(onclickLogin)}>
|
||||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||||
|
|||||||
@@ -61,14 +61,11 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
|||||||
title: `注册成功`,
|
title: `注册成功`,
|
||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
typeof error === 'string' &&
|
toast({
|
||||||
toast({
|
title: error.message || '注册异常',
|
||||||
title: error,
|
status: 'error'
|
||||||
status: 'error',
|
});
|
||||||
duration: 4000,
|
|
||||||
isClosable: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setRequesting(false);
|
setRequesting(false);
|
||||||
},
|
},
|
||||||
@@ -78,7 +75,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||||
注册 DocGPT 账号
|
注册 FastGPT 账号
|
||||||
</Box>
|
</Box>
|
||||||
<form onSubmit={handleSubmit(onclickRegister)}>
|
<form onSubmit={handleSubmit(onclickRegister)}>
|
||||||
<FormControl mt={8} isInvalid={!!errors.email}>
|
<FormControl mt={8} isInvalid={!!errors.email}>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
.loginPage {
|
.loginPage {
|
||||||
background: url('/icon/login-bg.svg') no-repeat;
|
background: url('/icon/login-bg.svg') no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||||
import { PageTypeEnum } from '@/constants/user';
|
import { PageTypeEnum } from '@/constants/user';
|
||||||
import LoginForm from './components/LoginForm';
|
|
||||||
import RegisterForm from './components/RegisterForm';
|
|
||||||
import ForgetPasswordForm from './components/ForgetPasswordForm';
|
|
||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
import type { ResLogin } from '@/api/response/user';
|
import type { ResLogin } from '@/api/response/user';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
const LoginForm = dynamic(() => import('./components/LoginForm'));
|
||||||
|
const RegisterForm = dynamic(() => import('./components/RegisterForm'));
|
||||||
|
const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm'));
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isPc } = useScreen();
|
const { isPc } = useScreen();
|
||||||
@@ -19,28 +21,29 @@ const Login = () => {
|
|||||||
const loginSuccess = useCallback(
|
const loginSuccess = useCallback(
|
||||||
(res: ResLogin) => {
|
(res: ResLogin) => {
|
||||||
setUserInfo(res.user, res.token);
|
setUserInfo(res.user, res.token);
|
||||||
router.push('/');
|
router.push('/model/list');
|
||||||
},
|
},
|
||||||
[router, setUserInfo]
|
[router, setUserInfo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const map = {
|
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {
|
||||||
[PageTypeEnum.login]: {
|
const TypeMap = {
|
||||||
Component: <LoginForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
[PageTypeEnum.login]: LoginForm,
|
||||||
img: '/icon/loginLeft.svg'
|
[PageTypeEnum.register]: RegisterForm,
|
||||||
},
|
[PageTypeEnum.forgetPassword]: ForgetPasswordForm
|
||||||
[PageTypeEnum.register]: {
|
};
|
||||||
Component: <RegisterForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
|
||||||
img: '/icon/loginLeft.svg'
|
const Component = TypeMap[type];
|
||||||
},
|
|
||||||
[PageTypeEnum.forgetPassword]: {
|
return <Component setPageType={setPageType} loginSuccess={loginSuccess} />;
|
||||||
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
}
|
||||||
img: '/icon/loginLeft.svg'
|
|
||||||
}
|
useEffect(() => {
|
||||||
};
|
router.prefetch('/model/list');
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.loginPage} p={isPc ? '10vh 10vw' : 0}>
|
<Box className={styles.loginPage} h={'100%'} p={isPc ? '10vh 10vw' : 0}>
|
||||||
<Flex
|
<Flex
|
||||||
maxW={'1240px'}
|
maxW={'1240px'}
|
||||||
m={'auto'}
|
m={'auto'}
|
||||||
@@ -54,7 +57,7 @@ const Login = () => {
|
|||||||
>
|
>
|
||||||
{isPc && (
|
{isPc && (
|
||||||
<Image
|
<Image
|
||||||
src={map[pageType].img}
|
src={'/icon/loginLeft.svg'}
|
||||||
order={pageType === PageTypeEnum.login ? 0 : 2}
|
order={pageType === PageTypeEnum.login ? 0 : 2}
|
||||||
flex={'1 0 0'}
|
flex={'1 0 0'}
|
||||||
w="0"
|
w="0"
|
||||||
@@ -76,7 +79,7 @@ const Login = () => {
|
|||||||
px={10}
|
px={10}
|
||||||
borderRadius={isPc ? 'md' : 'none'}
|
borderRadius={isPc ? 'md' : 'none'}
|
||||||
>
|
>
|
||||||
{map[pageType].Component}
|
<DynamicComponent type={pageType} />
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ interface CreateFormType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateModel = ({
|
const CreateModel = ({
|
||||||
isOpen,
|
|
||||||
setCreateModelOpen,
|
setCreateModelOpen,
|
||||||
onSuccess
|
onSuccess
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
|
||||||
setCreateModelOpen: Dispatch<boolean>;
|
setCreateModelOpen: Dispatch<boolean>;
|
||||||
onSuccess: Dispatch<ModelType>;
|
onSuccess: Dispatch<ModelType>;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -72,7 +70,7 @@ const CreateModel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal isOpen={isOpen} onClose={() => setCreateModelOpen(false)}>
|
<Modal isOpen={true} onClose={() => setCreateModelOpen(false)}>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>创建模型</ModalHeader>
|
<ModalHeader>创建模型</ModalHeader>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Grid, Box, Card, Flex, Button, FormControl, Input, Textarea } from '@chakra-ui/react';
|
import { Grid, Box, Card, Flex, Button, FormControl, Input, Textarea } from '@chakra-ui/react';
|
||||||
import type { ModelType } from '@/types/model';
|
import type { ModelType } from '@/types/model';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -7,17 +7,17 @@ import { putModelById } from '@/api/model';
|
|||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
|
||||||
const ModelEditForm = ({ model }: { model: ModelType }) => {
|
const ModelEditForm = ({ model }: { model?: ModelType }) => {
|
||||||
|
const isInit = useRef(false);
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
reset,
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
} = useForm<ModelType>({
|
} = useForm<ModelType>();
|
||||||
defaultValues: model
|
|
||||||
});
|
|
||||||
const { setLoading } = useGlobalStore();
|
const { setLoading } = useGlobalStore();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { isPc } = useScreen();
|
const { media } = useScreen();
|
||||||
|
|
||||||
const onclickSave = useCallback(
|
const onclickSave = useCallback(
|
||||||
async (data: ModelType) => {
|
async (data: ModelType) => {
|
||||||
@@ -34,7 +34,7 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
toast({
|
toast({
|
||||||
title: err as string,
|
title: err as string,
|
||||||
status: 'success'
|
status: 'success'
|
||||||
@@ -61,8 +61,16 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
|
|||||||
});
|
});
|
||||||
}, [errors, toast]);
|
}, [errors, toast]);
|
||||||
|
|
||||||
|
/* model 只会改变一次 */
|
||||||
|
useEffect(() => {
|
||||||
|
if (model && !isInit.current) {
|
||||||
|
reset(model);
|
||||||
|
isInit.current = true;
|
||||||
|
}
|
||||||
|
}, [model, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
<Grid gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||||
<Card p={4}>
|
<Card p={4}>
|
||||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||||
@@ -83,7 +91,7 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
|
|||||||
<FormControl mt={5}>
|
<FormControl mt={5}>
|
||||||
<Flex alignItems={'center'}>
|
<Flex alignItems={'center'}>
|
||||||
<Box flex={'0 0 80px'}>对话模型:</Box>
|
<Box flex={'0 0 80px'}>对话模型:</Box>
|
||||||
<Box>{model.service.modelName}</Box>
|
<Box>{model?.service.modelName}</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl mt={5}>
|
<FormControl mt={5}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Box, Button, Flex, Heading, Tag } from '@chakra-ui/react';
|
import { Box, Button, Flex, Tag } from '@chakra-ui/react';
|
||||||
import type { ModelType } from '@/types/model';
|
import type { ModelType } from '@/types/model';
|
||||||
import { formatModelStatus } from '@/constants/model';
|
import { formatModelStatus } from '@/constants/model';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -14,6 +14,10 @@ const ModelPhoneList = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.prefetch('/chat');
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
|
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Table,
|
Table,
|
||||||
@@ -84,6 +84,10 @@ const ModelTable = ({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
router.prefetch('/chat');
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card py={3}>
|
<Card py={3}>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useCallback, useState } from 'react';
|
import React, { useEffect, useCallback, useState } from 'react';
|
||||||
import { Box, Card, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
|
import { Box, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
|
||||||
import { ModelType } from '@/types/model';
|
import { ModelType } from '@/types/model';
|
||||||
import { getModelTrainings } from '@/api/model';
|
import { getModelTrainings } from '@/api/model';
|
||||||
import type { TrainingItemType } from '@/types/training';
|
import type { TrainingItemType } from '@/types/training';
|
||||||
@@ -29,7 +29,7 @@ const Training = ({ model }: { model: ModelType }) => {
|
|||||||
const res = await getModelTrainings(id);
|
const res = await getModelTrainings(id);
|
||||||
setRecords(res);
|
setRecords(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log('error->', error);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const Training = ({ model }: { model: ModelType }) => {
|
|||||||
}, [loadTrainingRecords, model]);
|
}, [loadTrainingRecords, model]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card p={4} h={'100%'}>
|
<>
|
||||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||||
训练记录: {model.trainingTimes}次
|
训练记录: {model.trainingTimes}次
|
||||||
</Box>
|
</Box>
|
||||||
@@ -63,7 +63,7 @@ const Training = ({ model }: { model: ModelType }) => {
|
|||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import { useGlobalStore } from '@/store/global';
|
|||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
import ModelEditForm from './components/ModelEditForm';
|
import ModelEditForm from './components/ModelEditForm';
|
||||||
import Icon from '@/components/Icon';
|
import Icon from '@/components/Icon';
|
||||||
import Training from './components/Training';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const Training = dynamic(() => import('./components/Training'));
|
||||||
|
|
||||||
const ModelDetail = () => {
|
const ModelDetail = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isPc } = useScreen();
|
const { isPc, media } = useScreen();
|
||||||
const { setLoading } = useGlobalStore();
|
const { setLoading } = useGlobalStore();
|
||||||
const { openConfirm, ConfirmChild } = useConfirm({
|
const { openConfirm, ConfirmChild } = useConfirm({
|
||||||
content: '确认删除该模型?'
|
content: '确认删除该模型?'
|
||||||
@@ -39,17 +41,16 @@ const ModelDetail = () => {
|
|||||||
const res = await getModelById(modelId as string);
|
const res = await getModelById(modelId as string);
|
||||||
res.security.expiredTime /= 60 * 60 * 1000;
|
res.security.expiredTime /= 60 * 60 * 1000;
|
||||||
setModel(res);
|
setModel(res);
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [modelId, setLoading]);
|
}, [modelId, setLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadModel();
|
loadModel();
|
||||||
}, [loadModel, modelId]);
|
router.prefetch('/chat');
|
||||||
|
}, [loadModel, modelId, router]);
|
||||||
|
|
||||||
/* 点击删除 */
|
/* 点击删除 */
|
||||||
const handleDelModel = useCallback(async () => {
|
const handleDelModel = useCallback(async () => {
|
||||||
@@ -63,7 +64,7 @@ const ModelDetail = () => {
|
|||||||
});
|
});
|
||||||
router.replace('/model/list');
|
router.replace('/model/list');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [setLoading, model, router, toast]);
|
}, [setLoading, model, router, toast]);
|
||||||
@@ -77,7 +78,7 @@ const ModelDetail = () => {
|
|||||||
|
|
||||||
router.push(`/chat?chatId=${chatId}`);
|
router.push(`/chat?chatId=${chatId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [setLoading, model, router]);
|
}, [setLoading, model, router]);
|
||||||
@@ -105,7 +106,7 @@ const ModelDetail = () => {
|
|||||||
title: typeof err === 'string' ? err : '文件格式错误',
|
title: typeof err === 'string' ? err : '文件格式错误',
|
||||||
status: 'error'
|
status: 'error'
|
||||||
});
|
});
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
@@ -120,122 +121,126 @@ const ModelDetail = () => {
|
|||||||
try {
|
try {
|
||||||
await putModelTrainingStatus(model._id);
|
await putModelTrainingStatus(model._id);
|
||||||
loadModel();
|
loadModel();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.log(error);
|
console.log('error->', error);
|
||||||
|
toast({
|
||||||
|
title: error.message || '更新失败',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, [setLoading, loadModel, model]);
|
}, [model, setLoading, loadModel, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!model && (
|
{/* 头部 */}
|
||||||
<>
|
<Card px={6} py={3}>
|
||||||
{/* 头部 */}
|
{isPc ? (
|
||||||
<Card px={6} py={3}>
|
<Flex alignItems={'center'}>
|
||||||
{isPc ? (
|
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||||
<Flex alignItems={'center'}>
|
{model?.name || '模型'} 配置
|
||||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
</Box>
|
||||||
{model.name} 配置
|
{!!model && (
|
||||||
</Box>
|
<Tag
|
||||||
<Tag
|
ml={2}
|
||||||
ml={2}
|
variant="solid"
|
||||||
variant="solid"
|
colorScheme={formatModelStatus[model.status].colorTheme}
|
||||||
colorScheme={formatModelStatus[model.status].colorTheme}
|
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
|
||||||
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
|
onClick={handleClickUpdateStatus}
|
||||||
onClick={handleClickUpdateStatus}
|
>
|
||||||
>
|
{formatModelStatus[model.status].text}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
<Box flex={1} />
|
||||||
|
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||||
|
对话体验
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Flex alignItems={'center'}>
|
||||||
|
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||||
|
{model?.name || '模型'} 配置
|
||||||
|
</Box>
|
||||||
|
{!!model && (
|
||||||
|
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
|
||||||
{formatModelStatus[model.status].text}
|
{formatModelStatus[model.status].text}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Box flex={1} />
|
)}
|
||||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
</Flex>
|
||||||
对话体验
|
<Box mt={4} textAlign={'right'}>
|
||||||
</Button>
|
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||||
</Flex>
|
对话体验
|
||||||
) : (
|
</Button>
|
||||||
<>
|
</Box>
|
||||||
<Flex alignItems={'center'}>
|
</>
|
||||||
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
)}
|
||||||
{model.name} 配置
|
</Card>
|
||||||
</Box>
|
{/* 基本信息编辑 */}
|
||||||
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
|
<Box mt={5}>
|
||||||
{formatModelStatus[model.status].text}
|
<ModelEditForm model={model} />
|
||||||
</Tag>
|
</Box>
|
||||||
</Flex>
|
{/* 其他配置 */}
|
||||||
<Box mt={4} textAlign={'right'}>
|
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
<Card p={4}>{!!model && <Training model={model} />}</Card>
|
||||||
对话体验
|
<Card p={4}>
|
||||||
</Button>
|
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||||
</Box>
|
神奇操作
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
{/* 基本信息编辑 */}
|
|
||||||
<Box mt={5}>
|
|
||||||
<ModelEditForm model={model} />
|
|
||||||
</Box>
|
</Box>
|
||||||
{/* 其他配置 */}
|
<Flex mt={5} alignItems={'center'}>
|
||||||
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
<Box flex={'0 0 80px'}>模型微调:</Box>
|
||||||
<Training model={model} />
|
<Button
|
||||||
<Card h={'100%'} p={4}>
|
size={'sm'}
|
||||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
onClick={() => {
|
||||||
神奇操作
|
SelectFileDom.current?.click();
|
||||||
</Box>
|
}}
|
||||||
<Flex mt={5} alignItems={'center'}>
|
title={!canTrain ? '' : '模型不支持微调'}
|
||||||
<Box flex={'0 0 80px'}>模型微调:</Box>
|
isDisabled={!canTrain}
|
||||||
<Button
|
>
|
||||||
size={'sm'}
|
上传微调数据集
|
||||||
onClick={() => {
|
</Button>
|
||||||
SelectFileDom.current?.click();
|
<Flex
|
||||||
}}
|
as={'a'}
|
||||||
title={!canTrain ? '' : '模型不支持微调'}
|
href="/TrainingTemplate.jsonl"
|
||||||
isDisabled={!canTrain}
|
download
|
||||||
>
|
ml={5}
|
||||||
上传微调数据集
|
cursor={'pointer'}
|
||||||
</Button>
|
alignItems={'center'}
|
||||||
<Flex
|
color={'blue.500'}
|
||||||
as={'a'}
|
>
|
||||||
href="/TrainingTemplate.jsonl"
|
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
|
||||||
download
|
下载模板
|
||||||
ml={5}
|
</Flex>
|
||||||
cursor={'pointer'}
|
</Flex>
|
||||||
alignItems={'center'}
|
{/* 提示 */}
|
||||||
color={'blue.500'}
|
<Box mt={3} py={3} color={'blackAlpha.500'}>
|
||||||
>
|
<Box as={'li'} lineHeight={1.9}>
|
||||||
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
|
每行包括一个 prompt 和一个 completion
|
||||||
下载模板
|
</Box>
|
||||||
</Flex>
|
<Box as={'li'} lineHeight={1.9}>
|
||||||
</Flex>
|
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
|
||||||
{/* 提示 */}
|
内容不都是同一个标点结尾,可以加一个空格打断相同性,
|
||||||
<Box mt={3} py={3} color={'blackAlpha.500'}>
|
</Box>
|
||||||
<Box as={'li'} lineHeight={1.9}>
|
<Box as={'li'} lineHeight={1.9}>
|
||||||
每行包括一个 prompt 和一个 completion
|
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
|
||||||
</Box>
|
</Box>
|
||||||
<Box as={'li'} lineHeight={1.9}>
|
</Box>
|
||||||
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
|
<Flex mt={5} alignItems={'center'}>
|
||||||
内容不都是同一个标点结尾,可以加一个空格打断相同性,
|
<Box flex={'0 0 80px'}>删除模型:</Box>
|
||||||
</Box>
|
<Button
|
||||||
<Box as={'li'} lineHeight={1.9}>
|
colorScheme={'red'}
|
||||||
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
|
size={'sm'}
|
||||||
</Box>
|
onClick={() => {
|
||||||
</Box>
|
openConfirm(() => {
|
||||||
<Flex mt={5} alignItems={'center'}>
|
handleDelModel();
|
||||||
<Box flex={'0 0 80px'}>删除模型:</Box>
|
});
|
||||||
<Button
|
}}
|
||||||
colorScheme={'red'}
|
>
|
||||||
size={'sm'}
|
删除模型
|
||||||
onClick={() => {
|
</Button>
|
||||||
openConfirm(() => {
|
</Flex>
|
||||||
handleDelModel();
|
</Card>
|
||||||
});
|
</Grid>
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除模型
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||||
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,36 +1,34 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Box, Button, Flex, Card } from '@chakra-ui/react';
|
import { Box, Button, Flex, Card } from '@chakra-ui/react';
|
||||||
import { getMyModels } from '@/api/model';
|
import { getMyModels } from '@/api/model';
|
||||||
import { getChatSiteId } from '@/api/chat';
|
import { getChatSiteId } from '@/api/chat';
|
||||||
import { ModelType } from '@/types/model';
|
import { ModelType } from '@/types/model';
|
||||||
import CreateModel from './components/CreateModel';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import ModelTable from './components/ModelTable';
|
import ModelTable from './components/ModelTable';
|
||||||
import ModelPhoneList from './components/ModelPhoneList';
|
import ModelPhoneList from './components/ModelPhoneList';
|
||||||
import { useScreen } from '@/hooks/useScreen';
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
|
const CreateModel = dynamic(() => import('./components/CreateModel'));
|
||||||
|
|
||||||
const ModelList = () => {
|
const ModelList = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
const { isPc } = useScreen();
|
const { isPc } = useScreen();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [models, setModels] = useState<ModelType[]>([]);
|
const [models, setModels] = useState<ModelType[]>([]);
|
||||||
const [openCreateModel, setOpenCreateModel] = useState(false);
|
const [openCreateModel, setOpenCreateModel] = useState(false);
|
||||||
const { setLoading } = useGlobalStore();
|
const { Loading, setIsLoading } = useLoading();
|
||||||
|
|
||||||
/* 加载模型 */
|
/* 加载模型 */
|
||||||
const loadModels = useCallback(async () => {
|
const { isLoading } = useQuery(['loadModels'], () => getMyModels(), {
|
||||||
setLoading(true);
|
onSuccess(res) {
|
||||||
try {
|
if (!res) return;
|
||||||
const res = await getMyModels();
|
|
||||||
setModels(res);
|
setModels(res);
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
});
|
||||||
}, [setLoading]);
|
|
||||||
useEffect(() => {
|
|
||||||
loadModels();
|
|
||||||
}, [loadModels]);
|
|
||||||
|
|
||||||
/* 创建成功回调 */
|
/* 创建成功回调 */
|
||||||
const createModelSuccess = useCallback((data: ModelType) => {
|
const createModelSuccess = useCallback((data: ModelType) => {
|
||||||
@@ -40,19 +38,23 @@ const ModelList = () => {
|
|||||||
/* 点前往聊天预览页 */
|
/* 点前往聊天预览页 */
|
||||||
const handlePreviewChat = useCallback(
|
const handlePreviewChat = useCallback(
|
||||||
async (modelId: string) => {
|
async (modelId: string) => {
|
||||||
setLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const chatId = await getChatSiteId(modelId);
|
const chatId = await getChatSiteId(modelId);
|
||||||
|
|
||||||
router.push(`/chat?chatId=${chatId}`, undefined, {
|
router.push(`/chat?chatId=${chatId}`, undefined, {
|
||||||
shallow: true
|
shallow: true
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
|
toast({
|
||||||
|
title: err.message || '出现一些异常',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
[router, setLoading]
|
[router, setIsLoading, toast]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,11 +80,11 @@ const ModelList = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{/* 创建弹窗 */}
|
{/* 创建弹窗 */}
|
||||||
<CreateModel
|
{openCreateModel && (
|
||||||
isOpen={openCreateModel}
|
<CreateModel setCreateModelOpen={setOpenCreateModel} onSuccess={createModelSuccess} />
|
||||||
setCreateModelOpen={setOpenCreateModel}
|
)}
|
||||||
onSuccess={createModelSuccess}
|
|
||||||
/>
|
<Loading loading={isLoading} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
Select,
|
Select,
|
||||||
Input
|
Input,
|
||||||
|
IconButton
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { DeleteIcon } from '@chakra-ui/icons';
|
||||||
import { useForm, useFieldArray } from 'react-hook-form';
|
import { useForm, useFieldArray } from 'react-hook-form';
|
||||||
import { UserUpdateParams } from '@/types/user';
|
import { UserUpdateParams } from '@/types/user';
|
||||||
import { putUserInfo } from '@/api/user';
|
import { putUserInfo } from '@/api/user';
|
||||||
@@ -130,7 +132,15 @@ const NumberSetting = () => {
|
|||||||
></Input>
|
></Input>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Button onClick={() => removeAccount(i)}>删除</Button>
|
<IconButton
|
||||||
|
aria-label="删除账号"
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
colorScheme={'red'}
|
||||||
|
onClick={() => {
|
||||||
|
removeAccount(i);
|
||||||
|
handleSubmit(onclickSave)();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
export const openaiError: Record<string, string> = {
|
export const openaiError: Record<string, string> = {
|
||||||
context_length_exceeded: '内容超出长度'
|
context_length_exceeded: '内容超长了,请重置对话',
|
||||||
|
Unauthorized: 'API-KEY 不合法',
|
||||||
|
rate_limit_reached: '同时访问用户过多,请稍后再试',
|
||||||
|
'Bad Request': '上下文太多了,请重开对话~'
|
||||||
|
};
|
||||||
|
export const proxyError: Record<string, boolean> = {
|
||||||
|
ECONNABORTED: true,
|
||||||
|
ECONNRESET: true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import type { Mongoose } from 'mongoose';
|
|
||||||
|
|
||||||
let cachedClient: Mongoose;
|
/**
|
||||||
|
* 连接 MongoDB 数据库
|
||||||
export async function connectToDatabase() {
|
*/
|
||||||
if (cachedClient && cachedClient.connection.readyState === 1) {
|
export async function connectToDatabase(): Promise<void> {
|
||||||
return cachedClient;
|
if (global.mongodb) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedClient = await mongoose.connect(process.env.MONGODB_UR as string, {
|
global.mongodb = 'connecting';
|
||||||
dbName: 'doc_gpt'
|
console.log('connect mongo');
|
||||||
});
|
try {
|
||||||
|
mongoose.set('strictQuery', true);
|
||||||
return cachedClient;
|
global.mongodb = await mongoose.connect(process.env.MONGODB_URI as string, {
|
||||||
|
bufferCommands: true,
|
||||||
|
dbName: 'doc_gpt',
|
||||||
|
maxPoolSize: 5,
|
||||||
|
minPoolSize: 1,
|
||||||
|
maxConnecting: 5
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error->', 'mongo connect error');
|
||||||
|
global.mongodb = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './models/authCode';
|
export * from './models/authCode';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { openaiError } from './errorCode';
|
import { openaiError, proxyError } from './errorCode';
|
||||||
|
|
||||||
export interface ResponseType<T = any> {
|
export interface ResponseType<T = any> {
|
||||||
code: number;
|
code: number;
|
||||||
@@ -20,12 +20,16 @@ export const jsonRes = (
|
|||||||
|
|
||||||
let msg = message;
|
let msg = message;
|
||||||
if ((code < 200 || code >= 400) && !message) {
|
if ((code < 200 || code >= 400) && !message) {
|
||||||
msg =
|
msg = error?.message || '请求错误';
|
||||||
typeof error === 'string'
|
if (typeof error === 'string') {
|
||||||
? error
|
msg = error;
|
||||||
: openaiError[error?.response?.data?.message] || error?.message || '请求错误';
|
} else if (proxyError[error?.code]) {
|
||||||
|
msg = '服务器代理出错';
|
||||||
|
} else if (openaiError[error?.response?.statusText]) {
|
||||||
|
msg = openaiError[error.response.statusText];
|
||||||
|
}
|
||||||
|
|
||||||
console.log(msg);
|
console.log('error->', error.code, error?.response?.statusText, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const authChat = async (chatId: string) => {
|
|||||||
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
|
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
|
||||||
|
|
||||||
if (!userApiKey) {
|
if (!userApiKey) {
|
||||||
return Promise.reject('该用户缺少ApiKey, 无法请求');
|
return Promise.reject('缺少ApiKey, 无法请求');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,26 +15,26 @@ let mailTransport = nodemailer.createTransport({
|
|||||||
|
|
||||||
const emailMap: { [key: string]: any } = {
|
const emailMap: { [key: string]: any } = {
|
||||||
[EmailTypeEnum.register]: {
|
[EmailTypeEnum.register]: {
|
||||||
subject: '注册 DocGPT 账号',
|
subject: '注册 FastGPT 账号',
|
||||||
html: (code: string) => `<div>您正在注册 DocGPT 账号,验证码为:${code}</div>`
|
html: (code: string) => `<div>您正在注册 FastGPT 账号,验证码为:${code}</div>`
|
||||||
},
|
},
|
||||||
[EmailTypeEnum.findPassword]: {
|
[EmailTypeEnum.findPassword]: {
|
||||||
subject: '修改 DocGPT 密码',
|
subject: '修改 FastGPT 密码',
|
||||||
html: (code: string) => `<div>您正在修改 DocGPT 账号密码,验证码为:${code}</div>`
|
html: (code: string) => `<div>您正在修改 FastGPT 账号密码,验证码为:${code}</div>`
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) => {
|
export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
from: `"DocGPT" ${myEmail}`,
|
from: `"FastGPT" ${myEmail}`,
|
||||||
to: email,
|
to: email,
|
||||||
subject: emailMap[type]?.subject,
|
subject: emailMap[type]?.subject,
|
||||||
html: emailMap[type]?.html(code)
|
html: emailMap[type]?.html(code)
|
||||||
};
|
};
|
||||||
mailTransport.sendMail(options, function (err, msg) {
|
mailTransport.sendMail(options, function (err, msg) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
reject('邮箱异常');
|
reject('邮箱异常');
|
||||||
} else {
|
} else {
|
||||||
resolve('');
|
resolve('');
|
||||||
@@ -46,14 +46,14 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`)
|
|||||||
export const sendTrainSucceed = (email: string, modelName: string) => {
|
export const sendTrainSucceed = (email: string, modelName: string) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
from: `"DocGPT" ${myEmail}`,
|
from: `"FastGPT" ${myEmail}`,
|
||||||
to: email,
|
to: email,
|
||||||
subject: '模型训练完成通知',
|
subject: '模型训练完成通知',
|
||||||
html: `你的模型 ${modelName} 已于 ${dayjs().format('YYYY-MM-DD HH:mm')} 训练完成!`
|
html: `你的模型 ${modelName} 已于 ${dayjs().format('YYYY-MM-DD HH:mm')} 训练完成!`
|
||||||
};
|
};
|
||||||
mailTransport.sendMail(options, function (err, msg) {
|
mailTransport.sendMail(options, function (err, msg) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
console.log('error->', err);
|
||||||
reject('邮箱异常');
|
reject('邮箱异常');
|
||||||
} else {
|
} else {
|
||||||
resolve('');
|
resolve('');
|
||||||
|
|||||||
@@ -49,15 +49,12 @@ export const getUserOpenaiKey = async (userId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* 代理 */
|
/* 代理 */
|
||||||
export const openaiProxy: any =
|
export const httpsAgent =
|
||||||
process.env.AXIOS_PROXY_PORT && process.env.AXIOS_PROXY_HOST
|
process.env.AXIOS_PROXY_HOST && process.env.AXIOS_PROXY_PORT
|
||||||
? {
|
? tunnel.httpsOverHttp({
|
||||||
httpsAgent: tunnel.httpsOverHttp({
|
proxy: {
|
||||||
proxy: {
|
host: process.env.AXIOS_PROXY_HOST,
|
||||||
host: process.env.AXIOS_PROXY_HOST,
|
port: +process.env.AXIOS_PROXY_PORT
|
||||||
port: +process.env.AXIOS_PROXY_PORT
|
}
|
||||||
}
|
})
|
||||||
}),
|
|
||||||
proxy: false
|
|
||||||
}
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -24,63 +24,9 @@ td,
|
|||||||
svg {
|
svg {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
body,
|
|
||||||
button,
|
#__next {
|
||||||
input,
|
height: 100%;
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
font: 12px/1.5tahoma, arial, \5b8b\4f53;
|
|
||||||
}
|
|
||||||
// h1, h2, h3, h4, h5, h6{ font-size:100%; }
|
|
||||||
address,
|
|
||||||
cite,
|
|
||||||
dfn,
|
|
||||||
em,
|
|
||||||
var {
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
code,
|
|
||||||
kbd,
|
|
||||||
pre,
|
|
||||||
samp {
|
|
||||||
font-family: couriernew, courier, monospace;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
sup {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
sub {
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
legend {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
fieldset,
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
button,
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar,
|
::-webkit-scrollbar,
|
||||||
@@ -104,6 +50,9 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
::-webkit-scrollbar,
|
::-webkit-scrollbar,
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
|||||||
6
src/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Mongoose } from 'mongoose';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var mongodb: Mongoose | string | null;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
@@ -8,19 +8,25 @@ export const useCopyData = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
return {
|
return {
|
||||||
copyData: (data: string, title: string = '复制成功') => {
|
copyData: (data: string, title: string = '复制成功') => {
|
||||||
const clipboardObj = navigator.clipboard;
|
try {
|
||||||
clipboardObj
|
const textarea = document.createElement('textarea');
|
||||||
.writeText(data)
|
textarea.value = data;
|
||||||
.then(() => {
|
document.body.appendChild(textarea);
|
||||||
toast({
|
textarea.select();
|
||||||
title,
|
document.execCommand('copy');
|
||||||
status: 'success',
|
document.body.removeChild(textarea);
|
||||||
duration: 1000
|
toast({
|
||||||
});
|
title,
|
||||||
})
|
status: 'success',
|
||||||
.catch((err) => {
|
duration: 1000
|
||||||
console.log(err);
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('error->', error);
|
||||||
|
toast({
|
||||||
|
title: '复制失败',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const tokenKey = 'doc-gpt-token';
|
const tokenKey = 'fast-gpt-token';
|
||||||
|
|
||||||
export const setToken = (val: string) => {
|
export const setToken = (val: string) => {
|
||||||
localStorage.setItem(tokenKey, val);
|
localStorage.setItem(tokenKey, val);
|
||||||
|
|||||||
@@ -19,6 +19,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||