This commit is contained in:
archer
2023-03-30 22:33:58 +08:00
36 changed files with 1187 additions and 232 deletions

View File

@@ -0,0 +1,141 @@
import React, { useState, useCallback } from 'react';
import {
Box,
IconButton,
Flex,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
Input,
Textarea
} from '@chakra-ui/react';
import { useForm, useFieldArray } from 'react-hook-form';
import { postModelDataInput } from '@/api/model';
import { useToast } from '@/hooks/useToast';
import { DeleteIcon } from '@chakra-ui/icons';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
type FormData = { text: string; q: { val: string }[] };
const InputDataModal = ({
onClose,
onSuccess,
modelId
}: {
onClose: () => void;
onSuccess: () => void;
modelId: string;
}) => {
const [importing, setImporting] = useState(false);
const { toast } = useToast();
const { register, handleSubmit, control } = useForm<FormData>({
defaultValues: {
text: '',
q: [{ val: '' }]
}
});
const {
fields: inputQ,
append: appendQ,
remove: removeQ
} = useFieldArray({
control,
name: 'q'
});
const sureImportData = useCallback(
async (e: FormData) => {
setImporting(true);
try {
await postModelDataInput({
modelId: modelId,
data: [
{
text: e.text,
q: e.q.map((item) => ({
id: nanoid(),
text: item.val
}))
}
]
});
toast({
title: '导入数据成功,需要一段时间训练',
status: 'success'
});
onClose();
onSuccess();
} catch (err) {
console.log(err);
}
setImporting(false);
},
[modelId, onClose, onSuccess, toast]
);
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent maxW={'min(900px, 90vw)'} maxH={'80vh'} position={'relative'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<Box px={6} pb={2} overflowY={'auto'}>
<Box mb={2}>:</Box>
<Textarea
mb={4}
placeholder="知识点"
rows={3}
maxH={'200px'}
{...register(`text`, {
required: '知识点'
})}
/>
{inputQ.map((item, index) => (
<Box key={item.id} mb={5}>
<Box mb={2}>{index + 1}:</Box>
<Flex>
<Input
placeholder="问法"
{...register(`q.${index}.val`, {
required: '问法不能为空'
})}
></Input>
{inputQ.length > 1 && (
<IconButton
icon={<DeleteIcon />}
aria-label={'delete'}
colorScheme={'gray'}
variant={'unstyled'}
onClick={() => removeQ(index)}
/>
)}
</Flex>
</Box>
))}
</Box>
<Flex px={6} pt={2} pb={4}>
<Button alignSelf={'flex-start'} variant={'outline'} onClick={() => appendQ({ val: '' })}>
</Button>
<Box flex={1}></Box>
<Button variant={'outline'} mr={3} onClick={onClose}>
</Button>
<Button isLoading={importing} onClick={handleSubmit(sureImportData)}>
</Button>
</Flex>
</ModalContent>
</Modal>
);
};
export default InputDataModal;

View File

