Files
FastGPT/src/pages/model/detail.tsx
2023-03-10 19:44:06 +08:00

252 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { useRouter } from 'next/router';
import { getModelById, delModelById, postTrainModel, putModelTrainingStatus } from '@/api/model';
import { getChatSiteId } from '@/api/chat';
import type { ModelType } from '@/types/model';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useConfirm } from '@/hooks/useConfirm';
import { formatModelStatus, ModelStatusEnum, OpenAiList } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import ModelEditForm from './components/ModelEditForm';
import Icon from '@/components/Icon';
import dynamic from 'next/dynamic';
const Training = dynamic(() => import('./components/Training'));
const ModelDetail = () => {
const { toast } = useToast();
const router = useRouter();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该模型?'
});
const SelectFileDom = useRef<HTMLInputElement>(null);
const { modelId } = router.query as { modelId: string };
const [model, setModel] = useState<ModelType>();
const canTrain = useMemo(() => {
const openai = OpenAiList.find((item) => item.model === model?.service.modelName);
return openai && openai.canTraining === true;
}, [model]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
if (!modelId) return;
setLoading(true);
try {
const res = await getModelById(modelId as string);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
} catch (err) {
console.error(err);
}
setLoading(false);
}, [modelId, setLoading]);
useEffect(() => {
loadModel();
}, [loadModel, modelId]);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!model) return;
setLoading(true);
try {
await delModelById(model._id);
toast({
title: '删除成功',
status: 'success'
});
router.replace('/model/list');
} catch (err) {
console.error(err);
}
setLoading(false);
}, [setLoading, model, router, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
if (!model) return;
setLoading(true);
try {
const chatId = await getChatSiteId(model._id);
router.push(`/chat?chatId=${chatId}`);
} catch (err) {
console.error(err);
}
setLoading(false);
}, [setLoading, model, router]);
/* 上传数据集,触发微调 */
const startTraining = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!modelId || !e.target.files || e.target.files?.length === 0) return;
setLoading(true);
try {
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
await postTrainModel(modelId, formData);
toast({
title: '开始训练,大约需要 30 分钟',
status: 'success'
});
// 重新获取模型
loadModel();
} catch (err) {
toast({
title: typeof err === 'string' ? err : '文件格式错误',
status: 'error'
});
console.error(err);
}
setLoading(false);
},
[setLoading, loadModel, modelId, toast]
);
/* 点击更新模型状态 */
const handleClickUpdateStatus = useCallback(async () => {
if (!model || model.status !== ModelStatusEnum.training) return;
setLoading(true);
try {
await putModelTrainingStatus(model._id);
loadModel();
} catch (error: any) {
console.error(error);
toast({
title: error.message || '更新失败',
status: 'error'
});
}
setLoading(false);
}, [model, setLoading, loadModel, toast]);
return (
<>
{/* 头部 */}
<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>
)}
</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>
<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>
<ConfirmChild />
</>
);
};
export default ModelDetail;