perf: 聊天页优化

perf: md解析样式

perf: ui调整

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

perf: 去除console,

perf: 图片cdn

feat: 图片地址

perf: 登录顺序

feat: 流优化
This commit is contained in:
Archer 2023-03-09 20:44:13 +08:00
parent 2390823282
commit 17364e9da3
No known key found for this signature in database
GPG Key ID: A3F5915562F98511
47 changed files with 1384 additions and 852 deletions

View File

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

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
``` ```
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数据库地址
MY_MAIL=发送验证码邮箱 MY_MAIL=发送验证码邮箱
MAILE_CODE=邮箱秘钥 MAILE_CODE=邮箱秘钥
TOKEN_KEY=随便填一个用于生成和校验token TOKEN_KEY=随便填一个用于生成和校验token
@ -15,22 +15,62 @@ TOKEN_KEY=随便填一个用于生成和校验token
```bash ```bash
pnpm dev pnpm dev
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 部署 ## 部署
```bash ```bash
# 本地 docker 打包 # 本地 docker 打包
docker build -t imageName . docker build -t imageName:tag .
docker push imageName docker push imageName:tag
```
# 服务器拉取部署
docker pull imageName 服务器请准备好 docker mongonginx和代理。 镜像走本机的代理,所以用 hostport改成代理的端口clash一般都是7890。
docker stop doc-gpt || true
docker rm doc-gpt || true ```bash
# 运行时才把参数写入 # 服务器拉取部署, imageName 替换成镜像名
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 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://aha:ROOT_root123@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.
# 介绍页 # 介绍页
@ -70,4 +110,4 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
### 其他问题 ### 其他问题
还有其他问题,可以加我 wx拉个交流群大家一起聊聊。 还有其他问题,可以加我 wx拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg) ![](/icon/erweima.jpg)

View File

@ -6,7 +6,17 @@ const isDev = process.env.NODE_ENV === 'development';
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
reactStrictMode: false, reactStrictMode: false,
compress: true compress: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'docgpt-1301319986.cos.ap-shanghai.myqcloud.com',
port: '',
pathname: '/**'
}
]
}
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@ -19,11 +19,10 @@
"@next/font": "13.1.6", "@next/font": "13.1.6",
"@reduxjs/toolkit": "^1.9.3", "@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",
"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,13 +31,16 @@
"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",
@ -56,6 +58,8 @@
"@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"

647
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 { 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
@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
}); });
/* 获取 Chat 的 Event 对象,进行持续通信 */ /* 获取 Chat 的 Event 对象,进行持续通信 */
export const getChatGPTSendEvent = (chatId: string, windowId: string) => 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) { function checkRes(data: ResponseDataType) {
if (data === undefined) { if (data === undefined) {
console.log(data, 'data is empty'); console.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);
@ -49,21 +49,20 @@ function responseError(err: any) {
console.error('请求错误', err); console.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);
} }
/* 创建请求实例 */ /* 创建请求实例 */

View File

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

View File

@ -43,18 +43,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}>

View File

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

View File

