Commit 17364e9d by Archer

conflict

perf: 聊天页优化

perf: md解析样式

perf: ui调整

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

perf: 去除console,

perf: 图片cdn

feat: 图片地址

perf: 登录顺序

feat: 流优化
parent 23908232
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=
\ No newline at end of file
...@@ -34,6 +34,6 @@ yarn-error.log* ...@@ -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
\ No newline at end of file
{
"editor.formatOnType": true,
"editor.formatOnSave": true ,
"prettier.tabWidth": 2
}
\ No newline at end of file
...@@ -58,7 +58,7 @@ ENV PORT 3000 ...@@ -58,7 +58,7 @@ ENV PORT 3000
ENV MAX_USER '' ENV MAX_USER ''
ENV AXIOS_PROXY_HOST '' ENV AXIOS_PROXY_HOST ''
ENV AXIOS_PROXY_PORT '' ENV AXIOS_PROXY_PORT ''
ENV MONGODB_UR '' ENV MONGODB_URI ''
ENV MY_MAIL '' ENV MY_MAIL ''
ENV MAILE_CODE '' ENV MAILE_CODE ''
ENV TOKEN_KEY '' ENV TOKEN_KEY ''
......
...@@ -6,7 +6,7 @@ ...@@ -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 ...@@ -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, mongo,nginx和代理。 镜像走本机的代理,所以用 host,port改成代理的端口,clash一般都是7890。
docker pull imageName
```bash
# 服务器拉取部署, imageName 替换成镜像名
docker pull imageName:tag
# 获取本地旧镜像ID
OLD_IMAGE_ID=$(docker images imageName -f "dangling=true" -q)
docker stop doc-gpt || true docker stop doc-gpt || true
docker rm doc-gpt || true docker rm doc-gpt || true
# 运行时才把参数写入 docker run -d --network=host --name doc-gpt \
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 -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 ...@@ -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)
\ No newline at end of file \ No newline at end of file
...@@ -6,7 +6,17 @@ const isDev = process.env.NODE_ENV === 'development'; ...@@ -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;
...@@ -19,11 +19,10 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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"
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<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>
\ No newline at end of file
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 = ({ ...@@ -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()}`);
/** /**
* 删除最后一句 * 删除最后一句
......
...@@ -34,7 +34,7 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) { ...@@ -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) { ...@@ -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);
} }
/* 创建请求实例 */ /* 创建请求实例 */
......
...@@ -7,6 +7,7 @@ import { useGlobalStore } from '@/store/global'; ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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;
...@@ -43,18 +43,16 @@ const navbarList = [ ...@@ -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}>
......
...@@ -3,7 +3,6 @@ import { Box, Flex } from '@chakra-ui/react'; ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -47,6 +46,7 @@ const Navbar = ({
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
onClick={() => onClick={() =>
!item.activeLink.includes(router.pathname) &&
router.push(item.link, undefined, { router.push(item.link, undefined, {
shallow: true shallow: true
}) })
......
...@@ -45,15 +45,15 @@ const NavbarPhone = ({ ...@@ -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 = ({ ...@@ -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)
? { ? {
......
...@@ -27,96 +27,356 @@ ...@@ -27,96 +27,356 @@
opacity: 1; opacity: 1;
} }
} }
.markdown {
/* 标题样式 */
h1 {
font-size: 1.8rem;
}
h2 {
font-size: 1.6rem;
}
h3 {
font-size: 1.4rem;
}
h4 {
font-size: 1.2rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.83rem;
}
/* 列表样式 */
ol,
ul {
padding-left: 1.5rem;
margin-left: 1rem;
}
ul {
list-style: inside;
}
ol {
list-style: decimal;
}
/* 链接样式 */
a {
color: #0077cc;
text-decoration: none;
border-bottom: 1px solid #0077cc;
}
a:hover {
color: #005580;
border-bottom-color: #005580;
}
/* 图片样式 */
img {
max-width: 100%;
max-height: 200px;
margin: auto;
}
/* 强调样式 */
em,
i {
font-style: italic;
}
strong, .markdown > *:first-child {
b { margin-top: 0 !important;
font-weight: bold; }
} .markdown > *:last-child {
margin-bottom: 0 !important;
/* 代码样式 */ }
code { .markdown a.absent {
border-radius: 3px; color: #cc0000;
width: 100%; }
} .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 {
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);
} }
} }
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 ...@@ -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>
); );
}; };
......
...@@ -6,8 +6,7 @@ export enum EmailTypeEnum { ...@@ -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 = ` ...@@ -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)
`; `;
...@@ -58,7 +58,11 @@ export const theme = extendTheme({ ...@@ -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'
} }
} }
}, },
......
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 ...@@ -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]
) )
}; };
}; };
import { useState } from 'react'; import { useState, useCallback } from 'react';
import { Spinner, Flex } from '@chakra-ui/react'; import { Spinner, Flex } from '@chakra-ui/react';
export const useLoading = (props?: { defaultLoading: boolean }) => { export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false); const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = ({ const Loading = useCallback(
loading, ({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
fixed = true return isLoading || loading ? (
}: { <Flex
loading?: boolean; position={fixed ? 'fixed' : 'absolute'}
fixed?: boolean; zIndex={100}
}): JSX.Element | null => { backgroundColor={'rgba(255,255,255,0.5)'}
return isLoading || loading ? ( top={0}
<Flex left={0}
position={fixed ? 'fixed' : 'absolute'} right={0}
zIndex={100} bottom={0}
backgroundColor={'rgba(255,255,255,0.5)'} alignItems={'center'}
top={0} justifyContent={'center'}
left={0} >
right={0} <Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
bottom={0} </Flex>
alignItems={'center'} ) : null;
justifyContent={'center'} },
> [isLoading]
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" /> );
</Flex>
) : null;
};
return { return {
isLoading, isLoading,
......
...@@ -11,6 +11,6 @@ export function useScreen() { ...@@ -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)
}; };
} }
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, // Create a client
retry: false, const queryClient = new QueryClient({
cacheTime: 0 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) { ...@@ -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>
......
// 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'; ...@@ -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) ...@@ -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) ...@@ -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 内容
await ChatWindow.findByIdAndUpdate(windowId, { match.forEach((item: string, i: number) => {
$push: { try {
content: { const json = JSON.parse(item);
obj: 'AI', // 开头的换行忽略
value: AIResponse 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) {
updateTime: Date.now() err;
}
}); });
res.end(); responseData.push(`event: done\ndata: \n\n`);
// 存入库
(async () => {
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
})();
} 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);
} }
...@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) ...@@ -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) ...@@ -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
......
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);
}
...@@ -13,15 +13,19 @@ import { Textarea, Box, Flex, Button } from '@chakra-ui/react'; ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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();
reject('服务器超时');
}, 300000);
event.addEventListener('responseData', ({ data }) => {
/* 重置定时器 */
clearTimeout(timer);
timer = setTimeout(() => {
event.close(); event.close();
setChatList((state) => reject('服务器超时');
state.map((item, index) => { }, 300000);
if (index !== state.length - 1) return item;
return { const msg = data.replace(/<br\/>/g, '\n');
...item, setChatList((state) =>
status: 'finish' state.map((item, index) => {
}; if (index !== state.length - 1) return item;
}) return {
); ...item,
resolve(''); value: item.value + msg
} else if (data) { };
const msg = data.replace(/<br\/>/g, '\n'); })
setChatList((state) => );
state.map((item, index) => { });
if (index !== state.length - 1) return item; event.addEventListener('done', () => {
return { clearTimeout(timer);
...item, event.close();
value: item.value + msg setChatList((state) =>
}; state.map((item, index) => {
}) if (index !== state.length - 1) return item;
); return {
} ...item,
}; status: 'finish'
event.onerror = (err) => { };
})
);
resolve('');
});
event.addEventListener('serviceError', ({ data: err }) => {
clearTimeout(timer);
event.close();
console.error(err, '==='); console.error(err, '===');
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
});
event.onerror = (err) => {
clearTimeout(timer);
event.close(); event.close();
reject('对话出现错误'); console.error(err);
reject(typeof err === 'string' ? err : '对话出现不知名错误~');
}; };
}); });
}, },
...@@ -179,8 +214,9 @@ const Chat = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -382,7 +432,6 @@ const Chat = () => {
</Box> </Box>
)} )}
</Box> </Box>
<Loading loading={!chatSiteData} />
</Flex> </Flex>
); );
}; };
......
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} />
......
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 { ...@@ -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) => { ...@@ -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' &&
......
.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;
} }
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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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>
......
...@@ -25,11 +25,9 @@ interface CreateFormType { ...@@ -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 = ({ ...@@ -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>
......
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'; ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -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}>
......
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 }) => { ...@@ -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 }) => { ...@@ -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 }) => { ...@@ -63,7 +63,7 @@ const Training = ({ model }: { model: ModelType }) => {
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>
</Card> </>
); );
}; };
......
...@@ -11,12 +11,14 @@ import { useGlobalStore } from '@/store/global'; ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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>
......
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 = () => { ...@@ -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 = () => { ...@@ -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 = () => { ...@@ -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>
); );
}; };
......
...@@ -8,7 +8,7 @@ export async function connectToDatabase() { ...@@ -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'
}); });
......
...@@ -24,8 +24,8 @@ export const jsonRes = ( ...@@ -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({
......
...@@ -34,7 +34,7 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) ...@@ -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) => { ...@@ -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('');
......
...@@ -24,63 +24,9 @@ td, ...@@ -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,
......
...@@ -8,19 +8,25 @@ export const useCopyData = () => { ...@@ -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'
});
}
} }
}; };
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment