Compare commits

...

12 Commits
v0.9 ... v1.1

Author SHA1 Message Date
Archer
17cbfa05d3 perf: 表格样式。代理配置 2023-03-12 00:06:12 +08:00
Archer
fd8135f50c feat: md数学表达式;perf: 字体样式;fix: 发送验证码错误提醒。聊天二次加载 2023-03-11 16:56:27 +08:00
Archer
9f96593136 README.md 2023-03-11 13:19:00 +08:00
Archer
ed9e72ec9a perf: logs 2023-03-10 22:12:13 +08:00
Archer
7fb76cde0b feat: 类型声明 2023-03-10 20:18:31 +08:00
Archer
38d49ea05f feat: 错误提示 2023-03-10 19:44:06 +08:00
Archer
453f3be8ce fix: mongo内存泄漏 2023-03-10 18:54:51 +08:00
Archer
65da4653bc feat: stream流响应 2023-03-10 02:57:13 +08:00
Archer
26888e855b rename 2023-03-09 21:37:56 +08:00
Archer
e132c622a6 feat: 文案内容 2023-03-09 21:23:58 +08:00
Archer
17364e9da3 conflict
perf: 聊天页优化

perf: md解析样式

perf: ui调整

perf: 懒加载和动态加载优化

perf: 去除console,

perf: 图片cdn

feat: 图片地址

perf: 登录顺序

feat: 流优化
2023-03-09 20:44:54 +08:00
archer
2390823282 feat: 限流配置 2023-03-04 13:30:20 +08:00
64 changed files with 1621 additions and 1019 deletions

View File

@@ -1,6 +1,6 @@
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT=33210
MONGODB_UR=
MONGODB_URI=
MY_MAIL=
MAILE_CODE=
TOKEN_KEY=

5
.gitignore vendored
View File

@@ -34,5 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
public/trainData/
.vscode/
/public/trainData/
/.vscode/
platform.json

View File

@@ -1,5 +0,0 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true ,
"prettier.tabWidth": 2
}

View File

@@ -55,5 +55,12 @@ USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV MAX_USER ''
ENV AXIOS_PROXY_HOST ''
ENV AXIOS_PROXY_PORT ''
ENV MONGODB_URI ''
ENV MY_MAIL ''
ENV MAILE_CODE ''
ENV TOKEN_KEY ''
CMD ["node", "server.js"]

View File

@@ -1,4 +1,6 @@
# Doc GPT
# Fast GPT
Fast GPT 允许你是用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
## 初始化
复制 .env.template 成 .env.local ,填写核心参数
@@ -6,7 +8,7 @@
```
AXIOS_PROXY_HOST=axios代理地址目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
AXIOS_PROXY_PORT=代理端口
MONGODB_UR=mongo数据库地址
MONGODB_URI=mongo数据库地址
MY_MAIL=发送验证码邮箱
MAILE_CODE=邮箱秘钥
TOKEN_KEY=随便填一个用于生成和校验token
@@ -15,26 +17,66 @@ TOKEN_KEY=随便填一个用于生成和校验token
```bash
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 部署
```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
docker build -t imageName:tag .
docker push imageName:tag
```
服务器请准备好 docker mongonginx和代理。 镜像走本机的代理,所以用 hostport改成代理的端口clash一般都是7890。
```bash
# 服务器拉取部署, imageName 替换成镜像名
docker pull imageName:tag
# 获取本地旧镜像ID
OLD_IMAGE_ID=$(docker images imageName -f "dangling=true" -q)
docker stop doc-gpt || true
docker rm doc-gpt || true
docker run -d --network=host --name doc-gpt \
-e MAX_USER=50 \
-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 logs doc-gpt
# 删除本地旧镜像
if [ ! -z "$OLD_IMAGE_ID" ]; then
docker rmi $OLD_IMAGE_ID
fi
```
### docker 安装
```bash
# 安装docker
curl -sSL https://get.daocloud.io/docker | sh
sudo systemctl start docker
```
### 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
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
# 介绍页
## 欢迎使用 Doc GPT
## 欢迎使用 Fast GPT
时间比较赶,介绍没来得及完善,先直接上怎么使用:
@@ -70,4 +112,4 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
### 其他问题
还有其他问题,可以加我 wx拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
![](/icon/erweima.jpg)

View File

@@ -19,11 +19,11 @@
"@next/font": "13.1.6",
"@reduxjs/toolkit": "^1.9.3",
"@tanstack/react-query": "^4.24.10",
"@types/nprogress": "^0.2.0",
"axios": "^1.3.3",
"crypto": "^1.0.1",
"dayjs": "^1.11.7",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"eventsource-parser": "^0.1.0",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"hyperdown": "^2.4.29",
@@ -32,17 +32,19 @@
"mongoose": "^6.10.0",
"next": "13.1.6",
"nodemailer": "^6.9.1",
"nprogress": "^0.2.0",
"openai": "^3.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.58.3",
"sharp": "^0.31.3",
"tunnel": "^0.0.6",
"typescript": "4.9.5",
"uuid": "^9.0.0",
"zustand": "^4.3.5"
},
@@ -56,9 +58,12 @@
"@types/react-syntax-highlighter": "^15.5.6",
"@types/tunnel": "^0.0.3",
"@types/uuid": "^9.0.1",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"prettier": "^2.8.4"
"prettier": "^2.8.4",
"typescript": "4.9.5"
},
"lint-staged": {
"./src/**/*.{ts,tsx,scss}": "npm run format"

657
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

BIN
public/icon/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,5 +1,6 @@
import { GET, POST, DELETE } from './request';
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
import axios from 'axios';
/**
* 获取一个聊天框的ID
@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
});
/* 获取 Chat 的 Event 对象,进行持续通信 */
export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}`);
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}&date=${Date.now()}`);
/**
* 删除最后一句

View File

@@ -34,7 +34,7 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) {
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
console.log(data, 'data is empty');
console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data.message);
@@ -46,24 +46,23 @@ function checkRes(data: ResponseDataType) {
* 响应错误
*/
function responseError(err: any) {
console.error('请求错误', err);
console.log('error->', '请求错误', err);
if (!err) {
return Promise.reject('未知错误');
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject(err);
return Promise.reject({ message: err });
}
if (err.response) {
// 有报错响应
const res = err.response;
/* token过期,判断请求token与本地是否相同若不同需要重发 */
if (res.data.code in TOKEN_ERROR_CODE) {
clearToken();
return Promise.reject('token过期重新登录');
return Promise.reject({ message: 'token过期重新登录' });
}
}
return Promise.reject('未知错误');
return Promise.reject(err);
}
/* 创建请求实例 */

