feat: 模型介绍和温度调整。完善聊天页提示

This commit is contained in:
Archer
2023-03-18 12:32:55 +08:00
parent 1c364eca35
commit 00b90f071d
32 changed files with 628 additions and 327 deletions

View File

@@ -1,16 +1,16 @@
import { GET, POST, DELETE, PUT } from './request';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { ModelUpdateParams } from '@/types/model';
import { TrainingItemType } from '../types/training';
export const getMyModels = () => GET<ModelType[]>('/model/list');
export const getMyModels = () => GET<ModelSchema[]>('/model/list');
export const postCreateModel = (data: { name: string; serviceModelName: string }) =>
POST<ModelType>('/model/create', data);
POST<ModelSchema>('/model/create', data);
export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`);
export const getModelById = (id: string) => GET<ModelType>(`/model/detail?modelId=${id}`);
export const getModelById = (id: string) => GET<ModelSchema>(`/model/detail?modelId=${id}`);
export const putModelById = (id: string, data: ModelUpdateParams) =>
PUT(`/model/update?modelId=${id}`, data);

View File

@@ -6,6 +6,7 @@ export type InitChatResponse = {
modelId: string;
name: string;
avatar: string;
intro: string;
secret: ModelSchema.secret;
chatModel: ModelSchema.service.ChatModel; // 模型名
history: ChatItemType[];

View File

@@ -172,7 +172,7 @@
}
.markdown ul,
.markdown ol {
padding-left: 30px;
padding-left: 1em;
}
.markdown ul.no-list,
.markdown ol.no-list {

View File

@@ -12,7 +12,7 @@ import 'katex/dist/katex.min.css';
import styles from './index.module.scss';
import { codeLight } from './codeLight';
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
const formatSource = useMemo(() => source, [source]);
const { copyData } = useCopyData();

View File

@@ -0,0 +1,82 @@
import React, { useMemo } from 'react';
import {
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Box
} from '@chakra-ui/react';
const MySlider = ({
markList,
setVal,
activeVal,
max = 100,
min = 0,
step = 1
}: {
markList: {
label: string | number;
value: number;
}[];
activeVal?: number;
setVal: (index: number) => void;
max?: number;
min?: number;
step?: number;
}) => {
const startEndPointStyle = {
content: '""',
borderRadius: '10px',
width: '10px',
height: '10px',
backgroundColor: '#ffffff',
border: '2px solid #D7DBE2',
position: 'absolute',
zIndex: 1,
top: 0,
transform: 'translateY(-3px)'
};
const value = useMemo(() => {
const index = markList.findIndex((item) => item.value === activeVal);
return index > -1 ? index : 0;
}, [activeVal, markList]);
return (
<Slider max={max} min={min} step={step} size={'lg'} value={value} onChange={setVal}>
{markList.map((item, i) => (
<SliderMark
key={item.value}
value={i}
mt={3}
fontSize={'sm'}
transform={'translateX(-50%)'}
{...(activeVal === item.value ? { color: 'blue.500', fontWeight: 'bold' } : {})}
>
<Box px={3} cursor={'pointer'}>
{item.label}
</Box>
</SliderMark>
))}
<SliderTrack
bg={'#EAEDF3'}
overflow={'visible'}
h={'4px'}
_before={{
...startEndPointStyle,
left: '-5px'
}}
_after={{
...startEndPointStyle,
right: '-5px'
}}
>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb border={'2.5px solid'} borderColor={'blue.500'}></SliderThumb>
</Slider>
);
};
export default MySlider;

View File

@@ -39,3 +39,25 @@ export const introPage = `
### 其他问题
还有其他问题,可以加我 wx: YNyiqi拉个交流群大家一起聊聊。
`;
export const chatProblem = `
**代理出错**
服务器代理不稳定,可以过一会儿再尝试。
**API key 问题**
请把 openai 的 API key 粘贴到账号里再创建对话。如果是使用分享的对话,不需要填写 API key。
`;
export const versionIntro = `
* 分享对话:使用的是分享者的 Api Key 生成一个对话窗口进行分享。
* 分享空白对话:为该模型创建一个空白的聊天分享出去。
* 分享当前对话:会把当前聊天的内容也分享出去,但是要注意不要多个人同时用一个聊天内容。
* 增加模型介绍:可以在模型编辑页添加对模型的介绍,方便提示模型的范围。
* 温度调整:可以在模型编辑页调整模型温度,以便适应不同类型的对话。例如,翻译类的模型可以把温度拉低;创作类的模型可以把温度拉高。
`;
export const shareHint = `
你正准备分享对话,请确保分享链接不会滥用,因为它是使用的是你的 API key。
* 分享空白对话:为该模型创建一个空白的聊天分享出去。
* 分享当前对话:会把当前聊天的内容也分享出去,但是要注意不要多个人同时用一个聊天内容。
`;

View File

@@ -1,23 +1,37 @@
import type { ServiceName } from '@/types/mongoSchema';
import { ModelSchema } from '../types/mongoSchema';
export enum ChatModelNameEnum {
GPT35 = 'gpt-3.5-turbo',
GPT3 = 'text-davinci-003'
}
export const OpenAiList = [
export type ModelConstantsData = {
name: string;
model: `${ChatModelNameEnum}`;
trainName: string; // 空字符串代表不能训练
maxToken: number;
maxTemperature: number;
};
export const ModelList: Record<ServiceName, ModelConstantsData[]> = {
openai: [
{
name: 'chatGPT',
model: ChatModelNameEnum.GPT35,
trainName: 'turbo',
canTraining: false,
maxToken: 4060
maxToken: 4000,
maxTemperature: 2
},
{
name: 'GPT3',
model: ChatModelNameEnum.GPT3,
trainName: 'davinci',
canTraining: true,
maxToken: 4060
maxToken: 4000,
maxTemperature: 2
}
];
]
};
export enum TrainingStatusEnum {
pending = 'pending',
@@ -51,3 +65,29 @@ export const formatModelStatus = {
text: '已关闭'
}
};
export const defaultModel: ModelSchema = {
_id: '',
userId: '',
name: '',
avatar: '',
status: ModelStatusEnum.pending,
updateTime: Date.now(),
trainingTimes: 0,
systemPrompt: '',
intro: '',
temperature: 5,
service: {
company: 'openai',
trainId: '',
chatModel: ChatModelNameEnum.GPT35,
modelName: ChatModelNameEnum.GPT35
},
security: {
domain: ['*'],
contextMaxLen: 1,
contentMaxLen: 1,
expiredTime: 9999,
maxLoadAmount: 1
}
};

View File

@@ -8,6 +8,7 @@ import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream';
import { ModelList } from '@/constants/model';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -56,6 +57,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
// 计算温度
const modelConstantsData = ModelList['openai'].find(
(item) => item.model === model.service.modelName
);
if (!modelConstantsData) {
throw new Error('模型异常');
}
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey);
let startTime = Date.now();
@@ -63,8 +73,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: 1,
// max_tokens: model.security.contentMaxLen,
temperature: temperature,
max_tokens: modelConstantsData.maxToken,
messages: formatPrompts,
stream: true
},

View File

@@ -5,6 +5,7 @@ import { connectToDatabase } from '@/service/mongo';
import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { httpsAgent } from '@/service/utils/tools';
import { ModelList } from '@/constants/model';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -27,13 +28,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// prompt处理
const formatPrompt = prompt.map((item) => `${item.value}\n\n###\n\n`).join('');
// 计算温度
const modelConstantsData = ModelList['openai'].find(
(item) => item.model === model.service.modelName
);
if (!modelConstantsData) {
throw new Error('模型异常');
}
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// 发送请求
const response = await chatAPI.createCompletion(
{
model: model.service.modelName,
prompt: formatPrompt,
temperature: 0.5,
max_tokens: model.security.contentMaxLen,
temperature: temperature,
max_tokens: modelConstantsData.maxToken,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0.6,

View File

@@ -47,6 +47,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
modelId: model._id,
name: model.name,
avatar: model.avatar,
intro: model.intro,
secret: model.security,
chatModel: model.service.chatModel,
history: chat.content

View File

@@ -3,12 +3,21 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelStatusEnum, OpenAiList } from '@/constants/model';
import { ModelStatusEnum, ModelList, ChatModelNameEnum } from '@/constants/model';
import type { ServiceName } from '@/types/mongoSchema';
import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, serviceModelName, serviceModelCompany = 'openai' } = req.body;
const {
name,
serviceModelName,
serviceModelCompany = 'openai'
} = req.body as {
name: string;
serviceModelName: `${ChatModelNameEnum}`;
serviceModelCompany: ServiceName;
};
const { authorization } = req.headers;
if (!authorization) {
@@ -22,10 +31,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 凭证校验
const userId = await authToken(authorization);
const modelItem = OpenAiList.find((item) => item.model === serviceModelName);
const modelItem = ModelList[serviceModelCompany].find(
(item) => item.model === serviceModelName
);
if (!modelItem) {
throw new Error('模型错误');
throw new Error('模型不存在');
}
await connectToDatabase();
@@ -43,8 +54,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const authCount = await Model.countDocuments({
userId
});
if (authCount >= 10) {
throw new Error('上限 10 个模型');
if (authCount >= 20) {
throw new Error('上限 20 个模型');
}
// 创建模型

View File

@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -26,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
// 根据 userId 获取模型信息
const model: ModelType | null = await Model.findOne({
const model = await Model.findOne<ModelSchema>({
userId,
_id: modelId
});

View File

@@ -6,7 +6,7 @@ import formidable from 'formidable';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { join } from 'path';
import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { httpsAgent } from '@/service/utils/tools';

View File

@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { TrainingItemType } from '@/types/training';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
@@ -26,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 获取模型
const model: ModelType | null = await Model.findById(modelId);
const model = await Model.findById<ModelSchema>(modelId);
if (!model || model.status !== 'training') {
throw new Error('模型不在训练中');

View File

@@ -7,7 +7,7 @@ import formidable from 'formidable';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { join } from 'path';
import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { httpsAgent } from '@/service/utils/tools';
@@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
// 获取模型的状态
const model: ModelType | null = await Model.findById(modelId);
const model = await Model.findById<ModelSchema>(modelId);
if (!model || model.status !== 'running') {
throw new Error('模型正忙');

View File

@@ -8,7 +8,8 @@ import type { ModelUpdateParams } from '@/types/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, service, security, systemPrompt } = req.body as ModelUpdateParams;
const { name, service, security, systemPrompt, intro, temperature } =
req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers;
@@ -33,8 +34,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
{
name,
service,
systemPrompt,
intro,
temperature,
service,
security
}
);

View File

@@ -1,23 +1,44 @@
import React from 'react';
import { Card, Flex, Box } from '@chakra-ui/react';
import { Card, Box, Mark } from '@chakra-ui/react';
import { versionIntro, chatProblem } from '@/constants/common';
import Markdown from '@/components/Markdown';
const Empty = () => {
const Empty = ({ intro }: { intro: string }) => {
const Header = ({ children }: { children: string }) => (
<Box fontSize={'lg'} fontWeight={'bold'} textAlign={'center'} pb={2}>
{children}
</Box>
);
return (
<Flex h={'100%'} alignItems={'center'} justifyContent={'center'}>
<Card p={5} w={'70%'}>
<Box fontSize={'xl'} fontWeight={'bold'} textAlign={'center'} pb={2}>
Fast Gpt version1.3
</Box>
<Box>
</Box>
<Box>使 Api Key </Box>
<br />
<Box></Box>
<br />
<Box>使</Box>
<Box
minH={'100%'}
w={'85%'}
maxW={'600px'}
m={'auto'}
py={'5vh'}
alignItems={'center'}
justifyContent={'center'}
>
{!!intro && (
<Card p={4} mb={10}>
<Header></Header>
<Box>{intro}</Box>
</Card>
</Flex>
)}
<Card p={4} mb={10}>
<Header></Header>
<Markdown source={chatProblem} />
</Card>
{/* version intro */}
<Card p={4}>
<Header>Fast Gpt version1.4</Header>
<Box>
</Box>
<br />
<Markdown source={versionIntro} />
</Card>
</Box>
);
};

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Box, Button } from '@chakra-ui/react';
import { AddIcon, ChatIcon, EditIcon, DeleteIcon } from '@chakra-ui/icons';
import { AddIcon, ChatIcon, DeleteIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Accordion,
AccordionItem,
AccordionButton,
@@ -9,16 +10,26 @@ import {
AccordionIcon,
Flex,
Divider,
IconButton
IconButton,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useChatStore } from '@/store/chat';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useScreen } from '@/hooks/useScreen';
import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import { useCopyData } from '@/utils/tools';
import Markdown from '@/components/Markdown';
import { shareHint } from '@/constants/common';
import { getChatSiteId } from '@/api/chat';
const SlideBar = ({
name,
@@ -36,10 +47,13 @@ const SlideBar = ({
const router = useRouter();
const { copyData } = useCopyData();
const { myModels, getMyModels } = useUserStore();
const { chatHistory, removeChatHistoryByWindowId, generateChatWindow, updateChatHistory } =
useChatStore();
const { isSuccess } = useQuery(['init'], getMyModels);
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
const [hasReady, setHasReady] = useState(false);
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
const { isSuccess } = useQuery(['init'], getMyModels, {
cacheTime: 5 * 60 * 1000
});
useEffect(() => {
setHasReady(true);
@@ -119,19 +133,8 @@ const SlideBar = ({
{/* 我的模型 & 历史记录 折叠框*/}
<Box flex={'1 0 0'} px={3} h={0} overflowY={'auto'}>
{isSuccess ? (
<Accordion defaultIndex={[0]} allowToggle>
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={0} px={0}>
{hasReady && <RenderHistory />}
</AccordionPanel>
</AccordionItem>
{isSuccess && (
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
@@ -161,7 +164,7 @@ const SlideBar = ({
: {})}
onClick={async () => {
if (item.name === name) return;
router.push(`/chat?chatId=${await generateChatWindow(item._id)}`);
router.push(`/chat?chatId=${await getChatSiteId(item._id)}`);
onClose();
}}
>
@@ -173,22 +176,24 @@ const SlideBar = ({
))}
</AccordionPanel>
</AccordionItem>
</Accordion>
) : (
<>
<Box mb={4} textAlign={'center'}>
)}
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<RenderHistory />
</>
)}
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={0} px={0}>
{hasReady && <RenderHistory />}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Box>
<Divider my={4} />
<Box px={3}>
{/* 分享 */}
{getToken() && (
<Flex
alignItems={'center'}
p={2}
@@ -197,34 +202,57 @@ const SlideBar = ({
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={async () => {
copyData(
`${location.origin}/chat?chatId=${await generateChatWindow(modelId)}`,
'已复制分享链接'
);
onClick={() => {
onOpenShare();
onClose();
}}
>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</Flex>
)}
<Flex
mt={4}
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={async () => {
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
}}
>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</Flex>
</Box>
{/* 分享提示modal */}
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Markdown source={shareHint} />
</ModalBody>
<ModalFooter>
<Button colorScheme="gray" variant={'outline'} mr={3} onClick={onCloseShare}>
Close
</Button>
{getToken() && (
<Button
variant="outline"
mr={3}
onClick={async () => {
copyData(
`${location.origin}/chat?chatId=${await getChatSiteId(modelId)}`,
'已复制分享链接'
);
onCloseShare();
}}
>
</Button>
)}
<Button
onClick={() => {
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
onCloseShare();
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Flex>
);
};

View File

@@ -51,6 +51,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
modelId: '',
name: '',
avatar: '',
intro: '',
secret: {},
chatModel: '',
history: [],
@@ -113,7 +114,11 @@ const Chat = ({ chatId }: { chatId: string }) => {
status: 'finish'
}))
});
if (res.history.length > 0) {
setTimeout(() => {
scrollToBottom();
}, 500);
}
},
onError(e: any) {
toast({
@@ -433,7 +438,7 @@ const Chat = ({ chatId }: { chatId: string }) => {
</Flex>
</Box>
))}
{chatData.history.length === 0 && <Empty />}
{chatData.history.length === 0 && <Empty intro={chatData.intro} />}
</Box>
{/* 发送区 */}
<Box

View File

@@ -16,8 +16,8 @@ import {
} 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';
import type { ModelSchema } from '@/types/mongoSchema';
import { ModelList } from '@/constants/model';
interface CreateFormType {
name: string;
@@ -29,7 +29,7 @@ const CreateModel = ({
onSuccess
}: {
setCreateModelOpen: Dispatch<boolean>;
onSuccess: Dispatch<ModelType>;
onSuccess: Dispatch<ModelSchema>;
}) => {
const [requesting, setRequesting] = useState(false);
const toast = useToast({
@@ -42,7 +42,7 @@ const CreateModel = ({
formState: { errors }
} = useForm<CreateFormType>({
defaultValues: {
serviceModelName: OpenAiList[0].model
serviceModelName: ModelList['openai'][0].model
}
});
@@ -95,7 +95,7 @@ const CreateModel = ({
required: '底层模型不能为空'
})}
>
{OpenAiList.map((item) => (
{ModelList['openai'].map((item) => (
<option key={item.model} value={item.model}>
{item.name}
</option>

View File

@@ -1,86 +1,37 @@
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';
import { useToast } from '@/hooks/useToast';
import { putModelById } from '@/api/model';
import { useScreen } from '@/hooks/useScreen';
import { useGlobalStore } from '@/store/global';
import React, { useState } from 'react';
import {
Box,
Card,
Flex,
FormControl,
Input,
Textarea,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Tooltip
} from '@chakra-ui/react';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import type { ModelSchema } from '@/types/mongoSchema';
import { UseFormReturn } from 'react-hook-form';
const ModelEditForm = ({ model }: { model?: ModelType }) => {
const isInit = useRef(false);
const {
register,
handleSubmit,
reset,
formState: { errors }
} = useForm<ModelType>();
const { setLoading } = useGlobalStore();
const { toast } = useToast();
const { media } = 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('error->', 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]);
/* model 只会改变一次 */
useEffect(() => {
if (model && !isInit.current) {
reset(model);
isInit.current = true;
}
}, [model, reset]);
const ModelEditForm = ({ formHooks }: { formHooks: UseFormReturn<ModelSchema> }) => {
const { register, setValue, getValues } = formHooks;
const [refresh, setRefresh] = useState(false);
return (
<Grid gridTemplateColumns={media('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>
<Box fontWeight={'bold'}></Box>
</Flex>
<FormControl mt={5}>
<FormControl mt={4}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'}>:</Box>
<Box flex={'0 0 50px'} w={0}>
:
</Box>
<Input
{...register('name', {
required: '展示名称不能为空'
@@ -88,30 +39,79 @@ const ModelEditForm = ({ model }: { model?: ModelType }) => {
></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}>
<FormControl mt={4}>
<Box mb={1}>:</Box>
<Textarea
rows={4}
rows={5}
maxLength={500}
{...register('systemPrompt')}
placeholder={
'模型默认的 prompt 词,可以通过调整该内容,生成一个限定范围的模型,更方便的去使用。'
}
{...register('intro')}
placeholder={'模型的介绍,仅做展示,不影响模型的效果'}
/>
</FormControl>
</Card>
<Card p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}>
<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}>
<Box mb={1}></Box>
<Textarea
rows={6}
maxLength={500}
{...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'}>:</Box>
<Box flex={'0 0 120px'} w={0}>
:
</Box>
<Input
flex={1}
type={'number'}
@@ -132,7 +132,9 @@ const ModelEditForm = ({ model }: { model?: ModelType }) => {
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'}>:</Box>
<Box flex={'0 0 120px'} w={0}>
:
</Box>
<Input
flex={1}
type={'number'}
@@ -153,7 +155,9 @@ const ModelEditForm = ({ model }: { model?: ModelType }) => {
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'}>:</Box>
<Box flex={'0 0 120px'} w={0}>
:
</Box>
<Input
flex={1}
type={'number'}
@@ -175,7 +179,9 @@ const ModelEditForm = ({ model }: { model?: ModelType }) => {
</FormControl>
<FormControl mt={5} pb={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 130px'}>:</Box>
<Box flex={'0 0 130px'} w={0}>
:
</Box>
<Box flex={1}>
<Input
type={'number'}
@@ -196,7 +202,7 @@ const ModelEditForm = ({ model }: { model?: ModelType }) => {
</Flex>
</FormControl>
</Card>
</Grid>
</>
);
};

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { Box, Button, Flex, Tag } from '@chakra-ui/react';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
@@ -9,7 +9,7 @@ const ModelPhoneList = ({
models,
handlePreviewChat
}: {
models: ModelType[];
models: ModelSchema[];
handlePreviewChat: (_: string) => void;
}) => {
const router = useRouter();

View File

@@ -14,14 +14,14 @@ import {
} from '@chakra-ui/react';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { useRouter } from 'next/router';
const ModelTable = ({
models = [],
handlePreviewChat
}: {
models: ModelType[];
models: ModelSchema[];
handlePreviewChat: (_: string) => void;
}) => {
const router = useRouter();
@@ -34,13 +34,13 @@ const ModelTable = ({
{
title: '最后更新时间',
key: 'updateTime',
render: (item: ModelType) => dayjs(item.updateTime).format('YYYY-MM-DD HH:mm')
render: (item: ModelSchema) => dayjs(item.updateTime).format('YYYY-MM-DD HH:mm')
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
render: (item: ModelType) => (
render: (item: ModelSchema) => (
<Tag
colorScheme={formatModelStatus[item.status].colorTheme}
variant="solid"
@@ -54,7 +54,7 @@ const ModelTable = ({
{
title: 'AI模型',
key: 'service',
render: (item: ModelType) => (
render: (item: ModelSchema) => (
<Box wordBreak={'break-all'} whiteSpace={'pre-wrap'} maxW={'200px'}>
{item.service.modelName}
</Box>
@@ -68,7 +68,7 @@ const ModelTable = ({
{
title: '操作',
key: 'control',
render: (item: ModelType) => (
render: (item: ModelSchema) => (
<>
<Button mr={3} onClick={() => handlePreviewChat(item._id)}>

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useCallback, useState } from 'react';
import { Box, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { getModelTrainings } from '@/api/model';
import type { TrainingItemType } from '@/types/training';
const Training = ({ model }: { model: ModelType }) => {
const Training = ({ model }: { model: ModelSchema }) => {
const columns: {
title: string;
key: keyof TrainingItemType;

View File

@@ -1,21 +1,29 @@
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import { getModelById, delModelById, postTrainModel, putModelTrainingStatus } from '@/api/model';
import {
getModelById,
delModelById,
postTrainModel,
putModelTrainingStatus,
putModelById
} from '@/api/model';
import { getChatSiteId } from '@/api/chat';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
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 { 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 Icon from '@/components/Iconfont';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
const Training = dynamic(() => import('./components/Training'));
const ModelDetail = () => {
const ModelDetail = ({ modelId }: { modelId: string }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc, media } = useScreen();
@@ -24,33 +32,35 @@ const ModelDetail = () => {
content: '确认删除该模型?'
});
const SelectFileDom = useRef<HTMLInputElement>(null);
const { modelId } = router.query as { modelId: string };
const [model, setModel] = useState<ModelType>();
const [model, setModel] = useState<ModelSchema>(defaultModel);
const formHooks = useForm<ModelSchema>({
defaultValues: model
});
const canTrain = useMemo(() => {
const openai = OpenAiList.find((item) => item.model === model?.service.modelName);
return openai && openai.canTraining === true;
const openai = ModelList[model.service.company].find(
(item) => item.model === model?.service.modelName
);
return openai && openai.trainName;
}, [model]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
if (!modelId) return;
setLoading(true);
try {
const res = await getModelById(modelId as string);
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);
}, [modelId, setLoading]);
return null;
}, [formHooks, modelId, setLoading]);
useEffect(() => {
loadModel();
router.prefetch('/chat');
}, [loadModel, modelId, router]);
useQuery([modelId], loadModel);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
@@ -71,7 +81,6 @@ const ModelDetail = () => {
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
if (!model) return;
setLoading(true);
try {
const chatId = await getChatSiteId(model._id);
@@ -131,6 +140,65 @@ const ModelDetail = () => {
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;
};
}, []);
return (
<>
{/* 头部 */}
@@ -138,9 +206,8 @@ const ModelDetail = () => {
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{model?.name || '模型'}
{model.name}
</Box>
{!!model && (
<Tag
ml={2}
variant="solid"
@@ -150,38 +217,37 @@ const ModelDetail = () => {
>
{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 || '模型'}
{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>
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
</Button>
</Box>
</>
)}
</Card>
{/* 基本信息编辑 */}
<Box mt={5}>
<ModelEditForm model={model} />
</Box>
{/* 其他配置 */}
<Grid mt={5} gridTemplateColumns={media('1fr 1fr', '1fr')} gridGap={5}>
<ModelEditForm formHooks={formHooks} />
<Card p={4}>{!!model && <Training model={model} />}</Card>
<Card p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}>
@@ -241,6 +307,8 @@ const ModelDetail = () => {
</Flex>
</Card>
</Grid>
{/* 文件选择 */}
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
</Box>
@@ -250,3 +318,11 @@ const ModelDetail = () => {
};
export default ModelDetail;
export async function getServerSideProps(context: any) {
const modelId = context.query?.modelId || '';
return {
props: { modelId }
};
}

View File

@@ -1,7 +1,7 @@
import React, { useState, useCallback } from 'react';
import { Box, Button, Flex, Card } from '@chakra-ui/react';
import { getChatSiteId } from '@/api/chat';
import { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { useRouter } from 'next/router';
import ModelTable from './components/ModelTable';
import ModelPhoneList from './components/ModelPhoneList';
@@ -27,7 +27,7 @@ const ModelList = () => {
/* 创建成功回调 */
const createModelSuccess = useCallback(
(data: ModelType) => {
(data: ModelSchema) => {
setMyModels([data, ...myModels]);
},
[myModels, setMyModels]

View File

@@ -25,7 +25,8 @@ const ChatSchema = new Schema({
type: Number,
required: true
},
content: [
content: {
type: [
{
obj: {
type: String,
@@ -37,7 +38,9 @@ const ChatSchema = new Schema({
required: true
}
}
]
],
default: []
}
});
export const Chat = models['chat'] || model('chat', ChatSchema);

View File

@@ -1,6 +1,11 @@
import { Schema, model, models } from 'mongoose';
const ModelSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
name: {
type: String,
required: true
@@ -10,13 +15,14 @@ const ModelSchema = new Schema({
default: '/imgs/modelAvatar.png'
},
systemPrompt: {
// 系统提示词
type: String,
default: ''
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
intro: {
// 模型介绍
type: String,
default: ''
},
status: {
type: String,
@@ -31,6 +37,12 @@ const ModelSchema = new Schema({
type: Number,
default: 0
},
temperature: {
type: Number,
min: 1,
max: 10,
default: 5
},
service: {
company: {
type: String,

View File

@@ -2,7 +2,7 @@ import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { UserType, UserUpdateParams } from '@/types/user';
import type { ModelType } from '@/types/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { setToken } from '@/utils/user';
import { getMyModels } from '@/api/model';
@@ -10,9 +10,9 @@ type State = {
userInfo: UserType | null;
setUserInfo: (user: UserType, token?: string) => void;
updateUserInfo: (user: UserUpdateParams) => void;
myModels: ModelType[];
myModels: ModelSchema[];
getMyModels: () => void;
setMyModels: (data: ModelType[]) => void;
setMyModels: (data: ModelSchema[]) => void;
};
export const useUserStore = create<State>()(
@@ -42,7 +42,7 @@ export const useUserStore = create<State>()(
});
return res;
}),
setMyModels(data: ModelType[]) {
setMyModels(data: ModelSchema[]) {
set((state) => {
state.myModels = data;
});

2
src/types/chat.d.ts vendored
View File

@@ -1,5 +1,3 @@
import type { ModelType } from './model';
export type ChatItemType = {
obj: 'Human' | 'AI' | 'SYSTEM';
value: string;

40
src/types/model.d.ts vendored
View File

@@ -1,40 +1,10 @@
import { ModelStatusEnum } from '@/constants/model';
export interface ModelType {
_id: string;
userId: string;
name: string;
avatar: string;
status: `${ModelStatusEnum}`;
updateTime: Date;
trainingTimes: number;
systemPrompt: string;
service: {
company: 'openai'; // 关联的厂商
trainId: string; // 训练时需要的ID
chatModel: string; // 聊天时用的模型
modelName: string; // 关联的模型
};
security: {
domain: string[];
contentMaxLen: number;
contextMaxLen: number;
expiredTime: number;
maxLoadAmount: number;
};
}
import type { ModelSchema } from './mongoSchema';
export interface ModelUpdateParams {
name: string;
systemPrompt: string;
service: {
company: 'openai'; // 关联的厂商
modelName: string; // 关联的模型
};
security: {
domain: string[];
contentMaxLen: number;
contextMaxLen: number;
expiredTime: number;
maxLoadAmount: number;
};
intro: string;
temperature: number;
service: ModelSchema.service;
security: ModelSchema.security;
}

View File

@@ -25,15 +25,17 @@ export interface ModelSchema {
name: string;
avatar: string;
systemPrompt: string;
intro: string;
userId: string;
status: `${ModelStatusEnum}`;
updateTime: number;
trainingTimes: number;
temperature: number;
service: {
company: ServiceName;
trainId: string;
chatModel: `${ChatModelNameEnum}`;
modelName: string;
trainId: string; // 训练的模型训练后就是训练的模型id
chatModel: string; // 聊天时用的模型,训练后就是训练的模型
modelName: `${ChatModelNameEnum}`; // 底层模型名称,不会变
};
security: {
domain: string[];