@@ -0,0 +1,202 @@
import React, { useCallback } from 'react';
import {
Box,
TableContainer,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
IconButton,
Flex,
Button,
useDisclosure,
Textarea,
Menu,
MenuButton,
MenuList,
MenuItem
} from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { ModelDataSchema } from '@/types/mongoSchema';
import { ModelDataStatusMap } from '@/constants/model';
import { usePaging } from '@/hooks/usePaging';
import ScrollData from '@/components/ScrollData';
import { getModelDataList, delOneModelData, putModelDataById } from '@/api/model';
import { DeleteIcon, RepeatIcon } from '@chakra-ui/icons';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import dynamic from 'next/dynamic';
const InputModel = dynamic(() => import('./InputDataModal'));
const SelectModel = dynamic(() => import('./SelectFileModal'));
const ModelDataCard = ({ model }: { model: ModelSchema }) => {
const { toast } = useToast();
const { Loading } = useLoading();
const {
nextPage,
isLoadAll,
requesting,
data: modelDataList,
total,
setData,
getData
} = usePaging<ModelDataSchema>({
api: getModelDataList,
pageSize: 20,
params: {
modelId: model._id
}
});
const updateAnswer = useCallback(
async (dataId: string, text: string) => {
await putModelDataById({
dataId,
text
});
toast({
title: '修改回答成功',
status: 'success'
});
},
[toast]
);
const {
isOpen: isOpenInputModal,
onOpen: onOpenInputModal,
onClose: onCloseInputModal
} = useDisclosure();
const {
isOpen: isOpenSelectModal,
onOpen: onOpenSelectModal,
onClose: onCloseSelectModal
} = useDisclosure();
return (
<>
<Flex>
<Box fontWeight={'bold'} fontSize={'lg'} flex={1}>
: {total}{' '}
<Box as={'span'} fontSize={'sm'}>
</Box>
</Box>
<IconButton
icon={<RepeatIcon />}
aria-label={'refresh'}
variant={'outline'}
mr={4}
onClick={() => getData(1, true)}
/>
<Menu>
<MenuButton as={Button}></MenuButton>
<MenuList>
<MenuItem onClick={onOpenInputModal}></MenuItem>
<MenuItem onClick={onOpenSelectModal}></MenuItem>
</MenuList>
</Menu>
</Flex>
<ScrollData
h={'100%'}
px={6}
mt={3}
isLoadAll={isLoadAll}
requesting={requesting}
nextPage={nextPage}
position={'relative'}
>
<TableContainer mt={4}>
<Table variant={'simple'}>
<Thead>
<Tr>
<Th>Question</Th>
<Th>Text</Th>
<Th>Status</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{modelDataList.map((item) => (
<Tr key={item._id}>
<Td w={'350px'}>
{item.q.map((item, i) => (
<Box
key={item.id}
fontSize={'xs'}
w={'100%'}
whiteSpace={'pre-wrap'}
_notLast={{ mb: 1 }}
>
Q{i + 1}:{' '}
<Box as={'span'} userSelect={'all'}>
{item.text}
</Box>
</Box>
))}
</Td>
<Td minW={'200px'}>
<Textarea
w={'100%'}
h={'100%'}
defaultValue={item.text}
fontSize={'xs'}
resize={'both'}
onBlur={(e) => {
const oldVal = modelDataList.find((data) => item._id === data._id)?.text;
if (oldVal !== e.target.value) {
updateAnswer(item._id, e.target.value);
setData((state) =>
state.map((data) => ({
...data,
text: data._id === item._id ? e.target.value : data.text
}))
);
}
}}
></Textarea>
</Td>
<Td w={'100px'}>{ModelDataStatusMap[item.status]}</Td>
<Td>
<IconButton
icon={<DeleteIcon />}
variant={'outline'}
colorScheme={'gray'}
aria-label={'delete'}
size={'sm'}
onClick={async () => {
delOneModelData(item._id);
setData((state) => state.filter((data) => data._id !== item._id));
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Loading loading={requesting} fixed={false} />
</ScrollData>
{isOpenInputModal && (
<InputModel
modelId={model._id}
onClose={onCloseInputModal}
onSuccess={() => getData(1, true)}
/>
)}
{isOpenSelectModal && (
<SelectModel
modelId={model._id}
onClose={onCloseSelectModal}
onSuccess={() => getData(1, true)}
/>
)}
</>
);
};
export default ModelDataCard;

View File

@@ -0,0 +1,263 @@
import React, { useState } from 'react';
import {
Box,
Card,
Flex,
FormControl,
Input,
Textarea,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Tooltip,
Button
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
import { UseFormReturn } from 'react-hook-form';
import { modelList } from '@/constants/model';
import { formatPrice } from '@/utils/user';
import { useConfirm } from '@/hooks/useConfirm';
const ModelEditForm = ({
formHooks,
canTrain,
handleDelModel
}: {
formHooks: UseFormReturn<ModelSchema>;
canTrain: boolean;
handleDelModel: () => void;
}) => {
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该模型?'
});
const { register, setValue, getValues } = formHooks;
const [refresh, setRefresh] = useState(false);
return (
<>
<Card p={4}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box fontWeight={'bold'}></Box>
</Flex>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Input
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Box>{getValues('service.modelName')}</Box>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 80px'} w={0}>
:
</Box>
<Box>
{formatPrice(
modelList.find((item) => item.model === getValues('service.modelName'))?.price || 0,
1000
)}
/1K tokens()
</Box>
</Flex>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Button
colorScheme={'gray'}
variant={'outline'}
size={'sm'}
onClick={openConfirm(handleDelModel)}
>
</Button>
</Flex>
{/* <FormControl mt={4}>
<Box mb={1}>介绍:</Box>
<Textarea
rows={5}
maxLength={500}
{...register('intro')}
placeholder={'模型的介绍,仅做展示,不影响模型的效果'}
/>
</FormControl> */}
</Card>
<Card p={4}>
<Box fontWeight={'bold'}></Box>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} w={0}>
<Box as={'span'} mr={2}>
</Box>
<Tooltip label={'温度越高,模型的发散能力越强;温度越低,内容越严谨。'}>
<QuestionOutlineIcon />
</Tooltip>
</Box>
<Slider
aria-label="slider-ex-1"
min={1}
max={10}
step={1}
value={getValues('temperature')}
onChange={(e) => {
setValue('temperature', e);
setRefresh(!refresh);
}}
>
<SliderMark
value={getValues('temperature')}
textAlign="center"
bg="blue.500"
color="white"
w={'18px'}
h={'18px'}
borderRadius={'100px'}
fontSize={'xs'}
transform={'translate(-50%, -200%)'}
>
{getValues('temperature')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
</Flex>
</FormControl>
<Box mt={4}>
{canTrain ? (
<Box fontWeight={'bold'}>
prompt
使 tokens
</Box>
) : (
<>
<Box mb={1}></Box>
<Textarea
rows={6}
maxLength={-1}
{...register('systemPrompt')}
placeholder={
'模型默认的 prompt 词,通过调整该内容,可以生成一个限定范围的模型。\n\n注意改功能会影响对话的整体朝向'
}
/>
</>
)}
</Box>
</Card>
{/* <Card p={4}>
<Box fontWeight={'bold'}>安全策略</Box>
<FormControl mt={2}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
单句最大长度:
</Box>
<Input
flex={1}
type={'number'}
{...register('security.contentMaxLen', {
required: '单句长度不能为空',
min: {
value: 0,
message: '单句长度最小为0'
},
max: {
value: 4000,
message: '单句长度最长为4000'
},
valueAsNumber: true
})}
></Input>
</Flex>
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
上下文最大长度:
</Box>
<Input
flex={1}
type={'number'}
{...register('security.contextMaxLen', {
required: '上下文长度不能为空',
min: {
value: 1,
message: '上下文长度最小为5'
},
max: {
value: 400000,
message: '上下文长度最长为 400000'
},
valueAsNumber: true
})}
></Input>
</Flex>
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'} w={0}>
聊天过期时间:
</Box>
<Input
flex={1}
type={'number'}
{...register('security.expiredTime', {
required: '聊天过期时间不能为空',
min: {
value: 0.1,
message: '聊天过期时间最小为0.1小时'
},
max: {
value: 999999,
message: '聊天过期时间最长为 999999 小时'
},
valueAsNumber: true
})}
></Input>
<Box ml={3}>小时</Box>
</Flex>
</FormControl>
<FormControl mt={5} pb={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 130px'} w={0}>
聊天最大加载次数:
</Box>
<Box flex={1}>
<Input
type={'number'}
{...register('security.maxLoadAmount', {
required: '聊天最大加载次数不能为空',
max: {
value: 999999,
message: '聊天最大加载次数最小为 999999 次'
},
valueAsNumber: true
})}
></Input>
<Box fontSize={'sm'} color={'blackAlpha.400'} position={'absolute'}>
设置为-1代表不限制次数
</Box>
</Box>
<Box ml={3}>次</Box>
</Flex>
</FormControl>
</Card> */}
<ConfirmChild />
</>
);
};
export default ModelEditForm;

View File

@@ -0,0 +1,155 @@
import React, { useState, useCallback } from 'react';
import {
Box,
Flex,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useSelectFile } from '@/hooks/useSelectFile';
import { customAlphabet } from 'nanoid';
import { encode } from 'gpt-token-utils';
import { useConfirm } from '@/hooks/useConfirm';
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/tools';
import { useMutation } from '@tanstack/react-query';
import { postModelDataFileText } from '@/api/model';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
const SelectFileModal = ({
onClose,
onSuccess,
modelId
}: {
onClose: () => void;
onSuccess: () => void;
modelId: string;
}) => {
const [selecting, setSelecting] = useState(false);
const { toast } = useToast();
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
const [fileText, setFileText] = useState('');
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认导入该文件,需要一定时间进行拆解,该任务无法终止!'
});
const onSelectFile = useCallback(
async (e: File[]) => {
setSelecting(true);
try {
const fileTexts = (
await Promise.all(
e.map((file) => {
// @ts-ignore
const extension = file?.name?.split('.').pop().toLowerCase();
switch (extension) {
case 'txt':
case 'md':
return readTxtContent(file);
case 'pdf':
return readPdfContent(file);
case 'doc':
case 'docx':
return readDocContent(file);
default:
return '';
}
})
)
)
.join('\n')
.replace(/\n+/g, '\n');
setFileText(fileTexts);
console.log(encode(fileTexts));
} catch (error: any) {
console.log(error);
toast({
title: typeof error === 'string' ? error : '解析文件失败',
status: 'error'
});
}
setSelecting(false);
},
[setSelecting, toast]
);
const { mutate, isLoading } = useMutation({
mutationFn: async () => {
if (!fileText) return;
await postModelDataFileText(modelId, fileText);
toast({
title: '导入数据成功,需要一段拆解和训练',
status: 'success'
});
onClose();
onSuccess();
},
onError() {
toast({
title: '导入文件失败',
status: 'error'
});
}
});
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent maxW={'min(900px, 90vw)'} position={'relative'}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex
flexDirection={'column'}
p={2}
h={'100%'}
alignItems={'center'}
justifyContent={'center'}
fontSize={'sm'}
>
<Button isLoading={selecting} onClick={onOpen}>
</Button>
<Box mt={2}> {fileExtension} . </Box>
<Box mt={2}>
{fileText.length} {encode(fileText).length} tokens
</Box>
<Box
h={'300px'}
w={'100%'}
overflow={'auto'}
p={2}
backgroundColor={'blackAlpha.50'}
whiteSpace={'pre'}
fontSize={'xs'}
>
{fileText}
</Box>
</Flex>
</ModalBody>
<Flex px={6} pt={2} pb={4}>
<Box flex={1}></Box>
<Button variant={'outline'} mr={3} onClick={onClose}>
</Button>
<Button isLoading={isLoading} isDisabled={fileText === ''} onClick={openConfirm(mutate)}>
</Button>
</Flex>
</ModalContent>
<ConfirmChild />
<File onSelect={onSelectFile} />
</Modal>
);
};
export default SelectFileModal;

View File

@@ -0,0 +1,70 @@
import React, { useEffect, useCallback, useState } from 'react';
import { Box, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { getModelTrainings } from '@/api/model';
import type { TrainingItemType } from '@/types/training';
const Training = ({ model }: { model: ModelSchema }) => {
const columns: {
title: string;
key: keyof TrainingItemType;
dataIndex: string;
}[] = [
{
title: '训练ID',
key: 'tuneId',
dataIndex: 'tuneId'
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
}
];
const [records, setRecords] = useState<TrainingItemType[]>([]);
const loadTrainingRecords = useCallback(async (id: string) => {
try {
const res = await getModelTrainings(id);
setRecords(res);
} catch (error) {
console.log('error->', error);
}
}, []);
useEffect(() => {
model._id && loadTrainingRecords(model._id);
}, [loadTrainingRecords, model]);
return (
<>
<Box fontWeight={'bold'} fontSize={'lg'}>
: {model.trainingTimes}
</Box>
<TableContainer mt={4}>
<Table variant={'simple'}>
<Thead>
<Tr>
{columns.map((item) => (
<Th key={item.key}>{item.title}</Th>
))}
</Tr>
</Thead>
<Tbody>
{records.map((item) => (
<Tr key={item._id}>
{columns.map((col) => (
// @ts-ignore
<Td key={col.key}>{item[col.dataIndex]}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</>
);
};
export default Training;

View File

@@ -0,0 +1,284 @@
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import { getModelById, delModelById, putModelTrainingStatus, putModelById } from '@/api/model';
import { getChatSiteId } from '@/api/chat';
import type { ModelSchema } from '@/types/mongoSchema';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useForm } from 'react-hook-form';
import { formatModelStatus, ModelStatusEnum, modelList, defaultModel } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import ModelEditForm from './components/ModelEditForm';
import { useQuery } from '@tanstack/react-query';
// import dynamic from 'next/dynamic';
import ModelDataCard from './components/ModelDataCard';
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
const ModelDetail = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore();
// const SelectFileDom = useRef<HTMLInputElement>(null);
const [model, setModel] = useState<ModelSchema>(defaultModel);
const formHooks = useForm<ModelSchema>({
defaultValues: model
});
const canTrain = useMemo(() => {
const openai = modelList.find((item) => item.model === model?.service.modelName);
return !!(openai && openai.trainName);
}, [model]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
setLoading(true);
try {
const res = await getModelById(modelId);
console.log(res);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
formHooks.reset(res);
} catch (err) {
console.log('error->', err);
}
setLoading(false);
return null;
}, [formHooks, modelId, setLoading]);
useQuery([modelId], loadModel);
/* 点击删除 */
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.log('error->', err);
}
setLoading(false);
}, [setLoading, model, router, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
setLoading(true);
try {
const chatId = await getChatSiteId(model._id);
router.push(`/chat?chatId=${chatId}`);
} catch (err) {
console.log('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: '开始训练...',
// status: 'success'
// });
// // 重新获取模型
// loadModel();
// } catch (err: any) {
// toast({
// title: err?.message || '上传文件失败',
// status: 'error'
// });
// console.log('error->', err);
// }
// setLoading(false);
// },
// [setLoading, loadModel, modelId, toast]
// );
/* 点击更新模型状态 */
const handleClickUpdateStatus = useCallback(async () => {
if (!model || model.status !== ModelStatusEnum.training) return;
setLoading(true);
try {
const res = await putModelTrainingStatus(model._id);
typeof res === 'string' &&
toast({
title: res,
status: 'info'
});
loadModel();
} catch (error: any) {
console.log('error->', error);
toast({
title: error.message || '更新失败',
status: 'error'
});
}
setLoading(false);
}, [model, setLoading, loadModel, toast]);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: ModelSchema) => {
setLoading(true);
try {
await putModelById(data._id, {
name: data.name,
systemPrompt: data.systemPrompt,
intro: data.intro,
temperature: data.temperature,
service: data.service,
security: data.security
});
toast({
title: '更新成功',
status: 'success'
});
} catch (err) {
console.log('error->', err);
toast({
title: err as string,
status: 'success'
});
}
setLoading(false);
},
[setLoading, toast]
);
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return '提交表单错误';
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(formHooks.formState.errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [formHooks.formState.errors, toast]);
useEffect(() => {
router.prefetch('/chat');
window.onbeforeunload = (e) => {
e.preventDefault();
e.returnValue = '内容已修改,确认离开页面吗?';
};
return () => {
window.onbeforeunload = null;
};
}, [router]);
return (
<>
{/* 头部 */}
<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}
>
{formatModelStatus[model.status].text}
</Tag>
<Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</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>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
</Box>
</>
)}
</Card>
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} canTrain={canTrain} />
{/* {canTrain && (
<Card p={4}>
<Training model={model} />
</Card>
)} */}
{canTrain && model._id && (
<Card
p={4}
height={'700px'}
{...media(
{
gridColumnStart: 1,
gridColumnEnd: 3
},
{}
)}
>
<ModelDataCard model={model} />
</Card>
)}
</Grid>
{/* 文件选择 */}
{/* <Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
</Box> */}
</>
);
};
export default ModelDetail;
export async function getServerSideProps(context: any) {
const modelId = context.query?.modelId || '';
return {
props: { modelId }
};
}