feat: new ui

This commit is contained in:
archer
2023-05-04 23:30:59 +08:00
parent 4d043e0e46
commit 014fb504a4
133 changed files with 2426 additions and 1696 deletions

View File

@@ -0,0 +1,171 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Flex, useTheme, Input, IconButton, Tooltip, Image } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { useRouter } from 'next/router';
import MyIcon from '@/components/Icon';
import { postCreateModel } from '@/api/model';
import { useLoading } from '@/hooks/useLoading';
import { useToast } from '@/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user';
const ModelList = ({ modelId }: { modelId: string }) => {
const theme = useTheme();
const router = useRouter();
const { toast } = useToast();
const { Loading, setIsLoading } = useLoading();
const { myModels, myCollectionModels, loadMyModels, refreshModel } = useUserStore();
const [searchText, setSearchText] = useState('');
/* 加载模型 */
const { isLoading } = useQuery(['loadModels'], () => loadMyModels(false));
const onclickCreateModel = useCallback(async () => {
setIsLoading(true);
try {
const id = await postCreateModel({ name: `AI助手${myModels.length + 1}` });
toast({
title: '创建成功',
status: 'success'
});
refreshModel.freshMyModels();
router.push(`/model?modelId=${id}`);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err.message || '出现了意外',
status: 'error'
});
}
setIsLoading(false);
}, [myModels.length, refreshModel, router, setIsLoading, toast]);
const models = useMemo(
() =>
[
{
label: '我的',
list: myModels.filter((item) =>
new RegExp(searchText, 'ig').test(item.name + item.systemPrompt)
)
},
{
label: '收藏',
list: myCollectionModels.filter((item) =>
new RegExp(searchText, 'ig').test(item.name + item.systemPrompt)
)
}
].filter((item) => item.list.length > 0),
[myCollectionModels, myModels, searchText]
);
const totalModels = useMemo(
() => models.reduce((sum, item) => sum + item.list.length, 0),
[models]
);
return (
<Flex
position={'relative'}
flexDirection={'column'}
w={'100%'}
h={'100%'}
bg={'white'}
borderRight={['', theme.borders.base]}
>
<Flex w={'90%'} my={5} mx={'auto'}>
<Flex flex={1} mr={2} position={'relative'} alignItems={'center'}>
<Input
h={'32px'}
placeholder="搜索 AI 助手"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
{searchText && (
<MyIcon
zIndex={10}
position={'absolute'}
right={3}
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.500'}
cursor={'pointer'}
onClick={() => setSearchText('')}
/>
)}
</Flex>
<Tooltip label={'新建一个AI助手'}>
<IconButton
h={'32px'}
icon={<AddIcon />}
aria-label={''}
variant={'outline'}
onClick={onclickCreateModel}
/>
</Tooltip>
</Flex>
<Box flex={'1 0 0'} h={0} overflow={'overlay'}>
{models.map((item) => (
<Box key={item.label} _notFirst={{ mt: 5 }}>
<Box fontWeight={'bold'} pl={5}>
{item.label}
</Box>
{item.list.map((item) => (
<Flex
key={item._id}
position={'relative'}
alignItems={['flex-start', 'center']}
p={3}
mb={[2, 0]}
cursor={'pointer'}
transition={'background-color .2s ease-in'}
borderLeft={['', '5px solid transparent']}
_hover={{
backgroundColor: ['', '#dee0e3']
}}
{...(modelId === item._id
? {
backgroundColor: '#eff0f1',
borderLeftColor: 'myBlue.600'
}
: {})}
onClick={() => {
if (item._id === modelId) return;
router.push(`/model?modelId=${item._id}`);
}}
>
<Image
src={item.avatar || '/icon/logo.png'}
alt=""
w={'34px'}
maxH={'50px'}
objectFit={'contain'}
/>
<Box flex={'1 0 0'} w={0} ml={3}>
<Box className="textEllipsis" color={'myGray.1000'}>
{item.name}
</Box>
<Box className="textEllipsis" color={'myGray.400'} fontSize={'sm'}>
{item.systemPrompt || '这个模型没有提示词~'}
</Box>
</Box>
</Flex>
))}
</Box>
))}
{!isLoading && totalModels === 0 && (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
AI ~
</Box>
</Flex>
)}
</Box>
<Loading loading={isLoading} fixed={false} />
</Flex>
);
};
export default ModelList;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useRef } from 'react';
import React, { useCallback, useState, useRef, useEffect } from 'react';
import {
Box,
TableContainer,
@@ -53,6 +53,7 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean
whiteSpace: 'pre-wrap',
overflowY: 'auto'
});
const {
data: modelDataList,
isLoading,
@@ -66,9 +67,14 @@ const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean
params: {
modelId,
searchText
}
},
defaultRequest: false
});
useEffect(() => {
getData(1);
}, [modelId, getData]);
const [editInputData, setEditInputData] = useState<InputDataType>();
const {

View File

@@ -54,9 +54,10 @@ const ModelEditForm = ({
if (!file) return;
try {
const base64 = await compressImg({
file
file,
maxW: 40,
maxH: 60
});
setValue('avatar', base64);
setRefresh((state) => !state);
} catch (err: any) {
@@ -184,7 +185,7 @@ const ModelEditForm = ({
<SliderMark
value={getValues('chat.temperature')}
textAlign="center"
bg="blue.500"
bg="myBlue.600"
color="white"
w={'18px'}
h={'18px'}
@@ -195,7 +196,7 @@ const ModelEditForm = ({
{getValues('chat.temperature')}
</SliderMark>
<SliderTrack>
<SliderFilledTrack />
<SliderFilledTrack bg={'myBlue.700'} />
</SliderTrack>
<SliderThumb />
</Slider>

View File

@@ -97,7 +97,7 @@ const SelectJsonModal = ({
my={3}
cursor={'pointer'}
textDecoration={'underline'}
color={'blue.600'}
color={'myBlue.600'}
onClick={() =>
fileDownload({
text: csvTemplate,

View File

@@ -1,72 +1,70 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router';
import { getModelById, delModelById, putModelById } from '@/api/model';
import { delModelById, putModelById } from '@/api/model';
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, defaultModel } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import { formatModelStatus } from '@/constants/model';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
import { useLoading } from '@/hooks/useLoading';
import ModelEditForm from './components/ModelEditForm';
import ModelDataCard from './components/ModelDataCard';
const ModelEditForm = dynamic(() => import('./components/ModelEditForm'));
const ModelDataCard = dynamic(() => import('./components/ModelDataCard'));
const ModelDetail = ({ modelId }: { modelId: string }) => {
const ModelDetail = ({ modelId, isPc }: { modelId: string; isPc: boolean }) => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { userInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const { userInfo, modelDetail, loadModelDetail, refreshModel, setLastModelId } = useUserStore();
const { Loading, setIsLoading } = useLoading();
const [btnLoading, setBtnLoading] = useState(false);
const [model, setModel] = useState<ModelSchema>(defaultModel);
const formHooks = useForm<ModelSchema>({
defaultValues: model
defaultValues: modelDetail
});
const isOwner = useMemo(() => model.userId === userInfo?._id, [model.userId, userInfo?._id]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
setLoading(true);
try {
const res = await getModelById(modelId);
setModel(res);
formHooks.reset(res);
} catch (err: any) {
// load model data
const { isLoading } = useQuery([modelId], () => loadModelDetail(modelId), {
onSuccess(res) {
res && formHooks.reset(res);
modelId && setLastModelId(modelId);
},
onError(err: any) {
toast({
title: err?.message || '获取模型异常',
status: 'error'
});
setLastModelId('');
refreshModel.freshMyModels();
router.replace('/model');
}
setLoading(false);
return null;
}, [formHooks, modelId, setLoading, toast]);
});
useQuery([modelId], loadModel);
const isOwner = useMemo(
() => modelDetail.userId === userInfo?._id,
[modelDetail.userId, userInfo?._id]
);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!model) return;
setLoading(true);
if (!modelDetail) return;
setIsLoading(true);
try {
await delModelById(model._id);
await delModelById(modelDetail._id);
toast({
title: '删除成功',
status: 'success'
});
router.replace('/model/list');
refreshModel.removeModelDetail(modelDetail._id);
router.replace('/model');
} catch (err: any) {
toast({
title: err?.message || '删除失败',
status: 'error'
});
}
setLoading(false);
}, [setLoading, model, router, toast]);
setIsLoading(false);
}, [modelDetail, setIsLoading, toast, refreshModel, router]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
@@ -76,7 +74,7 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: ModelSchema) => {
setLoading(true);
setBtnLoading(true);
try {
await putModelById(data._id, {
name: data.name,
@@ -89,15 +87,16 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
title: '更新成功',
status: 'success'
});
refreshModel.updateModelDetail(data);
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setLoading(false);
setBtnLoading(false);
},
[setLoading, toast]
[refreshModel, toast]
);
// 提交保存表单失败
const saveSubmitError = useCallback(() => {
@@ -131,23 +130,31 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
}, [router]);
return (
<>
<Box h={'100%'} p={5} overflow={'overlay'} position={'relative'}>
{/* 头部 */}
<Card px={6} py={3}>
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{model.name}
{modelDetail.name}
</Box>
<Tag ml={2} variant="solid" colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text}
<Tag
ml={2}
variant="solid"
colorScheme={formatModelStatus[modelDetail.status].colorTheme}
>
{formatModelStatus[modelDetail.status].text}
</Tag>
<Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}>
</Button>
{isOwner && (
<Button ml={4} onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}>
<Button
isLoading={btnLoading}
ml={4}
onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
)}
@@ -156,20 +163,21 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<>
<Flex alignItems={'center'}>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{model?.name}
{modelDetail.name}
</Box>
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text}
<Tag ml={2} colorScheme={formatModelStatus[modelDetail.status].colorTheme}>
{formatModelStatus[modelDetail.status].text}
</Tag>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'outline'} size={'sm'} onClick={handlePreviewChat}>
</Button>
{isOwner && (
<Button
ml={4}
size={'sm'}
isLoading={btnLoading}
onClick={formHooks.handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
@@ -182,22 +190,13 @@ const ModelDetail = ({ modelId }: { modelId: string }) => {
<Grid mt={5} gridTemplateColumns={['1fr', '1fr 1fr']} gridGap={5}>
<ModelEditForm formHooks={formHooks} handleDelModel={handleDelModel} isOwner={isOwner} />
{modelId && (
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<ModelDataCard modelId={modelId} isOwner={isOwner} />
</Card>
)}
<Card p={4} gridColumnStart={[1, 1]} gridColumnEnd={[2, 3]}>
<ModelDataCard modelId={modelId} isOwner={isOwner} />
</Card>
</Grid>
</>
<Loading loading={isLoading} fixed={false} />
</Box>
);
};
export default ModelDetail;
export async function getServerSideProps(context: any) {
const modelId = context.query?.modelId || '';
return {
props: { modelId }
};
}

47
src/pages/model/index.tsx Normal file
View File

@@ -0,0 +1,47 @@
import React, { useEffect } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { useScreen } from '@/hooks/useScreen';
import { useRouter } from 'next/router';
import ModelList from './components/ModelList';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/store/user';
const ModelDetail = dynamic(() => import('./components/detail/index'));
const Model = ({ modelId, isPcDevice }: { modelId: string; isPcDevice: boolean }) => {
const router = useRouter();
const { isPc } = useScreen({
defaultIsPc: isPcDevice
});
const { lastModelId } = useUserStore();
// redirect modelId
useEffect(() => {
if (isPc && !modelId && lastModelId) {
router.replace(`/model?modelId=${lastModelId}`);
}
}, [isPc, lastModelId, modelId, router]);
return (
<Flex h={'100%'} position={'relative'}>
{/* 模型列表 */}
{(isPc || !modelId) && (
<Box w={['100%', '250px']}>
<ModelList modelId={modelId} />
</Box>
)}
<Box flex={1} h={'100%'}>
{modelId && <ModelDetail modelId={modelId} isPc={isPc} />}
</Box>
</Flex>
);
};
export default Model;
Model.getInitialProps = ({ query, req }: any) => {
return {
modelId: query?.modelId || '',
isPcDevice: !/Mobile/.test(req ? req.headers['user-agent'] : navigator.userAgent)
};
};

View File

@@ -1,76 +0,0 @@
import React, { useEffect } from 'react';
import { Box, Button, Flex, Tag } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { formatModelStatus } from '@/constants/model';
import { useRouter } from 'next/router';
import { ChatModelMap } from '@/constants/model';
const ModelPhoneList = ({
models,
handlePreviewChat
}: {
models: ModelSchema[];
handlePreviewChat: (_: string) => void;
}) => {
const router = useRouter();
useEffect(() => {
router.prefetch('/chat');
}, [router]);
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'}>{ChatModelMap[model.chat.chatModel].name}</Box>
</Flex>
<Flex mt={5}>
<Box flex={'0 0 100px'}>: </Box>
<Box color={'blackAlpha.500'}>{model.chat.temperature}</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

@@ -1,120 +0,0 @@
import { useEffect } from 'react';
import {
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Tag,
Card,
Box
} from '@chakra-ui/react';
import { formatModelStatus } from '@/constants/model';
import type { ModelSchema } from '@/types/mongoSchema';
import { useRouter } from 'next/router';
import { ChatModelMap } from '@/constants/model';
const ModelTable = ({
models = [],
handlePreviewChat
}: {
models: ModelSchema[];
handlePreviewChat: (_: string) => void;
}) => {
const router = useRouter();
const columns = [
{
title: '模型名',
key: 'name',
dataIndex: 'name'
},
{
title: '对话模型',
key: 'service',
render: (model: ModelSchema) => (
<Box fontWeight={'bold'} whiteSpace={'pre-wrap'} maxW={'200px'}>
{ChatModelMap[model.chat.chatModel].name}
</Box>
)
},
{
title: '温度',
key: 'temperature',
render: (model: ModelSchema) => <>{model.chat.temperature}</>
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
render: (item: ModelSchema) => (
<Tag
colorScheme={formatModelStatus[item.status]?.colorTheme}
variant="solid"
px={3}
size={'md'}
>
{formatModelStatus[item.status]?.text}
</Tag>
)
},
{
title: '操作',
key: 'control',
render: (item: ModelSchema) => (
<>
<Button mr={3} onClick={() => handlePreviewChat(item._id)}>
</Button>
<Button
variant={'outline'}
onClick={() => router.push(`/model/detail?modelId=${item._id}`)}
>
</Button>
</>
)
}
];
useEffect(() => {
router.prefetch('/chat');
}, [router]);
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

@@ -1,90 +0,0 @@
import React, { useCallback } from 'react';
import { Box, Button, Flex, Card } from '@chakra-ui/react';
import type { ModelSchema } from '@/types/mongoSchema';
import { useRouter } from 'next/router';
import ModelTable from './components/ModelTable';
import ModelPhoneList from './components/ModelPhoneList';
import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import { useToast } from '@/hooks/useToast';
import { useUserStore } from '@/store/user';
import { postCreateModel } from '@/api/model';
const modelList = () => {
const { toast } = useToast();
const { isPc } = useScreen();
const router = useRouter();
const { myModels, getMyModels } = useUserStore();
const { Loading, setIsLoading } = useLoading();
/* 加载模型 */
const { isLoading } = useQuery(['loadModels'], getMyModels);
const handleCreateModel = useCallback(async () => {
setIsLoading(true);
try {
const id = await postCreateModel({ name: `模型${myModels.length}` });
toast({
title: '创建成功',
status: 'success'
});
router.push(`/model/detail?modelId=${id}`);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err.message || '出现了意外',
status: 'error'
});
}
setIsLoading(false);
}, [myModels.length, router, setIsLoading, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(
async (modelId: string) => {
setIsLoading(true);
try {
router.push(`/chat?modelId=${modelId}`, undefined, {
shallow: true
});
} catch (err: any) {
console.log('error->', err);
toast({
title: err.message || '出现一些异常',
status: 'error'
});
}
setIsLoading(false);
},
[router, setIsLoading, toast]
);
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={handleCreateModel}>
</Button>
</Flex>
</Card>
{/* 表单 */}
<Box mt={5} position={'relative'}>
{isPc ? (
<ModelTable models={myModels} handlePreviewChat={handlePreviewChat} />
) : (
<ModelPhoneList models={myModels} handlePreviewChat={handlePreviewChat} />
)}
</Box>
<Loading loading={isLoading} />
</Box>
);
};
export default modelList;

View File

@@ -44,7 +44,7 @@ const ShareModelList = ({
<Flex
alignItems={'center'}
cursor={'pointer'}
color={model.isCollection ? 'blue.600' : 'alphaBlack.700'}
color={model.isCollection ? 'myBlue.700' : 'blackAlpha.700'}
onClick={() => onclickCollection(model._id)}
>
<MyIcon
@@ -58,7 +58,7 @@ const ShareModelList = ({
<Button
size={'sm'}
variant={'outline'}
w={'80px'}
w={['60px', '80px']}
onClick={() => router.push(`/chat?modelId=${model._id}`)}
>
@@ -67,8 +67,8 @@ const ShareModelList = ({
<Button
ml={4}
size={'sm'}
w={'80px'}
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
w={['60px', '80px']}
onClick={() => router.push(`/model?modelId=${model._id}`)}
>
</Button>

View File

@@ -4,7 +4,7 @@ import { useLoading } from '@/hooks/useLoading';
import { getShareModelList, triggerModelCollection, getCollectionModels } from '@/api/model';
import { usePagination } from '@/hooks/usePagination';
import type { ShareModelItem } from '@/types/model';
import { useUserStore } from '@/store/user';
import ShareModelList from './components/list';
import { useQuery } from '@tanstack/react-query';
@@ -12,6 +12,7 @@ const modelList = () => {
const { Loading } = useLoading();
const lastSearch = useRef('');
const [searchText, setSearchText] = useState('');
const { refreshModel } = useUserStore();
/* 加载模型 */
const { data, isLoading, Pagination, getData, pageNum } = usePagination<ShareModelItem>({
@@ -41,15 +42,16 @@ const modelList = () => {
await triggerModelCollection(modelId);
getData(pageNum);
refetchCollection();
refreshModel.removeModelDetail(modelId);
} catch (error) {
console.log(error);
}
},
[getData, pageNum, refetchCollection]
[getData, pageNum, refetchCollection, refreshModel]
);
return (
<>
<Box py={[5, 10]} px={'5vw'}>
<Card px={6} py={3}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'} fontSize={'xl'}>
@@ -105,7 +107,7 @@ const modelList = () => {
</Card>
<Loading loading={isLoading} />
</>
</Box>
);
};