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 > *: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 {
border: 1px solid #cccccc;
padding: 6px 13px;
}
.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 { .markdown {
/* 标题样式 */ font-size: 14px;
h1 { line-height: 1.6;
font-size: 1.8rem; letter-spacing: 0.5px;
} text-align: justify;
h2 {
font-size: 1.6rem;
}
h3 {
font-size: 1.4rem;
}
h4 {
font-size: 1.2rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.83rem;
}
/* 列表样式 */
ol,
ul {
padding-left: 1.5rem;
margin-left: 1rem;
}
ul {
list-style: inside;
}
ol {
list-style: decimal;
}
/* 链接样式 */
a {
color: #0077cc;
text-decoration: none;
border-bottom: 1px solid #0077cc;
}
a:hover {
color: #005580;
border-bottom-color: #005580;
}
/* 图片样式 */
img {
max-width: 100%;
max-height: 200px;
margin: auto;
}
/* 强调样式 */
em,
i {
font-style: italic;
}
strong,
b {
font-weight: bold;
}
/* 代码样式 */
code {
border-radius: 3px;
width: 100%;
}
pre { pre {
padding: 10px 15px; display: block;
width: 100%; width: 100%;
padding: 15px;
margin: 0;
border: none;
border-radius: 0;
background-color: #222 !important; background-color: #222 !important;
overflow-x: auto; overflow-x: auto;
} }
pre code { pre code {
display: block;
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,45 +17,51 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
const cancelCb = useRef<any>(); const cancelCb = useRef<any>();
return { return {
openConfirm: (confirm?: any, cancel?: any) => { openConfirm: useCallback(
onOpen(); (confirm?: any, cancel?: any) => {
confirmCb.current = confirm; onOpen();
cancelCb.current = cancel; confirmCb.current = confirm;
}, cancelCb.current = cancel;
ConfirmChild: () => ( },
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}> [onOpen]
<AlertDialogOverlay> ),
<AlertDialogContent> ConfirmChild: useCallback(
<AlertDialogHeader fontSize="lg" fontWeight="bold"> () => (
{title} <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
</AlertDialogHeader> <AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{content}</AlertDialogBody> <AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogFooter> <AlertDialogFooter>
<Button <Button
colorScheme={'gray'} colorScheme={'gray'}
onClick={() => { onClick={() => {
onClose(); onClose();
typeof cancelCb.current === 'function' && cancelCb.current(); typeof cancelCb.current === 'function' && cancelCb.current();
}} }}
> >
</Button> </Button>
<Button <Button
colorScheme="blue" colorScheme="blue"
ml={3} ml={4}
onClick={() => { onClick={() => {
onClose(); onClose();
typeof confirmCb.current === 'function' && confirmCb.current(); typeof confirmCb.current === 'function' && confirmCb.current();
}} }}
> >
</Button> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialogOverlay> </AlertDialogOverlay>
</AlertDialog> </AlertDialog>
),
[content, isOpen, onClose, title]
) )
}; };
}; };

View File

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

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,23 +1,32 @@
import type { AppProps, NextWebVitalsMetric } from 'next/app'; import type { AppProps, NextWebVitalsMetric } from 'next/app';
import Script from 'next/script';
import Head from 'next/head'; import Head from 'next/head';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import Layout from '@/components/Layout'; import Layout from '@/components/Layout';
import { theme } from '@/constants/theme'; import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router';
import 'nprogress/nprogress.css';
import '../styles/reset.scss'; import '../styles/reset.scss';
export default function App({ Component, pageProps }: AppProps) { //Binding events.
// Create a client Router.events.on('routeChangeStart', () => NProgress.start());
const queryClient = new QueryClient({ Router.events.on('routeChangeComplete', () => NProgress.done());
defaultOptions: { Router.events.on('routeChangeError', () => NProgress.done());
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
}
});
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
}
});
export default function App({ Component, pageProps }: AppProps) {
return ( return (
<> <>
<Head> <Head>
@ -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,43 +85,75 @@ 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) {
match.forEach((item: string, i: number) => {
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;
}
});
}
res.write(`data: [DONE]\n\n`);
// 循环给 stream push 内容
match.forEach((item: string, i: number) => {
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
if (content) {
responseData.push(`event: responseData\ndata: ${content}\n\n`);
// res.write(`event: responseData\n`)
// res.write(`data: ${content}\n\n`)
}
} catch (err) {
err;
}
});
responseData.push(`event: done\ndata: \n\n`);
// 存入库 // 存入库
await ChatWindow.findByIdAndUpdate(windowId, { (async () => {
$push: { await ChatWindow.findByIdAndUpdate(windowId, {
content: { $push: {
obj: 'AI', content: {
value: AIResponse obj: 'AI',
} value: AIResponse
}, }
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') {
await ChatWindow.findByIdAndUpdate(windowId, { errorText = '服务器代理出错';
$pop: { content: 1 }, } else {
updateTime: Date.now() 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`);
res.end(); // 删除最一条数据库记录, 也就是预发送的那一条
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
});
})();
} }
// 开启 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,28 +51,40 @@ const Chat = () => {
}, []); }, []);
// 初始化聊天框 // 初始化聊天框
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), { useQuery(
cacheTime: 5 * 60 * 1000, [chatId, windowId],
onSuccess(res) { () => {
if (!res) return; if (!chatId) return null;
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`); setLoading(true);
return getInitChatSiteInfo(chatId, windowId);
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item,
status: 'finish'
}))
);
scrollToBottom();
}, },
onError() { {
toast({ cacheTime: 5 * 60 * 1000,
title: '初始化异常', onSuccess(res) {
status: 'error' if (!res) return;
}); router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item,
status: 'finish'
}))
);
scrollToBottom();
setLoading(false);
},
onError(e: any) {
toast({
title: e?.message || '初始化异常,请检查地址',
status: 'error',
isClosable: true,
duration: 5000
});
setLoading(false);
}
} }
}); );
// gpt3 方法 // gpt3 方法
const gpt3ChatPrompt = useCallback( const gpt3ChatPrompt = useCallback(
@ -107,36 +123,55 @@ 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();
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, '===');
event.close(); event.close();
reject('对话出现错误'); 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();
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.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'}>
<Markdown {item.obj === 'AI' ? (
source={item.value} <Markdown
isChatting={isChatting && index === chatList.length - 1} source={item.value}
/> 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' const Component = TypeMap[type];
},
[PageTypeEnum.forgetPassword]: { return <Component setPageType={setPageType} loginSuccess={loginSuccess} />;
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />, }
img: '/icon/loginLeft.svg'
}
};
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,121 +121,121 @@ 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}>
{/* 头部 */} {isPc ? (
<Card px={6} py={3}> <Flex alignItems={'center'}>
{isPc ? ( <Box fontSize={'xl'} fontWeight={'bold'}>
<Flex alignItems={'center'}> {model?.name || '模型'}
<Box fontSize={'xl'} fontWeight={'bold'}> </Box>
{model.name} {!!model && (
</Box> <Tag
<Tag ml={2}
ml={2} variant="solid"
variant="solid" colorScheme={formatModelStatus[model.status].colorTheme}
colorScheme={formatModelStatus[model.status].colorTheme} cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'} onClick={handleClickUpdateStatus}
onClick={handleClickUpdateStatus} >
> {formatModelStatus[model.status].text}
</Tag>
)}
<Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
</Flex>
) : (
<>
<Flex alignItems={'center'}>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{model?.name || '模型'}
</Box>
{!!model && (
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text} {formatModelStatus[model.status].text}
</Tag> </Tag>
<Box flex={1} /> )}
<Button variant={'outline'} onClick={handlePreviewChat}> </Flex>
<Box mt={4} textAlign={'right'}>
</Button> <Button variant={'outline'} onClick={handlePreviewChat}>
</Flex>
) : ( </Button>
<> </Box>
<Flex alignItems={'center'}> </>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}> )}
{model.name} </Card>
</Box> {/* 基本信息编辑 */}
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}> <Box mt={5}>
{formatModelStatus[model.status].text} <ModelEditForm model={model} />
</Tag> </Box>
</Flex> {/* 其他配置 */}
<Box mt={4} textAlign={'right'}> <Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
<Button variant={'outline'} onClick={handlePreviewChat}> <Card p={4}>{!!model && <Training model={model} />}</Card>
<Card p={4}>
</Button> <Box fontWeight={'bold'} fontSize={'lg'}>
</Box>
</>
)}
</Card>
{/* 基本信息编辑 */}
<Box mt={5}>
<ModelEditForm model={model} />
</Box> </Box>
{/* 其他配置 */} <Flex mt={5} alignItems={'center'}>
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}> <Box flex={'0 0 80px'}>:</Box>
<Training model={model} /> <Button
<Card h={'100%'} p={4}> size={'sm'}
<Box fontWeight={'bold'} fontSize={'lg'}> onClick={() => {
SelectFileDom.current?.click();
</Box> }}
<Flex mt={5} alignItems={'center'}> title={!canTrain ? '' : '模型不支持微调'}
<Box flex={'0 0 80px'}>:</Box> isDisabled={!canTrain}
<Button >
size={'sm'}
onClick={() => { </Button>
SelectFileDom.current?.click(); <Flex
}} as={'a'}
title={!canTrain ? '' : '模型不支持微调'} href="/TrainingTemplate.jsonl"
isDisabled={!canTrain} download
> ml={5}
cursor={'pointer'}
</Button> alignItems={'center'}
<Flex color={'blue.500'}
as={'a'} >
href="/TrainingTemplate.jsonl" <Icon name={'icon-yunxiazai'} color={'#3182ce'} />
download
ml={5} </Flex>
cursor={'pointer'} </Flex>
alignItems={'center'} {/* 提示 */}
color={'blue.500'} <Box mt={3} py={3} color={'blackAlpha.500'}>
> <Box as={'li'} lineHeight={1.9}>
<Icon name={'icon-yunxiazai'} color={'#3182ce'} /> prompt completion
</Box>
</Flex> <Box as={'li'} lineHeight={1.9}>
</Flex> prompt \n\n###\n\n prompt
{/* 提示 */}
<Box mt={3} py={3} color={'blackAlpha.500'}> </Box>
<Box as={'li'} lineHeight={1.9}> <Box as={'li'} lineHeight={1.9}>
prompt completion completion ###
</Box> </Box>
<Box as={'li'} lineHeight={1.9}> </Box>
prompt \n\n###\n\n prompt <Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
</Box> <Button
<Box as={'li'} lineHeight={1.9}> colorScheme={'red'}
completion ### size={'sm'}
</Box> onClick={() => {
</Box> openConfirm(() => {
<Flex mt={5} alignItems={'center'}> handleDelModel();
<Box flex={'0 0 80px'}>:</Box> });
<Button }}
colorScheme={'red'} >
size={'sm'}
onClick={() => { </Button>
openConfirm(() => { </Flex>
handleDelModel(); </Card>
}); </Grid>
}}
>
</Button>
</Flex>
</Card>
</Grid>
</>
)}
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}> <Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} /> <input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
</Box> </Box>

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