View File

@@ -7,6 +7,7 @@ import { useGlobalStore } from '@/store/global';
import { useQuery } from '@tanstack/react-query';
const unAuthPage: { [key: string]: boolean } = {
'/': true,
'/login': true,
'/chat': true
};
@@ -24,10 +25,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
useQuery(
[router.pathname, userInfo],
() => {
setLoading(true);
if (unAuthPage[router.pathname] === true || userInfo) {
return setLoading(false);
} else {
setLoading(true);
return getTokenLogin();
}
},
@@ -38,7 +39,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
}
},
onError(error) {
console.log(error);
console.log('error->', error);
router.push('/login');
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;

View File

@@ -43,18 +43,16 @@ const navbarList = [
const Layout = ({ children }: { children: JSX.Element }) => {
const { isPc } = useScreen();
const router = useRouter();
const { Loading } = useLoading({
defaultLoading: true
});
const { Loading } = useLoading({ defaultLoading: true });
const { loading } = useGlobalStore();
return (
<>
{!unShowLayoutRoute[router.pathname] ? (
<Box minHeight={'100vh'} backgroundColor={'gray.100'}>
<Box h={'100%'} backgroundColor={'gray.100'} overflow={'auto'}>
{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} />
</Box>
<Box ml={'80px'} p={7}>

View File

@@ -3,7 +3,6 @@ import { Box, Flex } from '@chakra-ui/react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import Icon from '../Icon';
import styles from './style.module.scss';
export enum NavbarTypeEnum {
normal = 'normal',
@@ -35,7 +34,7 @@ const Navbar = ({
>
{/* logo */}
<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 flex={1}>
@@ -47,6 +46,7 @@ const Navbar = ({
alignItems={'center'}
justifyContent={'center'}
onClick={() =>
!item.activeLink.includes(router.pathname) &&
router.push(item.link, undefined, {
shallow: true
})

View File

@@ -45,15 +45,15 @@ const NavbarPhone = ({
</Flex>
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
<DrawerOverlay />
<DrawerContent maxWidth={'60vw'}>
<DrawerContent maxWidth={'50vw'}>
<DrawerBody p={4}>
<Box pb={4}>
<Image src={'/logo.svg'} w={'100%'} h={'70px'} pt={2} alt=""></Image>
<Box py={4}>
<Image src={'/icon/logo.png'} margin={'auto'} w={'35'} h={'35'} alt=""></Image>
</Box>
{navbarList.map((item) => (
<Flex
key={item.label}
mb={4}
mb={5}
alignItems={'center'}
justifyContent={'center'}
onClick={() => {
@@ -61,8 +61,7 @@ const NavbarPhone = ({
onClose();
}}
cursor={'pointer'}
fontSize={'sm'}
h={'65px'}
h={'60px'}
borderRadius={'md'}
{...(item.activeLink.includes(router.pathname)
? {

View File

@@ -2,9 +2,7 @@ import React from 'react';
export const codeLight: { [key: string]: React.CSSProperties } = {
'code[class*=language-]': {
color: '#d4d4d4',
fontSize: '13px',
textShadow: 'none',
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
direction: 'ltr',
textAlign: 'left',
whiteSpace: 'pre',
@@ -21,9 +19,7 @@ export const codeLight: { [key: string]: React.CSSProperties } = {
},
'pre[class*=language-]': {
color: '#d4d4d4',
fontSize: '13px',
textShadow: 'none',
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
direction: 'ltr',
textAlign: 'left',
whiteSpace: 'pre',

View File

@@ -8,7 +8,7 @@
animation: blink 0.6s infinite;
}
.animation {
:last-child::after {
> :last-child::after {
display: inline-block;
content: '';
width: 4px;
@@ -27,96 +27,398 @@
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 {
/* 标题样式 */
h1 {
font-size: 1.8rem;
}
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%;
}
text-align: justify;
word-break: break-all;
overflow-y: hidden;
tab-size: 4;
word-spacing: normal;
pre {
padding: 10px 15px;
display: block;
width: 100%;
padding: 15px;
margin: 0;
border: none;
border-radius: 0;
background-color: #222 !important;
overflow-x: auto;
}
pre code {
display: block;
border: none;
background-color: #222;
background-color: #222 !important;
color: #fff;
width: 100%;
}
p {
line-height: 1.7;
a {
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;
}
}
}
}
}

View File

@@ -1,33 +1,44 @@
import React, { useMemo, memo } from 'react';
import React, { memo, useMemo } from 'react';
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 { codeLight } from './codeLight';
import { Box, Flex } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';
import styles from './index.module.scss';
import { codeLight } from './codeLight';
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();
return (
<ReactMarkdown
className={`${styles.markdown} ${
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
}`}
rehypePlugins={[remarkGfm]}
skipHtml={true}
remarkPlugins={[remarkMath]}
rehypePlugins={[remarkGfm, rehypeKatex]}
components={{
p: 'div',
pre: 'div',
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const code = String(children).replace(/\n$/, '');
const code = String(children);
return (
<Box my={3} borderRadius={'md'} overflow={'hidden'}>
<Flex py={2} px={5} backgroundColor={'#323641'} color={'#fff'} fontSize={'sm'}>
return !inline || match ? (
<Box my={3} borderRadius={'md'} overflow={'hidden'} backgroundColor={'#222'}>
<Flex
py={2}
px={5}
backgroundColor={'#323641'}
color={'#fff'}
fontSize={'sm'}
userSelect={'none'}
>
<Box flex={1}>{match?.[1]}</Box>
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
@@ -36,18 +47,23 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
</Flex>
<SyntaxHighlighter
style={codeLight as any}
showLineNumbers
language={match?.[1]}
PreTag="pre"
{...props}
>
{code}
</SyntaxHighlighter>
</Box>
) : (
<code className={className} {...props}>
{code}
</code>
);
}
}}
linkTarget="_blank"
>
{source}
{formatSource}
</ReactMarkdown>
);
};

View File

@@ -4,10 +4,11 @@ export enum EmailTypeEnum {
}
export const introPage = `
## 欢迎使用 Doc GPT
## 欢迎使用 Fast GPT
时间比较赶,介绍没来得及完善,先直接上怎么使用:
[Git 仓库](https://github.com/c121914yu/FastGPT)
时间比较赶,介绍没来得及完善,先直接上怎么使用:
1. 使用邮箱注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
@@ -39,6 +40,5 @@ export const introPage = `
* 分享链接应为http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
### 其他问题
还有其他问题,可以加我 wx拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。
`;

View File

@@ -20,24 +20,27 @@ const Button = defineStyleConfig({
baseStyle: {},
sizes: {
sm: {
fontSize: 'sm',
fontSize: 'xs',
px: 3,
py: 0,
fontWeight: 'normal',
height: '26px'
height: '26px',
lineHeight: '26px'
},
md: {
fontSize: 'md',
fontSize: 'sm',
px: 6,
py: 0,
height: '34px',
lineHeight: '34px',
fontWeight: 'normal'
},
lg: {
fontSize: 'lg',
fontSize: 'md',
px: 8,
py: 0,
height: '42px',
lineHeight: '42px',
fontWeight: 'normal'
}
},
@@ -58,13 +61,12 @@ export const theme = extendTheme({
global: {
'html, body': {
color: 'blackAlpha.800',
fontSize: '14px'
height: '100%',
maxHeight: '100vh',
overflowY: 'hidden'
}
}
},
fonts: {
body: 'system-ui, sans-serif'
},
fontSizes: {
xs: '0.8rem',
sm: '0.9rem',

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from 'react';
import { useCallback, useRef } from 'react';
import {
AlertDialog,
AlertDialogBody,
@@ -17,45 +17,51 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
const cancelCb = useRef<any>();
return {
openConfirm: (confirm?: any, cancel?: any) => {
onOpen();
confirmCb.current = confirm;
cancelCb.current = cancel;
},
ConfirmChild: () => (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
openConfirm: useCallback(
(confirm?: any, cancel?: any) => {
onOpen();
confirmCb.current = confirm;
cancelCb.current = cancel;
},
[onOpen]
),
ConfirmChild: useCallback(
() => (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogFooter>
<Button
colorScheme={'gray'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
</Button>
<Button
colorScheme="blue"
ml={3}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
<AlertDialogFooter>
<Button
colorScheme={'gray'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
</Button>
<Button
colorScheme="blue"
ml={4}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
),
[content, isOpen, onClose, title]
)
};
};

View File

@@ -1,32 +1,29 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import { Spinner, Flex } from '@chakra-ui/react';
export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = ({
loading,
fixed = true
}: {
loading?: boolean;
fixed?: boolean;
}): JSX.Element | null => {
return isLoading || loading ? (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
</Flex>
) : null;
};
const Loading = useCallback(
({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
return isLoading || loading ? (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
</Flex>
) : null;
},
[isLoading]
);
return {
isLoading,

View File

@@ -11,6 +11,6 @@ export function useScreen() {
isPc,
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [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)
};
}

View File

@@ -1,14 +1,11 @@
import { useState, useMemo, useCallback } from 'react';
import { sendCodeToEmail } from '@/api/user';
import { EmailTypeEnum } from '@/constants/common';
import { useToast } from '@chakra-ui/react';
let timer: any;
import { useToast } from './useToast';
export const useSendCode = () => {
const toast = useToast({
position: 'top',
duration: 2000
});
const { toast } = useToast();
const [codeSending, setCodeSending] = useState(false);
const [codeCountDown, setCodeCountDown] = useState(0);
const sendCodeText = useMemo(() => {
@@ -43,13 +40,11 @@ export const useSendCode = () => {
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
} catch (error: any) {
toast({
title: error.message || '发送验证码异常',
status: 'error'
});
}
setCodeSending(false);
},

View File

@@ -1,35 +1,44 @@
import type { AppProps, NextWebVitalsMetric } from 'next/app';
import Script from 'next/script';
import Head from 'next/head';
import { ChakraProvider } from '@chakra-ui/react';
import Layout from '@/components/Layout';
import { theme } from '@/constants/theme';
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';
export default function App({ Component, pageProps }: AppProps) {
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
}
});
//Binding events.
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
}
});
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>Doc GPT</title>
<meta name="description" content="Generated by Doc GPT" />
<title>Fast GPT</title>
<meta name="description" content="Generated by Fast GPT" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
/>
<link rel="icon" href="/favicon.ico" />
<script src="/iconfont.js" async></script>
</Head>
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<Layout>

View File

@@ -1,20 +1,24 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
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 { getOpenAIApi, authChat } from '@/service/utils/chat';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat';
import { openaiError } from '@/service/errorCode';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Encoding': 'none',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.on('close', () => {
res.end();
});
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
try {
@@ -50,9 +54,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
content: item.value
})
);
// 第一句话,强调代码类型
formatPrompts.unshift({
role: ChatCompletionRequestMessageRoleEnum.System,
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
});
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey);
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
@@ -61,50 +70,74 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
messages: formatPrompts,
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;
// @ts-ignore
const match = chatResponse.data.match(reg);
let AIResponse = '';
if (match) {
match.forEach((item: string, i: number) => {
// 解析数据
const decoder = new TextDecoder();
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
if (event.type === 'event') {
const data = event.data;
if (data === '[DONE]') {
// 存入库
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
res.write('event: done\ndata: \n\n');
return;
}
try {
const json = JSON.parse(item);
// 开头的换行忽略
if (i === 0 && json.content?.startsWith('\n')) return;
AIResponse += json.content;
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
content && res.write(`data: ${content}\n\n`);
} catch (err) {
err;
const json = JSON.parse(data);
const content: string = json?.choices?.[0].delta.content || '\n';
// console.log('content:', content)
res.write(`event: responseData\ndata: ${content.replace(/\n/g, '<br/>')}\n\n`);
AIResponse += content;
} catch (error) {
error;
}
});
}
};
try {
for await (const chunk of chatResponse.data as any) {
const parser = createParser(onParse);
parser.feed(decoder.decode(chunk));
}
} catch (error) {
console.log(error, '====');
throw new Error('错误了');
}
res.write(`data: [DONE]\n\n`);
// 存入库
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
res.end();
} catch (err: any) {
console.log(err?.response?.data || err);
console.log('error->', err?.response, '===');
let errorText = 'OpenAI 服务器访问超时';
if (err.code === 'ECONNRESET' || err?.response?.status === 502) {
errorText = '服务器代理出错';
} else if (err?.response?.statusText && openaiError[err.response.statusText]) {
errorText = openaiError[err.response.statusText];
}
console.log('error->', errorText);
res.write(`event: serviceError\ndata: ${errorText}\n\n`);
// 删除最一条数据库记录, 也就是预发送的那一条
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
});
res.end();
}
}

View File

@@ -5,7 +5,7 @@ import { connectToDatabase, Chat } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
/* 发送提示词 */
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,
stop: ['###']
},
openaiProxy
{
httpsAgent
}
);
const responseMessage = response.data.choices[0]?.text;

View File

@@ -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('聊天框已过期');
}
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err

View File

@@ -5,7 +5,7 @@ import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { TrainingStatusEnum } from '@/constants/model';
import { getOpenAIApi } from '@/service/utils/chat';
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>) {
@@ -47,12 +47,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
if (training) {
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 });
}
// 删除对应训练记录

View File

@@ -9,7 +9,7 @@ import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {

View File

@@ -7,7 +7,7 @@ import type { ModelType } from '@/types/model';
import { TrainingItemType } from '@/types/training';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
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) {
@@ -46,11 +46,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
// 获取 openai 的训练情况
const { data } = await openai.retrieveFineTune(training.tuneId, openaiProxy);
const { data } = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
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, {
@@ -74,7 +74,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
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, {

View File

@@ -10,7 +10,7 @@ import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { openaiProxy } from '@/service/utils/tools';
import { httpsAgent } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {
@@ -73,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// @ts-ignore
fs.createReadStream(file.filepath),
'fine-tune',
openaiProxy
{ httpsAgent }
);
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
@@ -84,7 +84,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: trainingType,
suffix: model.name
},
openaiProxy
{ httpsAgent }
);
trainId = trainRes.data.id; // 记录训练 ID
@@ -114,9 +114,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// @ts-ignore
if (openai) {
// @ts-ignore
uploadFileId && openai.deleteFile(uploadFileId, openaiProxy);
uploadFileId && openai.deleteFile(uploadFileId, { httpsAgent });
// @ts-ignore
trainId && openai.cancelFineTune(trainId, openaiProxy);
trainId && openai.cancelFineTune(trainId, { httpsAgent });
}
jsonRes(res, {

View File

@@ -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);
}

View File

@@ -8,7 +8,7 @@ import { getOpenAIApi } from '@/service/utils/chat';
import { getUserOpenaiKey } from '@/service/utils/tools';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
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) {
try {
@@ -23,10 +23,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const response = await Promise.all(
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) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
openai.deleteFile(data.training_files[0].id, { httpsAgent });
const model = await Model.findById(item.modelId).populate({
path: 'userId',

View File

@@ -2,13 +2,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { connectToDatabase, User } from '@/service/mongo';
import { sendCode } from '@/service/utils/sendEmail';
import { EmailTypeEnum } from '@/constants/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { email, type } = req.query;
const { email, type } = req.query as { email: string; type: `${EmailTypeEnum}` };
if (!email || !type) {
throw new Error('缺少参数');
@@ -16,6 +16,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 注册人数限流
if (type === EmailTypeEnum.register) {
const maxCount = process.env.MAX_USER ? +process.env.MAX_USER : Infinity;
const userCount = await User.count();
if (userCount >= maxCount) {
throw new Error('当前注册用户已满,请等待名额~');
}
}
let code = '';
for (let i = 0; i < 6; i++) {
code += Math.floor(Math.random() * 10);

View File

@@ -10,7 +10,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少参数');
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);

View File

@@ -13,16 +13,19 @@ import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon';
import { useScreen } from '@/hooks/useScreen';
import Markdown from '@/components/Markdown';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import { OpenAiModelEnum } from '@/constants/model';
import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global';
const Chat = () => {
const Markdown = dynamic(() => import('@/components/Markdown'));
const textareaMinH = '22px';
const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
const { toast } = useToast();
const router = useRouter();
const { media } = useScreen();
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
const { isPc, media } = useScreen();
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
@@ -32,11 +35,10 @@ const Chat = () => {
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
const { Loading } = useLoading();
const { setLoading } = useGlobalStore();
// 滚动到底部
const scrollToBottom = useCallback(() => {
// 滚动到底部
setTimeout(() => {
ChatBox.current &&
ChatBox.current.scrollTo({
@@ -47,28 +49,56 @@ const Chat = () => {
}, []);
// 初始化聊天框
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), {
cacheTime: 5 * 60 * 1000,
onSuccess(res) {
if (!res) return;
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item,
status: 'finish'
}))
);
scrollToBottom();
useQuery(
['initData'],
() => {
setLoading(true);
return getInitChatSiteInfo(chatId, windowId);
},
onError() {
toast({
title: '初始化异常',
status: 'error'
});
{
onSuccess(res) {
// 可能没有 windowId给它设置一下
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 方法
const gpt3ChatPrompt = useCallback(
@@ -107,36 +137,56 @@ const Chat = () => {
return new Promise((resolve, reject) => {
const event = getChatGPTSendEvent(chatId, windowId);
event.onmessage = ({ data }) => {
if (data === '[DONE]') {
event.close();
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
resolve('');
} else if (data) {
const msg = data.replace(/<br\/>/g, '\n');
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + msg
};
})
);
}
};
event.onerror = (err) => {
console.error(err, '===');
// 30s 收不到消息就报错
let timer = setTimeout(() => {
event.close();
reject('对话出现错误');
reject('服务器超时');
}, 30000);
event.addEventListener('responseData', ({ data }) => {
/* 重置定时器 */
clearTimeout(timer);
timer = setTimeout(() => {
event.close();
reject('服务器超时');
}, 30000);
const msg = data.replace(/<br\/>/g, '\n');
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + msg
};
})
);
});
event.addEventListener('done', () => {
console.log('done');
clearTimeout(timer);
event.close();
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
resolve('');
});
event.addEventListener('serviceError', ({ data: err }) => {
clearTimeout(timer);
event.close();
console.log('error->', err, '===');
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
});
event.onerror = (err) => {
clearTimeout(timer);
event.close();
console.log('error->', err);
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
};
});
},
@@ -174,15 +224,8 @@ const Chat = () => {
// 插入内容
setChatList(newChatList);
setInputVal('');
// 滚动到底部
setTimeout(() => {
scrollToBottom();
if (TextareaDom.current) {
TextareaDom.current.style.height = 22 + 'px';
}
}, 100);
resetInputVal('');
scrollToBottom();
const fnMap: { [key: string]: any } = {
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
@@ -202,13 +245,13 @@ const Chat = () => {
}
} catch (err) {
toast({
title: typeof err === 'string' ? err : '聊天已过期',
title: typeof err === 'string' ? err : '聊天出错了~',
status: 'warning',
duration: 5000,
isClosable: true
});
setInputVal(storeInput);
resetInputVal(storeInput);
setChatList(newChatList.slice(0, newChatList.length - 2));
}
@@ -219,6 +262,7 @@ const Chat = () => {
gpt3ChatPrompt,
inputVal,
isChatting,
resetInputVal,
scrollToBottom,
toast
]);
@@ -227,22 +271,16 @@ const Chat = () => {
const reEdit = useCallback(async () => {
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
// 删除数据库最后一句
delLastMessage(windowId);
await delLastMessage(windowId);
const val = chatList[chatList.length - 1].value;
setInputVal(val);
resetInputVal(val);
setChatList(chatList.slice(0, -1));
setTimeout(() => {
if (TextareaDom.current) {
TextareaDom.current.style.height = val.split('\n').length * 22 + 'px';
}
}, 100);
}, [chatList, windowId]);
}, [chatList, resetInputVal, windowId]);
return (
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}>
<Flex height={'100%'} flexDirection={'column'}>
{/* 头部 */}
<Flex
px={4}
@@ -253,14 +291,9 @@ const Chat = () => {
zIndex={1}
>
<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 && (
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
<Box mr={10} cursor={'pointer'} onClick={scrollToBottom}>
<Icon
name={'icon-xiangxiazhankai-xianxingyuankuang'}
width={25}
@@ -269,6 +302,10 @@ const Chat = () => {
></Icon>
</Box>
)}
{/* 重置按键 */}
<Button size={'sm'} colorScheme={'gray'} onClick={resetChat}>
</Button>
</Flex>
{/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}>
@@ -276,25 +313,29 @@ const Chat = () => {
<Box
key={index}
py={media(9, 6)}
px={media(4, 3)}
px={media(4, 2)}
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={4}>
<Box mr={media(4, 1)}>
<Image
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'}
alt="/imgs/modelAvatar.png"
width={30}
height={30}
></Image>
</Box>
<Box flex={'1 0 0'} w={0} overflowX={'auto'}>
<Markdown
source={item.value}
isChatting={isChatting && index === chatList.length - 1}
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png"
width={media(30, 20)}
height={media(30, 20)}
/>
</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>
</Box>
))}
@@ -302,18 +343,15 @@ const Chat = () => {
<Box
m={media('20px auto', '0 auto')}
w={media('100vw', '100%')}
maxW={'800px'}
maxW={media('800px', 'auto')}
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
>
{lastWordHuman ? (
<Box textAlign={'center'}>
<Box color={'red'}></Box>
<Flex py={5} justifyContent={'center'}>
<Button
mr={20}
onClick={() => router.replace(`/chat?chatId=${chatId}`)}
colorScheme={'green'}
>
<Button mr={20} onClick={resetChat} colorScheme={'green'}>
</Button>
<Button onClick={reEdit}></Button>
@@ -349,12 +387,12 @@ const Chat = () => {
onChange={(e) => {
const textarea = e.target;
setInputVal(textarea.value);
textarea.style.height = textarea.value.split('\n').length * 22 + 'px';
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
}}
onKeyDown={(e) => {
// 触发快捷发送
if (e.keyCode === 13 && !e.shiftKey) {
if (isPc && e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
@@ -382,9 +420,17 @@ const Chat = () => {
</Box>
)}
</Box>
<Loading loading={!chatSiteData} />
</Flex>
);
};
export default Chat;
export async function getServerSideProps(context: any) {
const chatId = context.query?.chatId || '';
const windowId = context.query?.windowId || '';
return {
props: { chatId, windowId }
};
}

View File

@@ -1,12 +1,9 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import { Card, Text, Box, Heading, Flex } from '@chakra-ui/react';
import React from 'react';
import { Card } from '@chakra-ui/react';
import Markdown from '@/components/Markdown';
import { introPage } from '@/constants/common';
const Home = () => {
const router = useRouter();
return (
<Card p={5} lineHeight={2}>
<Markdown source={introPage} isChatting={false} />

View File

@@ -1,19 +1,12 @@
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '../../../constants/user';
import { postFindPassword } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>;
@@ -28,7 +21,7 @@ interface RegisterType {
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
@@ -66,16 +59,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
);
toast({
title: `密码已找回`,
status: 'success',
position: 'top'
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || '修改密码异常',
status: 'error'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
}
setRequesting(false);
},
@@ -85,7 +75,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT
FastGPT
</Box>
<form onSubmit={handleSubmit(onclickFindPassword)}>
<FormControl mt={8} isInvalid={!!errors.email}>

View File

@@ -42,13 +42,11 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
title: '登录成功',
status: 'success'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
} catch (error: any) {
toast({
title: error.message || '登录异常',
status: 'error'
});
}
setRequesting(false);
},
@@ -58,7 +56,7 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT
FastGPT
</Box>
<form onSubmit={handleSubmit(onclickLogin)}>
<FormControl mt={8} isInvalid={!!errors.email}>

View File

@@ -1,19 +1,12 @@
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '@/constants/user';
import { postRegister } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
interface Props {
loginSuccess: (e: ResLogin) => void;
@@ -28,7 +21,7 @@ interface RegisterType {
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
@@ -66,16 +59,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
);
toast({
title: `注册成功`,
status: 'success',
position: 'top'
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || '注册异常',
status: 'error'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
}
setRequesting(false);
},
@@ -85,7 +75,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
DocGPT
FastGPT
</Box>
<form onSubmit={handleSubmit(onclickRegister)}>
<FormControl mt={8} isInvalid={!!errors.email}>

View File

@@ -1,7 +1,5 @@
.loginPage {
background: url('/icon/login-bg.svg') no-repeat;
background-size: cover;
height: 100vh;
width: 100vw;
user-select: none;
}

View File

@@ -1,15 +1,17 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import styles from './index.module.scss';
import { Box, Flex, Image } from '@chakra-ui/react';
import { PageTypeEnum } from '@/constants/user';
import LoginForm from './components/LoginForm';
import RegisterForm from './components/RegisterForm';
import ForgetPasswordForm from './components/ForgetPasswordForm';
import { useScreen } from '@/hooks/useScreen';
import type { ResLogin } from '@/api/response/user';
import { useRouter } from 'next/router';
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 router = useRouter();
const { isPc } = useScreen();
@@ -19,28 +21,29 @@ const Login = () => {
const loginSuccess = useCallback(
(res: ResLogin) => {
setUserInfo(res.user, res.token);
router.push('/');
router.push('/model/list');
},
[router, setUserInfo]
);
const map = {
[PageTypeEnum.login]: {
Component: <LoginForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
},
[PageTypeEnum.register]: {
Component: <RegisterForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
},
[PageTypeEnum.forgetPassword]: {
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
}
};
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {
const TypeMap = {
[PageTypeEnum.login]: LoginForm,
[PageTypeEnum.register]: RegisterForm,
[PageTypeEnum.forgetPassword]: ForgetPasswordForm
};
const Component = TypeMap[type];
return <Component setPageType={setPageType} loginSuccess={loginSuccess} />;
}
useEffect(() => {
router.prefetch('/model/list');
}, [router]);
return (
<Box className={styles.loginPage} p={isPc ? '10vh 10vw' : 0}>
<Box className={styles.loginPage} h={'100%'} p={isPc ? '10vh 10vw' : 0}>
<Flex
maxW={'1240px'}
m={'auto'}
@@ -54,7 +57,7 @@ const Login = () => {
>
{isPc && (
<Image
src={map[pageType].img}
src={'/icon/loginLeft.svg'}
order={pageType === PageTypeEnum.login ? 0 : 2}
flex={'1 0 0'}
w="0"
@@ -76,7 +79,7 @@ const Login = () => {
px={10}
borderRadius={isPc ? 'md' : 'none'}
>
{map[pageType].Component}
<DynamicComponent type={pageType} />
</Box>
</Flex>
</Box>

View File

@@ -25,11 +25,9 @@ interface CreateFormType {
}
const CreateModel = ({
isOpen,
setCreateModelOpen,
onSuccess
}: {
isOpen: boolean;
setCreateModelOpen: Dispatch<boolean>;
onSuccess: Dispatch<ModelType>;
}) => {
@@ -72,7 +70,7 @@ const CreateModel = ({
return (
<>
<Modal isOpen={isOpen} onClose={() => setCreateModelOpen(false)}>
<Modal isOpen={true} onClose={() => setCreateModelOpen(false)}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>

View File

@@ -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 type { ModelType } from '@/types/model';
import { useForm } from 'react-hook-form';
@@ -7,17 +7,17 @@ import { putModelById } from '@/api/model';
import { useScreen } from '@/hooks/useScreen';
import { useGlobalStore } from '@/store/global';
const ModelEditForm = ({ model }: { model: ModelType }) => {
const ModelEditForm = ({ model }: { model?: ModelType }) => {
const isInit = useRef(false);
const {
register,
handleSubmit,
reset,
formState: { errors }
} = useForm<ModelType>({
defaultValues: model
});
} = useForm<ModelType>();
const { setLoading } = useGlobalStore();
const { toast } = useToast();
const { isPc } = useScreen();
const { media } = useScreen();
const onclickSave = useCallback(
async (data: ModelType) => {
@@ -34,7 +34,7 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
status: 'success'
});
} catch (err) {
console.log(err);
console.log('error->', err);
toast({
title: err as string,
status: 'success'
@@ -61,8 +61,16 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
});
}, [errors, toast]);
/* model 只会改变一次 */
useEffect(() => {
if (model && !isInit.current) {
reset(model);
isInit.current = true;
}
}, [model, reset]);
return (
<Grid gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
<Grid gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
<Card p={4}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={'lg'}>
@@ -83,7 +91,7 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Box>{model.service.modelName}</Box>
<Box>{model?.service.modelName}</Box>
</Flex>
</FormControl>
<FormControl mt={5}>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Button, Flex, Heading, Tag } from '@chakra-ui/react';
import React, { useEffect } from 'react';
import { Box, Button, Flex, Tag } from '@chakra-ui/react';
import type { ModelType } from '@/types/model';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
@@ -14,6 +14,10 @@ const ModelPhoneList = ({
}) => {
const router = useRouter();
useEffect(() => {
router.prefetch('/chat');
}, [router]);
return (
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
{models.map((model) => (

View File

@@ -1,4 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import {
Button,
Table,
@@ -84,6 +84,10 @@ const ModelTable = ({
}
];
useEffect(() => {
router.prefetch('/chat');
}, [router]);
return (
<Card py={3}>
<TableContainer>

View File

@@ -1,5 +1,5 @@
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 { getModelTrainings } from '@/api/model';
import type { TrainingItemType } from '@/types/training';
@@ -29,7 +29,7 @@ const Training = ({ model }: { model: ModelType }) => {
const res = await getModelTrainings(id);
setRecords(res);
} catch (error) {
console.log(error);
console.log('error->', error);
}
}, []);
@@ -38,7 +38,7 @@ const Training = ({ model }: { model: ModelType }) => {
}, [loadTrainingRecords, model]);
return (
<Card p={4} h={'100%'}>
<>
<Box fontWeight={'bold'} fontSize={'lg'}>
: {model.trainingTimes}
</Box>
@@ -63,7 +63,7 @@ const Training = ({ model }: { model: ModelType }) => {
</Tbody>
</Table>
</TableContainer>
</Card>
</>
);
};

View File

@@ -11,12 +11,14 @@ import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import ModelEditForm from './components/ModelEditForm';
import Icon from '@/components/Icon';
import Training from './components/Training';
import dynamic from 'next/dynamic';
const Training = dynamic(() => import('./components/Training'));
const ModelDetail = () => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该模型?'
@@ -39,17 +41,16 @@ const ModelDetail = () => {
const res = await getModelById(modelId as string);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
console.log(res);
} catch (err) {
console.log(err);
console.log('error->', err);
}
setLoading(false);
}, [modelId, setLoading]);
useEffect(() => {
loadModel();
}, [loadModel, modelId]);
router.prefetch('/chat');
}, [loadModel, modelId, router]);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
@@ -63,7 +64,7 @@ const ModelDetail = () => {
});
router.replace('/model/list');
} catch (err) {
console.log(err);
console.log('error->', err);
}
setLoading(false);
}, [setLoading, model, router, toast]);
@@ -77,7 +78,7 @@ const ModelDetail = () => {
router.push(`/chat?chatId=${chatId}`);
} catch (err) {
console.log(err);
console.log('error->', err);
}
setLoading(false);
}, [setLoading, model, router]);
@@ -105,7 +106,7 @@ const ModelDetail = () => {
title: typeof err === 'string' ? err : '文件格式错误',
status: 'error'
});
console.log(err);
console.log('error->', err);
}
setLoading(false);
},
@@ -120,122 +121,126 @@ const ModelDetail = () => {
try {
await putModelTrainingStatus(model._id);
loadModel();
} catch (error) {
console.log(error);
} catch (error: any) {
console.log('error->', error);
toast({
title: error.message || '更新失败',
status: 'error'
});
}
setLoading(false);
}, [setLoading, loadModel, model]);
}, [model, setLoading, loadModel, toast]);
return (
<>
{!!model && (
<>
{/* 头部 */}
<Card px={6} py={3}>
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{model.name}
</Box>
<Tag
ml={2}
variant="solid"
colorScheme={formatModelStatus[model.status].colorTheme}
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
onClick={handleClickUpdateStatus}
>
{/* 头部 */}
<Card px={6} py={3}>
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{model?.name || '模型'}
</Box>
{!!model && (
<Tag
ml={2}
variant="solid"
colorScheme={formatModelStatus[model.status].colorTheme}
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
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}
</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>
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text}
</Tag>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
</Box>
</>
)}
</Card>
{/* 基本信息编辑 */}
<Box mt={5}>
<ModelEditForm model={model} />
)}
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
</Box>
</>
)}
</Card>
{/* 基本信息编辑 */}
<Box mt={5}>
<ModelEditForm model={model} />
</Box>
{/* 其他配置 */}
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
<Card p={4}>{!!model && <Training model={model} />}</Card>
<Card p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}>
</Box>
{/* 其他配置 */}
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
<Training model={model} />
<Card h={'100%'} p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}>
</Box>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Button
size={'sm'}
onClick={() => {
SelectFileDom.current?.click();
}}
title={!canTrain ? '' : '模型不支持微调'}
isDisabled={!canTrain}
>
</Button>
<Flex
as={'a'}
href="/TrainingTemplate.jsonl"
download
ml={5}
cursor={'pointer'}
alignItems={'center'}
color={'blue.500'}
>
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
</Flex>
</Flex>
{/* 提示 */}
<Box mt={3} py={3} color={'blackAlpha.500'}>
<Box as={'li'} lineHeight={1.9}>
prompt completion
</Box>
<Box as={'li'} lineHeight={1.9}>
prompt \n\n###\n\n prompt
</Box>
<Box as={'li'} lineHeight={1.9}>
completion ###
</Box>
</Box>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Button
colorScheme={'red'}
size={'sm'}
onClick={() => {
openConfirm(() => {
handleDelModel();
});
}}
>
</Button>
</Flex>
</Card>
</Grid>
</>
)}
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Button
size={'sm'}
onClick={() => {
SelectFileDom.current?.click();
}}
title={!canTrain ? '' : '模型不支持微调'}
isDisabled={!canTrain}
>
</Button>
<Flex
as={'a'}
href="/TrainingTemplate.jsonl"
download
ml={5}
cursor={'pointer'}
alignItems={'center'}
color={'blue.500'}
>
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
</Flex>
</Flex>
{/* 提示 */}
<Box mt={3} py={3} color={'blackAlpha.500'}>
<Box as={'li'} lineHeight={1.9}>
prompt completion
</Box>
<Box as={'li'} lineHeight={1.9}>
prompt \n\n###\n\n prompt
</Box>
<Box as={'li'} lineHeight={1.9}>
completion ###
</Box>
</Box>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Button
colorScheme={'red'}
size={'sm'}
onClick={() => {
openConfirm(() => {
handleDelModel();
});
}}
>
</Button>
</Flex>
</Card>
</Grid>
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
</Box>

View File

@@ -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 { getMyModels } from '@/api/model';
import { getChatSiteId } from '@/api/chat';
import { ModelType } from '@/types/model';
import CreateModel from './components/CreateModel';
import { useRouter } from 'next/router';
import ModelTable from './components/ModelTable';
import ModelPhoneList from './components/ModelPhoneList';
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 { toast } = useToast();
const { isPc } = useScreen();
const router = useRouter();
const [models, setModels] = useState<ModelType[]>([]);
const [openCreateModel, setOpenCreateModel] = useState(false);
const { setLoading } = useGlobalStore();
const { Loading, setIsLoading } = useLoading();
/* 加载模型 */
const loadModels = useCallback(async () => {
setLoading(true);
try {
const res = await getMyModels();
const { isLoading } = useQuery(['loadModels'], () => getMyModels(), {
onSuccess(res) {
if (!res) return;
setModels(res);
} catch (err) {
console.log(err);
}
setLoading(false);
}, [setLoading]);
useEffect(() => {
loadModels();
}, [loadModels]);
});
/* 创建成功回调 */
const createModelSuccess = useCallback((data: ModelType) => {
@@ -40,19 +38,23 @@ const ModelList = () => {
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(
async (modelId: string) => {
setLoading(true);
setIsLoading(true);
try {
const chatId = await getChatSiteId(modelId);
router.push(`/chat?chatId=${chatId}`, undefined, {
shallow: true
});
} catch (err) {
console.log(err);
} catch (err: any) {
console.log('error->', err);
toast({
title: err.message || '出现一些异常',
status: 'error'
});
}
setLoading(false);
setIsLoading(false);
},
[router, setLoading]
[router, setIsLoading, toast]
);
return (
@@ -78,11 +80,11 @@ const ModelList = () => {
)}
</Box>
{/* 创建弹窗 */}
<CreateModel
isOpen={openCreateModel}
setCreateModelOpen={setOpenCreateModel}
onSuccess={createModelSuccess}
/>
{openCreateModel && (
<CreateModel setCreateModelOpen={setOpenCreateModel} onSuccess={createModelSuccess} />
)}
<Loading loading={isLoading} />
</Box>
);
};

View File

@@ -1,3 +1,6 @@
export const openaiError: Record<string, string> = {
context_length_exceeded: '内容超出长度'
context_length_exceeded: '内容超长了,请重置对话',
Unauthorized: 'API-KEY 不合法',
rate_limit_reached: '同时访问用户过多,请稍后再试',
'Bad Request': '内容太多了~'
};

View File

@@ -1,18 +1,28 @@
import mongoose from 'mongoose';
import type { Mongoose } from 'mongoose';
let cachedClient: Mongoose;
export async function connectToDatabase() {
if (cachedClient && cachedClient.connection.readyState === 1) {
return cachedClient;
/**
* 连接 MongoDB 数据库
*/
export async function connectToDatabase(): Promise<void> {
if (global.mongodb) {
return;
}
cachedClient = await mongoose.connect(process.env.MONGODB_UR as string, {
dbName: 'doc_gpt'
});
return cachedClient;
global.mongodb = 'connecting';
console.log('connect mongo');
try {
mongoose.set('strictQuery', true);
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';

View File

@@ -20,12 +20,15 @@ export const jsonRes = (
let msg = message;
if ((code < 200 || code >= 400) && !message) {
msg =
typeof error === 'string'
? error
: openaiError[error?.response?.data?.message] || error?.message || '请求错误';
msg = error?.message || '请求错误';
if (typeof error === 'string') {
msg = error;
} else if (error?.response?.data?.message in openaiError) {
msg = openaiError[error?.response?.data?.message];
}
console.log(msg);
console.log('error->', error);
console.log('error->', msg);
}
res.json({

View File

@@ -15,26 +15,26 @@ let mailTransport = nodemailer.createTransport({
const emailMap: { [key: string]: any } = {
[EmailTypeEnum.register]: {
subject: '注册 DocGPT 账号',
html: (code: string) => `<div>您正在注册 DocGPT 账号,验证码为:${code}</div>`
subject: '注册 FastGPT 账号',
html: (code: string) => `<div>您正在注册 FastGPT 账号,验证码为:${code}</div>`
},
[EmailTypeEnum.findPassword]: {
subject: '修改 DocGPT 密码',
html: (code: string) => `<div>您正在修改 DocGPT 账号密码,验证码为:${code}</div>`
subject: '修改 FastGPT 密码',
html: (code: string) => `<div>您正在修改 FastGPT 账号密码,验证码为:${code}</div>`
}
};
export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
from: `"FastGPT" ${myEmail}`,
to: email,
subject: emailMap[type]?.subject,
html: emailMap[type]?.html(code)
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
console.log('error->', err);
reject('邮箱异常');
} else {
resolve('');
@@ -46,14 +46,14 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`)
export const sendTrainSucceed = (email: string, modelName: string) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
from: `"FastGPT" ${myEmail}`,
to: email,
subject: '模型训练完成通知',
html: `你的模型 ${modelName} 已于 ${dayjs().format('YYYY-MM-DD HH:mm')} 训练完成!`
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
console.log('error->', err);
reject('邮箱异常');
} else {
resolve('');

View File

@@ -49,15 +49,12 @@ export const getUserOpenaiKey = async (userId: string) => {
};
/* 代理 */
export const openaiProxy: any =
process.env.AXIOS_PROXY_PORT && process.env.AXIOS_PROXY_HOST
? {
httpsAgent: tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
}),
proxy: false
}
export const httpsAgent =
process.env.AXIOS_PROXY_HOST && process.env.AXIOS_PROXY_PORT
? tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
})
: undefined;

View File

@@ -24,63 +24,9 @@ td,
svg {
margin: 0;
}
body,
button,
input,
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;
#__next {
height: 100%;
}
::-webkit-scrollbar,
@@ -104,6 +50,9 @@ table {
}
@media (max-width: 900px) {
html {
font-size: 14px;
}
::-webkit-scrollbar,
::-webkit-scrollbar {
width: 2px;

6
src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import type { Mongoose } from 'mongoose';
declare global {
var mongodb: Mongoose | string | null;
}
export {};

View File

@@ -8,19 +8,25 @@ export const useCopyData = () => {
const { toast } = useToast();
return {
copyData: (data: string, title: string = '复制成功') => {
const clipboardObj = navigator.clipboard;
clipboardObj
.writeText(data)
.then(() => {
toast({
title,
status: 'success',
duration: 1000
});
})
.catch((err) => {
console.log(err);
try {
const textarea = document.createElement('textarea');
textarea.value = data;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
toast({
title,
status: 'success',
duration: 1000
});
} catch (error) {
console.log('error->', error);
toast({
title: '复制失败',
status: 'error'
});
}
}
};
};

View File

@@ -19,6 +19,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.d.ts"],
"exclude": ["node_modules"]
}