@ -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)
? { ? {

View File

@ -27,96 +27,356 @@
opacity: 1; opacity: 1;
} }
} }
.markdown {
/* 标题样式 */
h1 {
font-size: 1.8rem;
}
h2 { .markdown > *:first-child {
font-size: 1.6rem; margin-top: 0 !important;
} }
.markdown > *:last-child {
h3 { margin-bottom: 0 !important;
font-size: 1.4rem;
} }
.markdown a.absent {
h4 { color: #cc0000;
font-size: 1.2rem;
} }
.markdown a.anchor {
h5 { bottom: 0;
font-size: 1rem; cursor: pointer;
display: block;
left: 0;
margin-left: -30px;
padding-left: 30px;
position: absolute;
top: 0;
} }
.markdown h1,
h6 { .markdown h2,
font-size: 0.83rem; .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,
ol, .markdown h3 .mini-icon-link,
ul { .markdown h4 .mini-icon-link,
padding-left: 1.5rem; .markdown h5 .mini-icon-link,
margin-left: 1rem; .markdown h6 .mini-icon-link {
color: #000000;
display: none;
} }
ul { .markdown h1:hover a.anchor,
list-style: inside; .markdown h2:hover a.anchor,
} .markdown h3:hover a.anchor,
ol { .markdown h4:hover a.anchor,
list-style: decimal; .markdown h5:hover a.anchor,
} .markdown h6:hover a.anchor {
line-height: 1;
/* 链接样式 */ margin-left: -22px;
a { padding-left: 0;
color: #0077cc;
text-decoration: none; text-decoration: none;
border-bottom: 1px solid #0077cc; top: 15%;
} }
.markdown h1:hover a.anchor .mini-icon-link,
a:hover { .markdown h2:hover a.anchor .mini-icon-link,
color: #005580; .markdown h3:hover a.anchor .mini-icon-link,
border-bottom-color: #005580; .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,
img { .markdown h2 tt,
max-width: 100%; .markdown h2 code,
max-height: 200px; .markdown h3 tt,
margin: auto; .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;
em, font-size: 28px;
i { }
.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-style: italic;
font-weight: bold;
margin: 15px 0 5px;
padding: 0;
} }
.markdown dl dt:first-child {
strong, padding: 0;
b { }
.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; font-weight: bold;
} }
.markdown table th,
/* 代码样式 */ .markdown table td {
code { border: 1px solid #cccccc;
border-radius: 3px; padding: 6px 13px;
width: 100%;
} }
.markdown table tr {
background-color: #ffffff;
border-top: 1px solid #cccccc;
}
.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: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
}
.markdown pre code,
.markdown pre tt {
background-color: transparent;
border: medium none;
}
.markdown {
font-size: 14px;
line-height: 1.6;
letter-spacing: 0.5px;
text-align: justify;
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;
border: none;
background-color: #222; background-color: #222;
color: #fff; color: #fff;
width: 100%;
font-family: 'Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji';
} }
p { a {
line-height: 1.7; text-decoration: underline;
color: var(--chakra-colors-blue-600);
} }
} }

View File

@ -1,33 +1,41 @@
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 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 { 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';
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.replace(/\n/g, ' \n'), [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).replace(/\n$/, '');
return !inline ? (
return (
<Box my={3} borderRadius={'md'} overflow={'hidden'}> <Box my={3} borderRadius={'md'} overflow={'hidden'}>
<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 +44,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}>
{children}
</code>
); );
} }
}} }}
linkTarget="_blank"
> >
{source} {formatSource}
</ReactMarkdown> </ReactMarkdown>
); );
}; };

View File

@ -6,8 +6,7 @@ export enum EmailTypeEnum {
export const introPage = ` export const introPage = `
## 使 Doc GPT ## 使 Doc GPT
使 使:
1. 使 1. 使
2. openai openai API Key 2. openai openai API Key
3. ChatGPT 3. ChatGPT
@ -39,6 +38,5 @@ export const introPage = `
* http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764 * http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
### ###
wx wx: YNyiqi
![](/imgs/erweima.jpg)
`; `;

View File

@ -58,7 +58,11 @@ export const theme = extendTheme({
global: { global: {
'html, body': { 'html, body': {
color: 'blackAlpha.800', color: 'blackAlpha.800',
fontSize: '14px' fontSize: '14px',
fontFamily:
'Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji',
height: '100%',
overflowY: 'auto'
} }
} }
}, },

View File

@ -1,4 +1,4 @@
import { useState, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { import {
AlertDialog, AlertDialog,
AlertDialogBody, AlertDialogBody,
@ -17,12 +17,16 @@ 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(
(confirm?: any, cancel?: any) => {
onOpen(); onOpen();
confirmCb.current = confirm; confirmCb.current = confirm;
cancelCb.current = cancel; cancelCb.current = cancel;
}, },
ConfirmChild: () => ( [onOpen]
),
ConfirmChild: useCallback(
() => (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay> <AlertDialogOverlay>
<AlertDialogContent> <AlertDialogContent>
@ -44,7 +48,7 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
</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();
@ -56,6 +60,8 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
</AlertDialogContent> </AlertDialogContent>
</AlertDialogOverlay> </AlertDialogOverlay>
</AlertDialog> </AlertDialog>
),
[content, isOpen, onClose, title]
) )
}; };
}; };

View File

@ -1,16 +1,11 @@
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
}: {
loading?: boolean;
fixed?: boolean;
}): JSX.Element | null => {
return isLoading || loading ? ( return isLoading || loading ? (
<Flex <Flex
position={fixed ? 'fixed' : 'absolute'} position={fixed ? 'fixed' : 'absolute'}
@ -26,7 +21,9 @@ export const useLoading = (props?: { defaultLoading: boolean }) => {
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" /> <Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
</Flex> </Flex>
) : null; ) : null;
}; },
[isLoading]
);
return { return {
isLoading, isLoading,

View File

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

View File

@ -1,12 +1,20 @@
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.
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
// Create a client // Create a client
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -18,6 +26,7 @@ export default function App({ Component, pageProps }: AppProps) {
} }
}); });
export default function App({ Component, pageProps }: AppProps) {
return ( return (
<> <>
<Head> <Head>
@ -28,8 +37,8 @@ export default function App({ Component, pageProps }: AppProps) {
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>

View File

@ -1,6 +1,6 @@
// 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 { Readable } from 'stream';
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 { openaiProxy } from '@/service/utils/tools';
@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.writeHead(200, { res.setHeader('Connection', 'keep-alive');
Connection: 'keep-alive', res.setHeader('Cache-Control', 'no-cache');
'Content-Encoding': 'none', res.setHeader('Content-Type', 'text/event-stream');
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream' const responseData: string[] = [];
const stream = new Readable({
read(size) {
const data = responseData.shift() || null;
this.push(data);
}
}); });
res.on('close', () => {
res.end();
stream.destroy();
});
const { chatId, windowId } = req.query as { chatId: string; windowId: string }; const { chatId, windowId } = req.query as { chatId: string; windowId: string };
try { try {
@ -47,9 +58,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
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.replace(/(\n| )/g, '')
}) })
); );
// 第一句话,强调代码类型
formatPrompts.unshift({
role: ChatCompletionRequestMessageRoleEnum.System,
content:
'If the content is code or code blocks, please mark the code type as accurately as possible!'
});
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey); const chatAPI = getOpenAIApi(userApiKey);
@ -68,8 +85,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const reg = /{"content"(.*)"}/g; const reg = /{"content"(.*)"}/g;
// @ts-ignore // @ts-ignore
const match = chatResponse.data.match(reg); const match = chatResponse.data.match(reg);
if (!match) return;
let AIResponse = ''; let AIResponse = '';
if (match) {
// 循环给 stream push 内容
match.forEach((item: string, i: number) => { match.forEach((item: string, i: number) => {
try { try {
const json = JSON.parse(item); const json = JSON.parse(item);
@ -77,15 +97,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (i === 0 && json.content?.startsWith('\n')) return; if (i === 0 && json.content?.startsWith('\n')) return;
AIResponse += json.content; AIResponse += json.content;
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
content && res.write(`data: ${content}\n\n`); if (content) {
responseData.push(`event: responseData\ndata: ${content}\n\n`);
// res.write(`event: responseData\n`)
// res.write(`data: ${content}\n\n`)
}
} catch (err) { } catch (err) {
err; err;
} }
}); });
}
res.write(`data: [DONE]\n\n`);
responseData.push(`event: done\ndata: \n\n`);
// 存入库 // 存入库
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, { await ChatWindow.findByIdAndUpdate(windowId, {
$push: { $push: {
content: { content: {
@ -95,16 +119,41 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
updateTime: Date.now() updateTime: Date.now()
}); });
})();
res.end();
} catch (err: any) { } catch (err: any) {
console.log(err?.response?.data || err); let errorText = err;
if (err.code === 'ECONNRESET') {
errorText = '服务器代理出错';
} else {
switch (err?.response?.data?.error?.code) {
case 'invalid_api_key':
errorText = 'API-KEY不合法';
break;
case 'context_length_exceeded':
errorText = '内容超长了,请重置对话';
break;
case 'rate_limit_reached':
errorText = '同时访问用户过多,请稍后再试';
break;
case null:
errorText = 'OpenAI 服务器访问超时';
break;
default:
errorText = '服务器异常';
}
}
console.error(errorText);
responseData.push(`event: serviceError\ndata: ${errorText}\n\n`);
// 删除最一条数据库记录, 也就是预发送的那一条 // 删除最一条数据库记录, 也就是预发送的那一条
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, { await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 }, $pop: { content: 1 },
updateTime: Date.now() updateTime: Date.now()
}); });
})();
}
res.end(); // 开启 stream 传输
} stream.pipe(res);
} }

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('聊天框已过期'); 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

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

@ -13,15 +13,19 @@ 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';
const Markdown = dynamic(() => import('@/components/Markdown'));
const textareaMinH = '22px';
const Chat = () => { const Chat = () => {
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 { 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,7 +36,7 @@ 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(() => {
@ -47,7 +51,14 @@ const Chat = () => {
}, []); }, []);
// 初始化聊天框 // 初始化聊天框
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), { useQuery(
[chatId, windowId],
() => {
if (!chatId) return null;
setLoading(true);
return getInitChatSiteInfo(chatId, windowId);
},
{
cacheTime: 5 * 60 * 1000, cacheTime: 5 * 60 * 1000,
onSuccess(res) { onSuccess(res) {
if (!res) return; if (!res) return;
@ -61,14 +72,19 @@ const Chat = () => {
})) }))
); );
scrollToBottom(); scrollToBottom();
setLoading(false);
}, },
onError() { onError(e: any) {
toast({ toast({
title: '初始化异常', title: e?.message || '初始化异常,请检查地址',
status: 'error' status: 'error',
isClosable: true,
duration: 5000
}); });
setLoading(false);
} }
}); }
);
// gpt3 方法 // gpt3 方法
const gpt3ChatPrompt = useCallback( const gpt3ChatPrompt = useCallback(
@ -107,8 +123,32 @@ const Chat = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const event = getChatGPTSendEvent(chatId, windowId); const event = getChatGPTSendEvent(chatId, windowId);
event.onmessage = ({ data }) => { // 30s 收不到消息就报错
if (data === '[DONE]') { let timer = setTimeout(() => {
event.close();
reject('服务器超时');
}, 300000);
event.addEventListener('responseData', ({ data }) => {
/* 重置定时器 */
clearTimeout(timer);
timer = setTimeout(() => {
event.close();
reject('服务器超时');
}, 300000);
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', () => {
clearTimeout(timer);
event.close(); event.close();
setChatList((state) => setChatList((state) =>
state.map((item, index) => { state.map((item, index) => {
@ -120,23 +160,18 @@ const Chat = () => {
}) })
); );
resolve(''); resolve('');
} else if (data) { });
const msg = data.replace(/<br\/>/g, '\n'); event.addEventListener('serviceError', ({ data: err }) => {
setChatList((state) => clearTimeout(timer);
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + msg
};
})
);
}
};
event.onerror = (err) => {
console.error(err, '===');
event.close(); event.close();
reject('对话出现错误'); console.error(err, '===');
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
});
event.onerror = (err) => {
clearTimeout(timer);
event.close();
console.error(err);
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
}; };
}); });
}, },
@ -179,8 +214,9 @@ const Chat = () => {
setTimeout(() => { setTimeout(() => {
scrollToBottom(); scrollToBottom();
/* 回到最小高度 */
if (TextareaDom.current) { if (TextareaDom.current) {
TextareaDom.current.style.height = 22 + 'px'; TextareaDom.current.style.height = textareaMinH;
} }
}, 100); }, 100);
@ -242,7 +278,7 @@ const Chat = () => {
}, [chatList, windowId]); }, [chatList, windowId]);
return ( return (
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}> <Flex height={'100%'} flexDirection={'column'}>
{/* 头部 */} {/* 头部 */}
<Flex <Flex
px={4} px={4}
@ -258,7 +294,6 @@ const Chat = () => {
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon> <Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
</Box> </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 ml={10} cursor={'pointer'} onClick={scrollToBottom}>
<Icon <Icon
@ -281,29 +316,44 @@ const Chat = () => {
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={30}
height={30} height={30}
></Image> />
</Box> </Box>
<Box flex={'1 0 0'} w={0} overflowX={'auto'}> <Box flex={'1 0 0'} w={0} overflowX={'auto'}>
{item.obj === 'AI' ? (
<Markdown <Markdown
source={item.value} source={item.value}
isChatting={isChatting && index === chatList.length - 1} isChatting={isChatting && index === chatList.length - 1}
/> />
) : (
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
)}
</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'}>
@ -349,12 +399,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,7 +432,6 @@ const Chat = () => {
</Box> </Box>
)} )}
</Box> </Box>
<Loading loading={!chatSiteData} />
</Flex> </Flex>
); );
}; };

View File

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

View File

@ -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,8 +59,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
); );
toast({ toast({
title: `密码已找回`, title: `密码已找回`,
status: 'success', status: 'success'
position: 'top'
}); });
} catch (error) { } catch (error) {
typeof error === 'string' && typeof error === 'string' &&

View File

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

View File

@ -1,15 +1,17 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback, useMemo } 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();
@ -24,23 +26,20 @@ const Login = () => {
[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'
},
[PageTypeEnum.forgetPassword]: {
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
}
}; };
const Component = TypeMap[type];
return <Component setPageType={setPageType} loginSuccess={loginSuccess} />;
}
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 +53,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 +75,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>

View File

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

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 { 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.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}>

View File

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

View File

@ -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,10 +41,8 @@ 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.error(err);
} }
setLoading(false); setLoading(false);
}, [modelId, setLoading]); }, [modelId, setLoading]);
@ -63,7 +63,7 @@ const ModelDetail = () => {
}); });
router.replace('/model/list'); router.replace('/model/list');
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, [setLoading, model, router, toast]); }, [setLoading, model, router, toast]);
@ -77,7 +77,7 @@ const ModelDetail = () => {
router.push(`/chat?chatId=${chatId}`); router.push(`/chat?chatId=${chatId}`);
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, [setLoading, model, router]); }, [setLoading, model, router]);
@ -105,7 +105,7 @@ const ModelDetail = () => {
title: typeof err === 'string' ? err : '文件格式错误', title: typeof err === 'string' ? err : '文件格式错误',
status: 'error' status: 'error'
}); });
console.log(err); console.error(err);
} }
setLoading(false); setLoading(false);
}, },
@ -121,22 +121,21 @@ const ModelDetail = () => {
await putModelTrainingStatus(model._id); await putModelTrainingStatus(model._id);
loadModel(); loadModel();
} catch (error) { } catch (error) {
console.log(error); console.error(error);
} }
setLoading(false); setLoading(false);
}, [setLoading, loadModel, model]); }, [setLoading, loadModel, model]);
return ( return (
<>
{!!model && (
<> <>
{/* 头部 */} {/* 头部 */}
<Card px={6} py={3}> <Card px={6} py={3}>
{isPc ? ( {isPc ? (
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}> <Box fontSize={'xl'} fontWeight={'bold'}>
{model.name} {model?.name || '模型'}
</Box> </Box>
{!!model && (
<Tag <Tag
ml={2} ml={2}
variant="solid" variant="solid"
@ -146,6 +145,7 @@ const ModelDetail = () => {
> >
{formatModelStatus[model.status].text} {formatModelStatus[model.status].text}
</Tag> </Tag>
)}
<Box flex={1} /> <Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}> <Button variant={'outline'} onClick={handlePreviewChat}>
@ -155,11 +155,13 @@ const ModelDetail = () => {
<> <>
<Flex alignItems={'center'}> <Flex alignItems={'center'}>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}> <Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{model.name} {model?.name || '模型'}
</Box> </Box>
{!!model && (
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}> <Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text} {formatModelStatus[model.status].text}
</Tag> </Tag>
)}
</Flex> </Flex>
<Box mt={4} textAlign={'right'}> <Box mt={4} textAlign={'right'}>
<Button variant={'outline'} onClick={handlePreviewChat}> <Button variant={'outline'} onClick={handlePreviewChat}>
@ -174,9 +176,9 @@ const ModelDetail = () => {
<ModelEditForm model={model} /> <ModelEditForm model={model} />
</Box> </Box>
{/* 其他配置 */} {/* 其他配置 */}
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}> <Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
<Training model={model} /> <Card p={4}>{!!model && <Training model={model} />}</Card>
<Card h={'100%'} p={4}> <Card p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}> <Box fontWeight={'bold'} fontSize={'lg'}>
</Box> </Box>
@ -234,8 +236,6 @@ const ModelDetail = () => {
</Flex> </Flex>
</Card> </Card>
</Grid> </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>

View File

@ -1,36 +1,32 @@
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';
const CreateModel = dynamic(() => import('./components/CreateModel'));
const ModelList = () => { const ModelList = () => {
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,7 +36,7 @@ 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);
@ -48,11 +44,11 @@ const ModelList = () => {
shallow: true shallow: true
}); });
} catch (err) { } catch (err) {
console.log(err); console.error(err);
} }
setLoading(false); setIsLoading(false);
}, },
[router, setLoading] [router, setIsLoading]
); );
return ( return (
@ -78,11 +74,11 @@ const ModelList = () => {
)} )}
</Box> </Box>
{/* 创建弹窗 */} {/* 创建弹窗 */}
<CreateModel {openCreateModel && (
isOpen={openCreateModel} <CreateModel setCreateModelOpen={setOpenCreateModel} onSuccess={createModelSuccess} />
setCreateModelOpen={setOpenCreateModel} )}
onSuccess={createModelSuccess}
/> <Loading loading={isLoading} />
</Box> </Box>
); );
}; };

View File

@ -8,7 +8,7 @@ export async function connectToDatabase() {
return cachedClient; return cachedClient;
} }
cachedClient = await mongoose.connect(process.env.MONGODB_UR as string, { cachedClient = await mongoose.connect(process.env.MONGODB_URI as string, {
dbName: 'doc_gpt' dbName: 'doc_gpt'
}); });

View File

@ -24,8 +24,8 @@ export const jsonRes = (
typeof error === 'string' typeof error === 'string'
? error ? error
: openaiError[error?.response?.data?.message] || error?.message || '请求错误'; : openaiError[error?.response?.data?.message] || error?.message || '请求错误';
console.error(error);
console.log(msg); console.error(msg);
} }
res.json({ res.json({

View File

@ -34,7 +34,7 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`)
}; };
mailTransport.sendMail(options, function (err, msg) { mailTransport.sendMail(options, function (err, msg) {
if (err) { if (err) {
console.log(err); console.error(err);
reject('邮箱异常'); reject('邮箱异常');
} else { } else {
resolve(''); resolve('');
@ -53,7 +53,7 @@ export const sendTrainSucceed = (email: string, modelName: string) => {
}; };
mailTransport.sendMail(options, function (err, msg) { mailTransport.sendMail(options, function (err, msg) {
if (err) { if (err) {
console.log(err); console.error(err);
reject('邮箱异常'); reject('邮箱异常');
} else { } else {
resolve(''); resolve('');

View File

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

View File

@ -8,20 +8,26 @@ 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);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
toast({ toast({
title, title,
status: 'success', status: 'success',
duration: 1000 duration: 1000
}); });
}) } catch (error) {
.catch((err) => { console.error(error);
console.log(err); toast({
title: '复制失败',
status: 'error'
}); });
} }
}
}; };
}; };