mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-27 16:33:49 +00:00
conflict
perf: 聊天页优化 perf: md解析样式 perf: ui调整 perf: 懒加载和动态加载优化 perf: 去除console, perf: 图片cdn feat: 图片地址 perf: 登录顺序 feat: 流优化
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { GET, POST, DELETE } from './request';
|
||||
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 获取一个聊天框的ID
|
||||
@@ -56,7 +57,7 @@ export const postChatGptPrompt = ({
|
||||
});
|
||||
/* 获取 Chat 的 Event 对象,进行持续通信 */
|
||||
export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
|
||||
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}`);
|
||||
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}&date=${Date.now()}`);
|
||||
|
||||
/**
|
||||
* 删除最后一句
|
||||
|
@@ -34,7 +34,7 @@ function responseSuccess(response: AxiosResponse<ResponseDataType>) {
|
||||
*/
|
||||
function checkRes(data: ResponseDataType) {
|
||||
if (data === undefined) {
|
||||
console.log(data, 'data is empty');
|
||||
console.error(data, 'data is empty');
|
||||
return Promise.reject('服务器异常');
|
||||
} else if (data.code < 200 || data.code >= 400) {
|
||||
return Promise.reject(data.message);
|
||||
@@ -49,21 +49,20 @@ function responseError(err: any) {
|
||||
console.error('请求错误', err);
|
||||
|
||||
if (!err) {
|
||||
return Promise.reject('未知错误');
|
||||
return Promise.reject({ message: '未知错误' });
|
||||
}
|
||||
if (typeof err === 'string') {
|
||||
return Promise.reject(err);
|
||||
return Promise.reject({ message: err });
|
||||
}
|
||||
if (err.response) {
|
||||
// 有报错响应
|
||||
const res = err.response;
|
||||
/* token过期,判断请求token与本地是否相同,若不同需要重发 */
|
||||
if (res.data.code in TOKEN_ERROR_CODE) {
|
||||
clearToken();
|
||||
return Promise.reject('token过期,重新登录');
|
||||
return Promise.reject({ message: 'token过期,重新登录' });
|
||||
}
|
||||
}
|
||||
return Promise.reject('未知错误');
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
/* 创建请求实例 */
|
||||
|
@@ -7,6 +7,7 @@ import { useGlobalStore } from '@/store/global';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const unAuthPage: { [key: string]: boolean } = {
|
||||
'/': true,
|
||||
'/login': true,
|
||||
'/chat': true
|
||||
};
|
||||
@@ -24,10 +25,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
useQuery(
|
||||
[router.pathname, userInfo],
|
||||
() => {
|
||||
setLoading(true);
|
||||
if (unAuthPage[router.pathname] === true || userInfo) {
|
||||
return setLoading(false);
|
||||
} else {
|
||||
setLoading(true);
|
||||
return getTokenLogin();
|
||||
}
|
||||
},
|
||||
@@ -38,7 +39,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
router.push('/login');
|
||||
toast();
|
||||
},
|
||||
@@ -48,7 +49,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
}
|
||||
);
|
||||
|
||||
return userInfo || unAuthPage[router.pathname] === true ? <>{children}</> : null;
|
||||
return userInfo || unAuthPage[router.pathname] === true ? children : null;
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
|
@@ -43,18 +43,16 @@ const navbarList = [
|
||||
const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
const { isPc } = useScreen();
|
||||
const router = useRouter();
|
||||
const { Loading } = useLoading({
|
||||
defaultLoading: true
|
||||
});
|
||||
const { Loading } = useLoading({ defaultLoading: true });
|
||||
const { loading } = useGlobalStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!unShowLayoutRoute[router.pathname] ? (
|
||||
<Box minHeight={'100vh'} backgroundColor={'gray.100'}>
|
||||
<Box h={'100%'} backgroundColor={'gray.100'} overflow={'auto'}>
|
||||
{isPc ? (
|
||||
<>
|
||||
<Box h={'100vh'} position={'fixed'} left={0} top={0} w={'80px'}>
|
||||
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'80px'}>
|
||||
<Navbar navbarList={navbarList} />
|
||||
</Box>
|
||||
<Box ml={'80px'} p={7}>
|
||||
|
@@ -3,7 +3,6 @@ import { Box, Flex } from '@chakra-ui/react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import Icon from '../Icon';
|
||||
import styles from './style.module.scss';
|
||||
|
||||
export enum NavbarTypeEnum {
|
||||
normal = 'normal',
|
||||
@@ -35,7 +34,7 @@ const Navbar = ({
|
||||
>
|
||||
{/* logo */}
|
||||
<Box pb={4}>
|
||||
<Image src={'/logo.svg'} width={50} height={100} alt=""></Image>
|
||||
<Image src={'/icon/logo.png'} width={'35'} height={'35'} alt=""></Image>
|
||||
</Box>
|
||||
{/* 导航列表 */}
|
||||
<Box flex={1}>
|
||||
@@ -47,6 +46,7 @@ const Navbar = ({
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
onClick={() =>
|
||||
!item.activeLink.includes(router.pathname) &&
|
||||
router.push(item.link, undefined, {
|
||||
shallow: true
|
||||
})
|
||||
|
@@ -45,15 +45,15 @@ const NavbarPhone = ({
|
||||
</Flex>
|
||||
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent maxWidth={'60vw'}>
|
||||
<DrawerContent maxWidth={'50vw'}>
|
||||
<DrawerBody p={4}>
|
||||
<Box pb={4}>
|
||||
<Image src={'/logo.svg'} w={'100%'} h={'70px'} pt={2} alt=""></Image>
|
||||
<Box py={4}>
|
||||
<Image src={'/icon/logo.png'} margin={'auto'} w={'35'} h={'35'} alt=""></Image>
|
||||
</Box>
|
||||
{navbarList.map((item) => (
|
||||
<Flex
|
||||
key={item.label}
|
||||
mb={4}
|
||||
mb={5}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
onClick={() => {
|
||||
@@ -61,8 +61,7 @@ const NavbarPhone = ({
|
||||
onClose();
|
||||
}}
|
||||
cursor={'pointer'}
|
||||
fontSize={'sm'}
|
||||
h={'65px'}
|
||||
h={'60px'}
|
||||
borderRadius={'md'}
|
||||
{...(item.activeLink.includes(router.pathname)
|
||||
? {
|
||||
|
@@ -27,96 +27,356 @@
|
||||
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 {
|
||||
/* 标题样式 */
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.83rem;
|
||||
}
|
||||
|
||||
/* 列表样式 */
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 1.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
ul {
|
||||
list-style: inside;
|
||||
}
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
color: #0077cc;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #0077cc;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #005580;
|
||||
border-bottom-color: #005580;
|
||||
}
|
||||
|
||||
/* 图片样式 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* 强调样式 */
|
||||
em,
|
||||
i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 代码样式 */
|
||||
code {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: justify;
|
||||
|
||||
pre {
|
||||
padding: 10px 15px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: #222 !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
border: none;
|
||||
background-color: #222;
|
||||
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 {
|
||||
line-height: 1.7;
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--chakra-colors-blue-600);
|
||||
}
|
||||
}
|
||||
|
@@ -1,33 +1,41 @@
|
||||
import React, { useMemo, memo } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import styles from './index.module.scss';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { codeLight } from './codeLight';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useCopyData } from '@/utils/tools';
|
||||
import Icon from '@/components/Icon';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
|
||||
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();
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={`${styles.markdown} ${
|
||||
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
|
||||
}`}
|
||||
rehypePlugins={[remarkGfm]}
|
||||
skipHtml={true}
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[remarkGfm, rehypeKatex]}
|
||||
components={{
|
||||
p: 'div',
|
||||
pre: 'div',
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const code = String(children).replace(/\n$/, '');
|
||||
|
||||
return (
|
||||
return !inline ? (
|
||||
<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>
|
||||
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
|
||||
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
|
||||
@@ -36,18 +44,23 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
|
||||
</Flex>
|
||||
<SyntaxHighlighter
|
||||
style={codeLight as any}
|
||||
showLineNumbers
|
||||
language={match?.[1]}
|
||||
PreTag="pre"
|
||||
{...props}
|
||||
>
|
||||
{code}
|
||||
</SyntaxHighlighter>
|
||||
</Box>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
}}
|
||||
linkTarget="_blank"
|
||||
>
|
||||
{source}
|
||||
{formatSource}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
@@ -6,8 +6,7 @@ export enum EmailTypeEnum {
|
||||
export const introPage = `
|
||||
## 欢迎使用 Doc GPT
|
||||
|
||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
||||
|
||||
时间比较赶,介绍没来得及完善,先直接上怎么使用:
|
||||
1. 使用邮箱注册账号。
|
||||
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
|
||||
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
|
||||
@@ -39,6 +38,5 @@ export const introPage = `
|
||||
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
|
||||
|
||||
### 其他问题
|
||||
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
|
||||

|
||||
还有其他问题,可以加我 wx: YNyiqi,拉个交流群大家一起聊聊。
|
||||
`;
|
||||
|
@@ -58,7 +58,11 @@ export const theme = extendTheme({
|
||||
global: {
|
||||
'html, body': {
|
||||
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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
@@ -17,45 +17,51 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
|
||||
const cancelCb = useRef<any>();
|
||||
|
||||
return {
|
||||
openConfirm: (confirm?: any, cancel?: any) => {
|
||||
onOpen();
|
||||
confirmCb.current = confirm;
|
||||
cancelCb.current = cancel;
|
||||
},
|
||||
ConfirmChild: () => (
|
||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{title}
|
||||
</AlertDialogHeader>
|
||||
openConfirm: useCallback(
|
||||
(confirm?: any, cancel?: any) => {
|
||||
onOpen();
|
||||
confirmCb.current = confirm;
|
||||
cancelCb.current = cancel;
|
||||
},
|
||||
[onOpen]
|
||||
),
|
||||
ConfirmChild: useCallback(
|
||||
() => (
|
||||
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
{title}
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>{content}</AlertDialogBody>
|
||||
<AlertDialogBody>{content}</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof cancelCb.current === 'function' && cancelCb.current();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
ml={3}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof confirmCb.current === 'function' && confirmCb.current();
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
<AlertDialogFooter>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof cancelCb.current === 'function' && cancelCb.current();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
ml={4}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
typeof confirmCb.current === 'function' && confirmCb.current();
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
),
|
||||
[content, isOpen, onClose, title]
|
||||
)
|
||||
};
|
||||
};
|
||||
|
@@ -1,32 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Spinner, Flex } from '@chakra-ui/react';
|
||||
|
||||
export const useLoading = (props?: { defaultLoading: boolean }) => {
|
||||
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
|
||||
|
||||
const Loading = ({
|
||||
loading,
|
||||
fixed = true
|
||||
}: {
|
||||
loading?: boolean;
|
||||
fixed?: boolean;
|
||||
}): JSX.Element | null => {
|
||||
return isLoading || loading ? (
|
||||
<Flex
|
||||
position={fixed ? 'fixed' : 'absolute'}
|
||||
zIndex={100}
|
||||
backgroundColor={'rgba(255,255,255,0.5)'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
|
||||
</Flex>
|
||||
) : null;
|
||||
};
|
||||
const Loading = useCallback(
|
||||
({ loading, fixed = true }: { loading?: boolean; fixed?: boolean }): JSX.Element | null => {
|
||||
return isLoading || loading ? (
|
||||
<Flex
|
||||
position={fixed ? 'fixed' : 'absolute'}
|
||||
zIndex={100}
|
||||
backgroundColor={'rgba(255,255,255,0.5)'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
|
||||
</Flex>
|
||||
) : null;
|
||||
},
|
||||
[isLoading]
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
@@ -11,6 +11,6 @@ export function useScreen() {
|
||||
isPc,
|
||||
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [isPc]),
|
||||
mediaMdSm: useMemo(() => (isPc ? 'md' : 'sm'), [isPc]),
|
||||
media: (pc: number | string, phone: number | string) => (isPc ? pc : phone)
|
||||
media: (pc: any, phone: any) => (isPc ? pc : phone)
|
||||
};
|
||||
}
|
||||
|
@@ -1,23 +1,32 @@
|
||||
import type { AppProps, NextWebVitalsMetric } from 'next/app';
|
||||
import Script from 'next/script';
|
||||
import Head from 'next/head';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import Layout from '@/components/Layout';
|
||||
import { theme } from '@/constants/theme';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import NProgress from 'nprogress'; //nprogress module
|
||||
import Router from 'next/router';
|
||||
import 'nprogress/nprogress.css';
|
||||
import '../styles/reset.scss';
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
cacheTime: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
//Binding events.
|
||||
Router.events.on('routeChangeStart', () => NProgress.start());
|
||||
Router.events.on('routeChangeComplete', () => NProgress.done());
|
||||
Router.events.on('routeChangeError', () => NProgress.done());
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
cacheTime: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -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;"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script src="/iconfont.js" async></script>
|
||||
</Head>
|
||||
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<Layout>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
|
||||
import { Readable } from 'stream';
|
||||
import { connectToDatabase, ChatWindow } from '@/service/mongo';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { getOpenAIApi, authChat } from '@/service/utils/chat';
|
||||
import { openaiProxy } from '@/service/utils/tools';
|
||||
@@ -9,12 +9,23 @@ import { ChatItemType } from '@/types/chat';
|
||||
|
||||
/* 发送提示词 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Encoding': 'none',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'text/event-stream'
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('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 };
|
||||
|
||||
try {
|
||||
@@ -47,9 +58,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
|
||||
(item: ChatItemType) => ({
|
||||
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
|
||||
const chatAPI = getOpenAIApi(userApiKey);
|
||||
@@ -68,43 +85,75 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const reg = /{"content"(.*)"}/g;
|
||||
// @ts-ignore
|
||||
const match = chatResponse.data.match(reg);
|
||||
if (!match) return;
|
||||
|
||||
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, {
|
||||
$push: {
|
||||
content: {
|
||||
obj: 'AI',
|
||||
value: AIResponse
|
||||
}
|
||||
},
|
||||
updateTime: Date.now()
|
||||
});
|
||||
|
||||
res.end();
|
||||
(async () => {
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$push: {
|
||||
content: {
|
||||
obj: 'AI',
|
||||
value: AIResponse
|
||||
}
|
||||
},
|
||||
updateTime: Date.now()
|
||||
});
|
||||
})();
|
||||
} catch (err: any) {
|
||||
console.log(err?.response?.data || err);
|
||||
// 删除最一条数据库记录, 也就是预发送的那一条
|
||||
await ChatWindow.findByIdAndUpdate(windowId, {
|
||||
$pop: { content: 1 },
|
||||
updateTime: Date.now()
|
||||
});
|
||||
let errorText = err;
|
||||
if (err.code === 'ECONNRESET') {
|
||||
errorText = '服务器代理出错';
|
||||
} else {
|
||||
switch (err?.response?.data?.error?.code) {
|
||||
case 'invalid_api_key':
|
||||
errorText = 'API-KEY不合法';
|
||||
break;
|
||||
case 'context_length_exceeded':
|
||||
errorText = '内容超长了,请重置对话';
|
||||
break;
|
||||
case 'rate_limit_reached':
|
||||
errorText = '同时访问用户过多,请稍后再试';
|
||||
break;
|
||||
case null:
|
||||
errorText = 'OpenAI 服务器访问超时';
|
||||
break;
|
||||
default:
|
||||
errorText = '服务器异常';
|
||||
}
|
||||
}
|
||||
console.error(errorText);
|
||||
responseData.push(`event: serviceError\ndata: ${errorText}\n\n`);
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
// 安全校验
|
||||
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
|
||||
if (!chat || chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
|
||||
throw new Error('聊天框已过期');
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
|
@@ -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);
|
||||
}
|
@@ -13,15 +13,19 @@ import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Icon from '@/components/Icon';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { OpenAiModelEnum } from '@/constants/model';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const Markdown = dynamic(() => import('@/components/Markdown'));
|
||||
|
||||
const textareaMinH = '22px';
|
||||
|
||||
const Chat = () => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { media } = useScreen();
|
||||
const { isPc, media } = useScreen();
|
||||
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
|
||||
const ChatBox = useRef<HTMLDivElement>(null);
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -32,7 +36,7 @@ const Chat = () => {
|
||||
|
||||
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
|
||||
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
|
||||
const { Loading } = useLoading();
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback(() => {
|
||||
@@ -47,28 +51,40 @@ const Chat = () => {
|
||||
}, []);
|
||||
|
||||
// 初始化聊天框
|
||||
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), {
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
|
||||
|
||||
setChatSiteData(res.chatSite);
|
||||
setChatList(
|
||||
res.history.map((item) => ({
|
||||
...item,
|
||||
status: 'finish'
|
||||
}))
|
||||
);
|
||||
scrollToBottom();
|
||||
useQuery(
|
||||
[chatId, windowId],
|
||||
() => {
|
||||
if (!chatId) return null;
|
||||
setLoading(true);
|
||||
return getInitChatSiteInfo(chatId, windowId);
|
||||
},
|
||||
onError() {
|
||||
toast({
|
||||
title: '初始化异常',
|
||||
status: 'error'
|
||||
});
|
||||
{
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
|
||||
|
||||
setChatSiteData(res.chatSite);
|
||||
setChatList(
|
||||
res.history.map((item) => ({
|
||||
...item,
|
||||
status: 'finish'
|
||||
}))
|
||||
);
|
||||
scrollToBottom();
|
||||
setLoading(false);
|
||||
},
|
||||
onError(e: any) {
|
||||
toast({
|
||||
title: e?.message || '初始化异常,请检查地址',
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
duration: 5000
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// gpt3 方法
|
||||
const gpt3ChatPrompt = useCallback(
|
||||
@@ -107,36 +123,55 @@ const Chat = () => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const event = getChatGPTSendEvent(chatId, windowId);
|
||||
event.onmessage = ({ data }) => {
|
||||
if (data === '[DONE]') {
|
||||
event.close();
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
resolve('');
|
||||
} else if (data) {
|
||||
const msg = data.replace(/<br\/>/g, '\n');
|
||||
setChatList((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + msg
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
event.onerror = (err) => {
|
||||
console.error(err, '===');
|
||||
// 30s 收不到消息就报错
|
||||
let timer = setTimeout(() => {
|
||||
event.close();
|
||||
reject('对话出现错误');
|
||||
reject('服务器超时');
|
||||
}, 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(() => {
|
||||
scrollToBottom();
|
||||
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height = 22 + 'px';
|
||||
TextareaDom.current.style.height = textareaMinH;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
@@ -242,7 +278,7 @@ const Chat = () => {
|
||||
}, [chatList, windowId]);
|
||||
|
||||
return (
|
||||
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}>
|
||||
<Flex height={'100%'} flexDirection={'column'}>
|
||||
{/* 头部 */}
|
||||
<Flex
|
||||
px={4}
|
||||
@@ -258,7 +294,6 @@ const Chat = () => {
|
||||
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
|
||||
</Box>
|
||||
{/* 滚动到底部按键 */}
|
||||
{/* 滚动到底部 */}
|
||||
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
|
||||
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
|
||||
<Icon
|
||||
@@ -281,29 +316,44 @@ const Chat = () => {
|
||||
borderBottom={'1px solid rgba(0,0,0,0.1)'}
|
||||
>
|
||||
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
|
||||
<Box mr={4}>
|
||||
<Box mr={media(4, 1)}>
|
||||
<Image
|
||||
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'}
|
||||
alt="/imgs/modelAvatar.png"
|
||||
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
|
||||
alt="/icon/logo.png"
|
||||
width={30}
|
||||
height={30}
|
||||
></Image>
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} w={0} overflowX={'auto'}>
|
||||
<Markdown
|
||||
source={item.value}
|
||||
isChatting={isChatting && index === chatList.length - 1}
|
||||
/>
|
||||
{item.obj === 'AI' ? (
|
||||
<Markdown
|
||||
source={item.value}
|
||||
isChatting={isChatting && index === chatList.length - 1}
|
||||
/>
|
||||
) : (
|
||||
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{/* 空内容提示 */}
|
||||
{/* {
|
||||
chatList.length === 0 && (
|
||||
<>
|
||||
<Card>
|
||||
内容太长
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
} */}
|
||||
<Box
|
||||
m={media('20px auto', '0 auto')}
|
||||
w={media('100vw', '100%')}
|
||||
maxW={'800px'}
|
||||
maxW={media('800px', 'auto')}
|
||||
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
|
||||
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
|
||||
>
|
||||
{lastWordHuman ? (
|
||||
<Box textAlign={'center'}>
|
||||
@@ -349,12 +399,12 @@ const Chat = () => {
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
setInputVal(textarea.value);
|
||||
|
||||
textarea.style.height = textarea.value.split('\n').length * 22 + 'px';
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
if (isPc && e.keyCode === 13 && !e.shiftKey) {
|
||||
sendPrompt();
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -382,7 +432,6 @@ const Chat = () => {
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Loading loading={!chatSiteData} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@@ -1,12 +1,9 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Card, Text, Box, Heading, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Card } from '@chakra-ui/react';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { introPage } from '@/constants/common';
|
||||
|
||||
const Home = () => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card p={5} lineHeight={2}>
|
||||
<Markdown source={introPage} isChatting={false} />
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import React, { useState, Dispatch, useCallback } from 'react';
|
||||
import {
|
||||
FormControl,
|
||||
Box,
|
||||
Input,
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
useToast,
|
||||
Flex
|
||||
} from '@chakra-ui/react';
|
||||
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { PageTypeEnum } from '../../../constants/user';
|
||||
import { postFindPassword } from '@/api/user';
|
||||
import { useSendCode } from '@/hooks/useSendCode';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
||||
interface Props {
|
||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||
@@ -28,7 +21,7 @@ interface RegisterType {
|
||||
}
|
||||
|
||||
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
const toast = useToast();
|
||||
const { toast } = useToast();
|
||||
const { mediaLgMd } = useScreen();
|
||||
const {
|
||||
register,
|
||||
@@ -66,8 +59,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
);
|
||||
toast({
|
||||
title: `密码已找回`,
|
||||
status: 'success',
|
||||
position: 'top'
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
typeof error === 'string' &&
|
||||
|
@@ -1,7 +1,5 @@
|
||||
.loginPage {
|
||||
background: url('/icon/login-bg.svg') no-repeat;
|
||||
background-size: cover;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
user-select: none;
|
||||
}
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import styles from './index.module.scss';
|
||||
import { Box, Flex, Image } from '@chakra-ui/react';
|
||||
import { PageTypeEnum } from '@/constants/user';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
import ForgetPasswordForm from './components/ForgetPasswordForm';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
const LoginForm = dynamic(() => import('./components/LoginForm'));
|
||||
const RegisterForm = dynamic(() => import('./components/RegisterForm'));
|
||||
const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm'));
|
||||
|
||||
const Login = () => {
|
||||
const router = useRouter();
|
||||
const { isPc } = useScreen();
|
||||
@@ -24,23 +26,20 @@ const Login = () => {
|
||||
[router, setUserInfo]
|
||||
);
|
||||
|
||||
const map = {
|
||||
[PageTypeEnum.login]: {
|
||||
Component: <LoginForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
||||
img: '/icon/loginLeft.svg'
|
||||
},
|
||||
[PageTypeEnum.register]: {
|
||||
Component: <RegisterForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
||||
img: '/icon/loginLeft.svg'
|
||||
},
|
||||
[PageTypeEnum.forgetPassword]: {
|
||||
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />,
|
||||
img: '/icon/loginLeft.svg'
|
||||
}
|
||||
};
|
||||
function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) {
|
||||
const TypeMap = {
|
||||
[PageTypeEnum.login]: LoginForm,
|
||||
[PageTypeEnum.register]: RegisterForm,
|
||||
[PageTypeEnum.forgetPassword]: ForgetPasswordForm
|
||||
};
|
||||
|
||||
const Component = TypeMap[type];
|
||||
|
||||
return <Component setPageType={setPageType} loginSuccess={loginSuccess} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={styles.loginPage} p={isPc ? '10vh 10vw' : 0}>
|
||||
<Box className={styles.loginPage} h={'100%'} p={isPc ? '10vh 10vw' : 0}>
|
||||
<Flex
|
||||
maxW={'1240px'}
|
||||
m={'auto'}
|
||||
@@ -54,7 +53,7 @@ const Login = () => {
|
||||
>
|
||||
{isPc && (
|
||||
<Image
|
||||
src={map[pageType].img}
|
||||
src={'/icon/loginLeft.svg'}
|
||||
order={pageType === PageTypeEnum.login ? 0 : 2}
|
||||
flex={'1 0 0'}
|
||||
w="0"
|
||||
@@ -76,7 +75,7 @@ const Login = () => {
|
||||
px={10}
|
||||
borderRadius={isPc ? 'md' : 'none'}
|
||||
>
|
||||
{map[pageType].Component}
|
||||
<DynamicComponent type={pageType} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
@@ -25,11 +25,9 @@ interface CreateFormType {
|
||||
}
|
||||
|
||||
const CreateModel = ({
|
||||
isOpen,
|
||||
setCreateModelOpen,
|
||||
onSuccess
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setCreateModelOpen: Dispatch<boolean>;
|
||||
onSuccess: Dispatch<ModelType>;
|
||||
}) => {
|
||||
@@ -72,7 +70,7 @@ const CreateModel = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={() => setCreateModelOpen(false)}>
|
||||
<Modal isOpen={true} onClose={() => setCreateModelOpen(false)}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>创建模型</ModalHeader>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { Grid, Box, Card, Flex, Button, FormControl, Input, Textarea } from '@chakra-ui/react';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -7,17 +7,17 @@ import { putModelById } from '@/api/model';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const ModelEditForm = ({ model }: { model: ModelType }) => {
|
||||
const ModelEditForm = ({ model }: { model?: ModelType }) => {
|
||||
const isInit = useRef(false);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors }
|
||||
} = useForm<ModelType>({
|
||||
defaultValues: model
|
||||
});
|
||||
} = useForm<ModelType>();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { toast } = useToast();
|
||||
const { isPc } = useScreen();
|
||||
const { media } = useScreen();
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: ModelType) => {
|
||||
@@ -34,7 +34,7 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
|
||||
status: 'success'
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
toast({
|
||||
title: err as string,
|
||||
status: 'success'
|
||||
@@ -61,8 +61,16 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
|
||||
});
|
||||
}, [errors, toast]);
|
||||
|
||||
/* model 只会改变一次 */
|
||||
useEffect(() => {
|
||||
if (model && !isInit.current) {
|
||||
reset(model);
|
||||
isInit.current = true;
|
||||
}
|
||||
}, [model, reset]);
|
||||
|
||||
return (
|
||||
<Grid gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
||||
<Grid gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||
<Card p={4}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
@@ -83,7 +91,7 @@ const ModelEditForm = ({ model }: { model: ModelType }) => {
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>对话模型:</Box>
|
||||
<Box>{model.service.modelName}</Box>
|
||||
<Box>{model?.service.modelName}</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { Box, Card, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
|
||||
import { Box, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
|
||||
import { ModelType } from '@/types/model';
|
||||
import { getModelTrainings } from '@/api/model';
|
||||
import type { TrainingItemType } from '@/types/training';
|
||||
@@ -29,7 +29,7 @@ const Training = ({ model }: { model: ModelType }) => {
|
||||
const res = await getModelTrainings(id);
|
||||
setRecords(res);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -38,7 +38,7 @@ const Training = ({ model }: { model: ModelType }) => {
|
||||
}, [loadTrainingRecords, model]);
|
||||
|
||||
return (
|
||||
<Card p={4} h={'100%'}>
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
训练记录: {model.trainingTimes}次
|
||||
</Box>
|
||||
@@ -63,7 +63,7 @@ const Training = ({ model }: { model: ModelType }) => {
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -11,12 +11,14 @@ import { useGlobalStore } from '@/store/global';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import ModelEditForm from './components/ModelEditForm';
|
||||
import Icon from '@/components/Icon';
|
||||
import Training from './components/Training';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Training = dynamic(() => import('./components/Training'));
|
||||
|
||||
const ModelDetail = () => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc } = useScreen();
|
||||
const { isPc, media } = useScreen();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { openConfirm, ConfirmChild } = useConfirm({
|
||||
content: '确认删除该模型?'
|
||||
@@ -39,10 +41,8 @@ const ModelDetail = () => {
|
||||
const res = await getModelById(modelId as string);
|
||||
res.security.expiredTime /= 60 * 60 * 1000;
|
||||
setModel(res);
|
||||
|
||||
console.log(res);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [modelId, setLoading]);
|
||||
@@ -63,7 +63,7 @@ const ModelDetail = () => {
|
||||
});
|
||||
router.replace('/model/list');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, model, router, toast]);
|
||||
@@ -77,7 +77,7 @@ const ModelDetail = () => {
|
||||
|
||||
router.push(`/chat?chatId=${chatId}`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, model, router]);
|
||||
@@ -105,7 +105,7 @@ const ModelDetail = () => {
|
||||
title: typeof err === 'string' ? err : '文件格式错误',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
@@ -121,121 +121,121 @@ const ModelDetail = () => {
|
||||
await putModelTrainingStatus(model._id);
|
||||
loadModel();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading, loadModel, model]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!model && (
|
||||
<>
|
||||
{/* 头部 */}
|
||||
<Card px={6} py={3}>
|
||||
{isPc ? (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
{model.name} 配置
|
||||
</Box>
|
||||
<Tag
|
||||
ml={2}
|
||||
variant="solid"
|
||||
colorScheme={formatModelStatus[model.status].colorTheme}
|
||||
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
|
||||
onClick={handleClickUpdateStatus}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<Card px={6} py={3}>
|
||||
{isPc ? (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'}>
|
||||
{model?.name || '模型'} 配置
|
||||
</Box>
|
||||
{!!model && (
|
||||
<Tag
|
||||
ml={2}
|
||||
variant="solid"
|
||||
colorScheme={formatModelStatus[model.status].colorTheme}
|
||||
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
|
||||
onClick={handleClickUpdateStatus}
|
||||
>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||
对话体验
|
||||
</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
{model?.name || '模型'} 配置
|
||||
</Box>
|
||||
{!!model && (
|
||||
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
<Box flex={1} />
|
||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||
对话体验
|
||||
</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
{model.name} 配置
|
||||
</Box>
|
||||
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
</Flex>
|
||||
<Box mt={4} textAlign={'right'}>
|
||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||
对话体验
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
{/* 基本信息编辑 */}
|
||||
<Box mt={5}>
|
||||
<ModelEditForm model={model} />
|
||||
)}
|
||||
</Flex>
|
||||
<Box mt={4} textAlign={'right'}>
|
||||
<Button variant={'outline'} onClick={handlePreviewChat}>
|
||||
对话体验
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
{/* 基本信息编辑 */}
|
||||
<Box mt={5}>
|
||||
<ModelEditForm model={model} />
|
||||
</Box>
|
||||
{/* 其他配置 */}
|
||||
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
|
||||
<Card p={4}>{!!model && <Training model={model} />}</Card>
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
神奇操作
|
||||
</Box>
|
||||
{/* 其他配置 */}
|
||||
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
||||
<Training model={model} />
|
||||
<Card h={'100%'} p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
神奇操作
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>模型微调:</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
SelectFileDom.current?.click();
|
||||
}}
|
||||
title={!canTrain ? '' : '模型不支持微调'}
|
||||
isDisabled={!canTrain}
|
||||
>
|
||||
上传微调数据集
|
||||
</Button>
|
||||
<Flex
|
||||
as={'a'}
|
||||
href="/TrainingTemplate.jsonl"
|
||||
download
|
||||
ml={5}
|
||||
cursor={'pointer'}
|
||||
alignItems={'center'}
|
||||
color={'blue.500'}
|
||||
>
|
||||
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
|
||||
下载模板
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* 提示 */}
|
||||
<Box mt={3} py={3} color={'blackAlpha.500'}>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
每行包括一个 prompt 和一个 completion
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
|
||||
内容不都是同一个标点结尾,可以加一个空格打断相同性,
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>删除模型:</Box>
|
||||
<Button
|
||||
colorScheme={'red'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
openConfirm(() => {
|
||||
handleDelModel();
|
||||
});
|
||||
}}
|
||||
>
|
||||
删除模型
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>模型微调:</Box>
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
SelectFileDom.current?.click();
|
||||
}}
|
||||
title={!canTrain ? '' : '模型不支持微调'}
|
||||
isDisabled={!canTrain}
|
||||
>
|
||||
上传微调数据集
|
||||
</Button>
|
||||
<Flex
|
||||
as={'a'}
|
||||
href="/TrainingTemplate.jsonl"
|
||||
download
|
||||
ml={5}
|
||||
cursor={'pointer'}
|
||||
alignItems={'center'}
|
||||
color={'blue.500'}
|
||||
>
|
||||
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
|
||||
下载模板
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* 提示 */}
|
||||
<Box mt={3} py={3} color={'blackAlpha.500'}>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
每行包括一个 prompt 和一个 completion
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
|
||||
内容不都是同一个标点结尾,可以加一个空格打断相同性,
|
||||
</Box>
|
||||
<Box as={'li'} lineHeight={1.9}>
|
||||
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>删除模型:</Box>
|
||||
<Button
|
||||
colorScheme={'red'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
openConfirm(() => {
|
||||
handleDelModel();
|
||||
});
|
||||
}}
|
||||
>
|
||||
删除模型
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
||||
</Box>
|
||||
|
@@ -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 { getMyModels } from '@/api/model';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import { ModelType } from '@/types/model';
|
||||
import CreateModel from './components/CreateModel';
|
||||
import { useRouter } from 'next/router';
|
||||
import ModelTable from './components/ModelTable';
|
||||
import ModelPhoneList from './components/ModelPhoneList';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const CreateModel = dynamic(() => import('./components/CreateModel'));
|
||||
|
||||
const ModelList = () => {
|
||||
const { isPc } = useScreen();
|
||||
const router = useRouter();
|
||||
const [models, setModels] = useState<ModelType[]>([]);
|
||||
const [openCreateModel, setOpenCreateModel] = useState(false);
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
|
||||
/* 加载模型 */
|
||||
const loadModels = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getMyModels();
|
||||
const { isLoading } = useQuery(['loadModels'], () => getMyModels(), {
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
setModels(res);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading]);
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
}, [loadModels]);
|
||||
});
|
||||
|
||||
/* 创建成功回调 */
|
||||
const createModelSuccess = useCallback((data: ModelType) => {
|
||||
@@ -40,7 +36,7 @@ const ModelList = () => {
|
||||
/* 点前往聊天预览页 */
|
||||
const handlePreviewChat = useCallback(
|
||||
async (modelId: string) => {
|
||||
setLoading(true);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const chatId = await getChatSiteId(modelId);
|
||||
|
||||
@@ -48,11 +44,11 @@ const ModelList = () => {
|
||||
shallow: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
setLoading(false);
|
||||
setIsLoading(false);
|
||||
},
|
||||
[router, setLoading]
|
||||
[router, setIsLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -78,11 +74,11 @@ const ModelList = () => {
|
||||
)}
|
||||
</Box>
|
||||
{/* 创建弹窗 */}
|
||||
<CreateModel
|
||||
isOpen={openCreateModel}
|
||||
setCreateModelOpen={setOpenCreateModel}
|
||||
onSuccess={createModelSuccess}
|
||||
/>
|
||||
{openCreateModel && (
|
||||
<CreateModel setCreateModelOpen={setOpenCreateModel} onSuccess={createModelSuccess} />
|
||||
)}
|
||||
|
||||
<Loading loading={isLoading} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@@ -8,7 +8,7 @@ export async function connectToDatabase() {
|
||||
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'
|
||||
});
|
||||
|
||||
|
@@ -24,8 +24,8 @@ export const jsonRes = (
|
||||
typeof error === 'string'
|
||||
? error
|
||||
: openaiError[error?.response?.data?.message] || error?.message || '请求错误';
|
||||
|
||||
console.log(msg);
|
||||
console.error(error);
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
@@ -34,7 +34,7 @@ export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`)
|
||||
};
|
||||
mailTransport.sendMail(options, function (err, msg) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
reject('邮箱异常');
|
||||
} else {
|
||||
resolve('');
|
||||
@@ -53,7 +53,7 @@ export const sendTrainSucceed = (email: string, modelName: string) => {
|
||||
};
|
||||
mailTransport.sendMail(options, function (err, msg) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
reject('邮箱异常');
|
||||
} else {
|
||||
resolve('');
|
||||
|
@@ -24,63 +24,9 @@ td,
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
body,
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: 12px/1.5tahoma, arial, \5b8b\4f53;
|
||||
}
|
||||
// h1, h2, h3, h4, h5, h6{ font-size:100%; }
|
||||
address,
|
||||
cite,
|
||||
dfn,
|
||||
em,
|
||||
var {
|
||||
font-style: normal;
|
||||
}
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: couriernew, courier, monospace;
|
||||
}
|
||||
small {
|
||||
font-size: 12px;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
sup {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
sub {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
legend {
|
||||
color: #000;
|
||||
}
|
||||
fieldset,
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 100%;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
|
||||
#__next {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar,
|
||||
|
@@ -8,19 +8,25 @@ export const useCopyData = () => {
|
||||
const { toast } = useToast();
|
||||
return {
|
||||
copyData: (data: string, title: string = '复制成功') => {
|
||||
const clipboardObj = navigator.clipboard;
|
||||
clipboardObj
|
||||
.writeText(data)
|
||||
.then(() => {
|
||||
toast({
|
||||
title,
|
||||
status: 'success',
|
||||
duration: 1000
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = data;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
toast({
|
||||
title,
|
||||
status: 'success',
|
||||
duration: 1000
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: '复制失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user