perf: 聊天页优化

perf: md解析样式

perf: ui调整

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

perf: 去除console,

perf: 图片cdn

feat: 图片地址

perf: 登录顺序

feat: 流优化
This commit is contained in:
Archer
2023-03-09 20:44:13 +08:00
parent 2390823282
commit 17364e9da3
47 changed files with 1384 additions and 852 deletions

View File

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

View File

@@ -1,6 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { 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);
}

View File

@@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
// 安全校验
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
if (!chat || chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
throw new Error('聊天框已过期');
}
@@ -82,7 +82,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err

View File

@@ -1,24 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (req.method !== 'GET') return;
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Encoding': 'none',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
});
let val = 0;
const timer = setInterval(() => {
console.log('发送消息', val);
res.write(`data: ${val++}\n\n`);
if (val > 30) {
clearInterval(timer);
res.write(`data: [DONE]\n\n`);
res.end();
}
}, 500);
}

View File

@@ -13,15 +13,19 @@ import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import 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>
);
};

View File

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

View File

@@ -1,19 +1,12 @@
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { FormControl, Box, Input, Button, FormErrorMessage, Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '../../../constants/user';
import { postFindPassword } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
import { useToast } from '@/hooks/useToast';
interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>;
@@ -28,7 +21,7 @@ interface RegisterType {
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
@@ -66,8 +59,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
);
toast({
title: `密码已找回`,
status: 'success',
position: 'top'
status: 'success'
});
} catch (error) {
typeof error === 'string' &&

View File

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

View File

@@ -1,15 +1,17 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, 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>

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React, { 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>
</>
);
};

View File

@@ -11,12 +11,14 @@ import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import ModelEditForm from './components/ModelEditForm';
import Icon from '@/components/Icon';
import Training from './components/Training';
import dynamic from 'next/dynamic';
const Training = dynamic(() => import('./components/Training'));
const ModelDetail = () => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该模型?'
@@ -39,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>

View File

@@ -1,36 +1,32 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useCallback } from 'react';
import { Box, Button, Flex, Card } from '@chakra-ui/react';
import { 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>
);
};