mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 09:44:47 +00:00
doc gpt V0.2
This commit is contained in:
126
src/pages/model/components/CreateModel.tsx
Normal file
126
src/pages/model/components/CreateModel.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { Dispatch, useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
Button,
|
||||
useToast,
|
||||
Input,
|
||||
Select
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postCreateModel } from '@/api/model';
|
||||
import { ModelType } from '@/types/model';
|
||||
import { OpenAiList } from '@/constants/model';
|
||||
|
||||
interface CreateFormType {
|
||||
name: string;
|
||||
serviceModelName: string;
|
||||
}
|
||||
|
||||
const CreateModel = ({
|
||||
isOpen,
|
||||
setCreateModelOpen,
|
||||
onSuccess
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
setCreateModelOpen: Dispatch<boolean>;
|
||||
onSuccess: Dispatch<ModelType>;
|
||||
}) => {
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
const toast = useToast({
|
||||
duration: 2000,
|
||||
position: 'top'
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<CreateFormType>({
|
||||
defaultValues: {
|
||||
serviceModelName: OpenAiList[0].model
|
||||
}
|
||||
});
|
||||
|
||||
const handleCreateModel = useCallback(
|
||||
async (data: CreateFormType) => {
|
||||
setRequesting(true);
|
||||
try {
|
||||
const res = await postCreateModel(data);
|
||||
toast({
|
||||
title: '创建成功',
|
||||
status: 'success'
|
||||
});
|
||||
onSuccess(res);
|
||||
setCreateModelOpen(false);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err.message || '出现了意外',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setRequesting(false);
|
||||
},
|
||||
[onSuccess, setCreateModelOpen, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={() => setCreateModelOpen(false)}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>创建模型</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<ModalBody>
|
||||
<FormControl mb={8} isInvalid={!!errors.name}>
|
||||
<Input
|
||||
placeholder="模型名称"
|
||||
{...register('name', {
|
||||
required: '模型名不能为空'
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.name && errors.name.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={!!errors.serviceModelName}>
|
||||
<Select
|
||||
placeholder="选择基础模型类型"
|
||||
{...register('serviceModelName', {
|
||||
required: '底层模型不能为空'
|
||||
})}
|
||||
>
|
||||
{OpenAiList.map((item) => (
|
||||
<option key={item.model} value={item.model}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormErrorMessage position={'absolute'} fontSize="xs">
|
||||
{!!errors.serviceModelName && errors.serviceModelName.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button mr={3} colorScheme={'gray'} onClick={() => setCreateModelOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={requesting} onClick={handleSubmit(handleCreateModel)}>
|
||||
确认创建
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateModel;
|
193
src/pages/model/components/ModelEditForm.tsx
Normal file
193
src/pages/model/components/ModelEditForm.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useCallback } 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';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { putModelById } from '@/api/model';
|
||||
import { useScreen } from '@/hooks/useScreen';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
const ModelEditForm = ({ model }: { model: ModelType }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<ModelType>({
|
||||
defaultValues: model
|
||||
});
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { toast } = useToast();
|
||||
const { isPc } = useScreen();
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: ModelType) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await putModelById(data._id, {
|
||||
name: data.name,
|
||||
systemPrompt: data.systemPrompt,
|
||||
service: data.service,
|
||||
security: data.security
|
||||
});
|
||||
toast({
|
||||
title: '更新成功',
|
||||
status: 'success'
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
toast({
|
||||
title: err as string,
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[setLoading, toast]
|
||||
);
|
||||
const submitError = 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(errors),
|
||||
status: 'error',
|
||||
duration: 4000,
|
||||
isClosable: true
|
||||
});
|
||||
}, [errors, toast]);
|
||||
|
||||
return (
|
||||
<Grid gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
|
||||
<Card p={4}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
修改模型信息
|
||||
</Box>
|
||||
<Button onClick={handleSubmit(onclickSave, submitError)}>保存</Button>
|
||||
</Flex>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>展示名称:</Box>
|
||||
<Input
|
||||
{...register('name', {
|
||||
required: '展示名称不能为空'
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>对话模型:</Box>
|
||||
<Box>{model.service.modelName}</Box>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl mt={5}>
|
||||
<Textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
{...register('systemPrompt')}
|
||||
placeholder={'系统的提示词,会在进入聊天时放置在第一句,用于限定模型的聊天范围'}
|
||||
/>
|
||||
</FormControl>
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Box fontWeight={'bold'} fontSize={'lg'}>
|
||||
安全策略
|
||||
</Box>
|
||||
<FormControl mt={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 120px'}>单句最大长度:</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'}>上下文最大长度:</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'}>聊天过期时间:</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'}>聊天最大加载次数:</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>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelEditForm;
|
76
src/pages/model/components/ModelPhoneList.tsx
Normal file
76
src/pages/model/components/ModelPhoneList.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Flex, Heading, Tag } from '@chakra-ui/react';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { formatModelStatus } from '@/constants/model';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ModelPhoneList = ({
|
||||
models,
|
||||
handlePreviewChat
|
||||
}: {
|
||||
models: ModelType[];
|
||||
handlePreviewChat: (_: string) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
|
||||
{models.map((model) => (
|
||||
<Box
|
||||
key={model._id}
|
||||
_notFirst={{ borderTop: '1px solid rgba(0,0,0,0.1)' }}
|
||||
px={6}
|
||||
py={3}
|
||||
backgroundColor={'white'}
|
||||
>
|
||||
<Flex alignItems={'flex-start'}>
|
||||
<Box flex={'1 0 0'} w={0} fontSize={'lg'} fontWeight={'bold'}>
|
||||
{model.name}
|
||||
</Box>
|
||||
<Tag
|
||||
colorScheme={formatModelStatus[model.status].colorTheme}
|
||||
variant="solid"
|
||||
px={3}
|
||||
size={'md'}
|
||||
>
|
||||
{formatModelStatus[model.status].text}
|
||||
</Tag>
|
||||
</Flex>
|
||||
<Flex mt={5}>
|
||||
<Box flex={'0 0 100px'}>最后更新时间: </Box>
|
||||
<Box color={'blackAlpha.500'}>{dayjs(model.updateTime).format('YYYY-MM-DD HH:mm')}</Box>
|
||||
</Flex>
|
||||
<Flex mt={5}>
|
||||
<Box flex={'0 0 100px'}>AI模型: </Box>
|
||||
<Box color={'blackAlpha.500'}>{model.service.modelName}</Box>
|
||||
</Flex>
|
||||
<Flex mt={5}>
|
||||
<Box flex={'0 0 100px'}>训练次数: </Box>
|
||||
<Box color={'blackAlpha.500'}>{model.trainingTimes}次</Box>
|
||||
</Flex>
|
||||
<Flex mt={5} justifyContent={'flex-end'}>
|
||||
<Button
|
||||
mr={3}
|
||||
variant={'outline'}
|
||||
w={'100px'}
|
||||
size={'sm'}
|
||||
onClick={() => handlePreviewChat(model._id)}
|
||||
>
|
||||
体验
|
||||
</Button>
|
||||
<Button
|
||||
size={'sm'}
|
||||
w={'100px'}
|
||||
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelPhoneList;
|
120
src/pages/model/components/ModelTable.tsx
Normal file
120
src/pages/model/components/ModelTable.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Tag,
|
||||
Card,
|
||||
Box
|
||||
} from '@chakra-ui/react';
|
||||
import { formatModelStatus } from '@/constants/model';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ModelType } from '@/types/model';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const ModelTable = ({
|
||||
models = [],
|
||||
handlePreviewChat
|
||||
}: {
|
||||
models: ModelType[];
|
||||
handlePreviewChat: (_: string) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const columns = [
|
||||
{
|
||||
title: '模型名',
|
||||
key: 'name',
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '最后更新时间',
|
||||
key: 'updateTime',
|
||||
render: (item: ModelType) => dayjs(item.updateTime).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
render: (item: ModelType) => (
|
||||
<Tag
|
||||
colorScheme={formatModelStatus[item.status].colorTheme}
|
||||
variant="solid"
|
||||
px={3}
|
||||
size={'md'}
|
||||
>
|
||||
{formatModelStatus[item.status].text}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'AI模型',
|
||||
key: 'service',
|
||||
render: (item: ModelType) => (
|
||||
<Box wordBreak={'break-all'} whiteSpace={'pre-wrap'} maxW={'200px'}>
|
||||
{item.service.modelName}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '训练次数',
|
||||
key: 'trainingTimes',
|
||||
dataIndex: 'trainingTimes'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'control',
|
||||
render: (item: ModelType) => (
|
||||
<>
|
||||
<Button mr={3} onClick={() => handlePreviewChat(item._id)}>
|
||||
对话
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme={'gray'}
|
||||
onClick={() => router.push(`/model/detail?modelId=${item._id}`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Card py={3}>
|
||||
<TableContainer>
|
||||
<Table variant={'simple'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
{columns.map((item) => (
|
||||
<Th key={item.key}>{item.title}</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{models.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
{columns.map((col) => (
|
||||
<Td key={col.key}>
|
||||
{col.render
|
||||
? col.render(item)
|
||||
: !!col.dataIndex
|
||||
? // @ts-ignore nextline
|
||||
item[col.dataIndex]
|
||||
: ''}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelTable;
|
70
src/pages/model/components/Training.tsx
Normal file
70
src/pages/model/components/Training.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { Box, Card, 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';
|
||||
|
||||
const Training = ({ model }: { model: ModelType }) => {
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
model._id && loadTrainingRecords(model._id);
|
||||
}, [loadTrainingRecords, model]);
|
||||
|
||||
return (
|
||||
<Card p={4} h={'100%'}>
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Training;
|
247
src/pages/model/detail.tsx
Normal file
247
src/pages/model/detail.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
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 Training from './components/Training';
|
||||
|
||||
const ModelDetail = () => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc } = 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);
|
||||
|
||||
console.log(res);
|
||||
} catch (err) {
|
||||
console.log(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.log(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.log(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.log(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) {
|
||||
console.log(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}
|
||||
>
|
||||
{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} />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
|
||||
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
|
||||
</Box>
|
||||
<ConfirmChild />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelDetail;
|
90
src/pages/model/list.tsx
Normal file
90
src/pages/model/list.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useState, useEffect, 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';
|
||||
|
||||
const ModelList = () => {
|
||||
const { isPc } = useScreen();
|
||||
const router = useRouter();
|
||||
const [models, setModels] = useState<ModelType[]>([]);
|
||||
const [openCreateModel, setOpenCreateModel] = useState(false);
|
||||
const { setLoading } = useGlobalStore();
|
||||
|
||||
/* 加载模型 */
|
||||
const loadModels = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getMyModels();
|
||||
setModels(res);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [setLoading]);
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
}, [loadModels]);
|
||||
|
||||
/* 创建成功回调 */
|
||||
const createModelSuccess = useCallback((data: ModelType) => {
|
||||
setModels((state) => [data, ...state]);
|
||||
}, []);
|
||||
|
||||
/* 点前往聊天预览页 */
|
||||
const handlePreviewChat = useCallback(
|
||||
async (modelId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const chatId = await getChatSiteId(modelId);
|
||||
|
||||
router.push(`/chat?chatId=${chatId}`, undefined, {
|
||||
shallow: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[router, setLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
{/* 头部 */}
|
||||
<Card px={6} py={3}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'} fontSize={'xl'}>
|
||||
模型列表
|
||||
</Box>
|
||||
|
||||
<Button flex={'0 0 145px'} variant={'outline'} onClick={() => setOpenCreateModel(true)}>
|
||||
新建模型
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
{/* 表单 */}
|
||||
<Box mt={5} position={'relative'}>
|
||||
{isPc ? (
|
||||
<ModelTable models={models} handlePreviewChat={handlePreviewChat} />
|
||||
) : (
|
||||
<ModelPhoneList models={models} handlePreviewChat={handlePreviewChat} />
|
||||
)}
|
||||
</Box>
|
||||
{/* 创建弹窗 */}
|
||||
<CreateModel
|
||||
isOpen={openCreateModel}
|
||||
setCreateModelOpen={setOpenCreateModel}
|
||||
onSuccess={createModelSuccess}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelList;
|
Reference in New Issue
Block a user