doc gpt V0.2

This commit is contained in:
archer
2023-02-19 14:35:25 +08:00
parent cc5cf99e7a
commit 0ecf576e4e
124 changed files with 11780 additions and 573 deletions

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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;