feat: app detail

This commit is contained in:
archer
2023-07-13 15:07:13 +08:00
parent 6c72c20317
commit b4d46ff34d
47 changed files with 1088 additions and 1091 deletions

View File

@@ -1,97 +1,34 @@
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Flex, Button, FormControl, Input, Textarea, Divider } from '@chakra-ui/react';
import { Box, Flex, Button, Grid, useTheme, BoxProps, IconButton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { delModelById, putAppById } from '@/api/app';
import { useSelectFile } from '@/hooks/useSelectFile';
import { compressImg } from '@/utils/file';
import { getErrText } from '@/utils/tools';
import { delModelById } from '@/api/app';
import { useConfirm } from '@/hooks/useConfirm';
import type { AppSchema } from '@/types/mongoSchema';
import dynamic from 'next/dynamic';
import { AppSchema } from '@/types/mongoSchema';
import Avatar from '@/components/Avatar';
import MyIcon from '@/components/Icon';
const InfoModal = dynamic(() => import('./InfoModal'));
const TokenUsage = dynamic(() => import('./Charts/TokenUsage'));
const AppEdit = dynamic(() => import('./edit'));
import styles from '../../list/index.module.scss';
const Settings = ({ appId }: { appId: string }) => {
const theme = useTheme();
const { toast } = useToast();
const router = useRouter();
const { Loading, setIsLoading } = useLoading();
const { userInfo, appDetail, myApps, loadAppDetail, setLastModelId } = useUserStore();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { appDetail, loadAppDetail } = useUserStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该应用?'
});
const [btnLoading, setBtnLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const {
register,
setValue,
getValues,
formState: { errors },
reset,
handleSubmit
} = useForm({
defaultValues: appDetail
});
const isOwner = useMemo(
() => appDetail.userId === userInfo?._id,
[appDetail.userId, userInfo?._id]
);
// 提交保存模型修改
const saveSubmitSuccess = useCallback(
async (data: AppSchema) => {
setBtnLoading(true);
try {
await putAppById(data._id, {
name: data.name,
avatar: data.avatar,
intro: data.intro,
chat: data.chat,
share: data.share
});
} catch (err: any) {
toast({
title: err?.message || '更新失败',
status: 'error'
});
}
setBtnLoading(false);
},
[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(errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [errors, toast]);
const saveUpdateModel = useCallback(
() => handleSubmit(saveSubmitSuccess, saveSubmitError)(),
[handleSubmit, saveSubmitError, saveSubmitSuccess]
);
const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>();
const [fullScreen, setFullScreen] = useState(false);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
@@ -113,156 +50,126 @@ const Settings = ({ appId }: { appId: string }) => {
setIsLoading(false);
}, [appDetail, setIsLoading, toast, router]);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImg({
file,
maxW: 100,
maxH: 100
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, '头像选择异常'),
status: 'warning'
});
}
},
[setValue, toast]
);
// load model data
const { isLoading } = useQuery([appId], () => loadAppDetail(appId, true), {
onSuccess(res) {
res && reset(res);
appId && setLastModelId(appId);
setRefresh(!refresh);
},
// load app data
const { isLoading, refetch } = useQuery([appId], () => loadAppDetail(appId, true), {
onError(err: any) {
toast({
title: err?.message || '获取应用异常',
status: 'error'
});
setLastModelId('');
router.replace('/model');
router.replace('/app/list');
},
onSettled() {
router.prefetch(`/chat?appId=${appId}`);
}
});
return (
<Box
pt={[0, 5]}
pb={3}
px={[5, '25px', '50px']}
fontSize={['sm', 'lg']}
position={'relative'}
maxW={['auto', '800px']}
>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
</Box>
<Flex mt={5} alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Avatar
src={getValues('avatar')}
w={['32px', '40px']}
h={['32px', '40px']}
cursor={isOwner ? 'pointer' : 'default'}
title={'点击切换头像'}
onClick={() => isOwner && onOpenSelectFile()}
/>
</Flex>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
<Flex h={'100%'} flexDirection={'column'} position={'relative'}>
<Box w={'100%'} pt={[0, 7]} px={[2, 5, 8]}>
<Grid gridTemplateColumns={['1fr', 'repeat(2,1fr)']} gridGap={[2, 4, 6]}>
<Box>
<Box mb={2} fontSize={['md', 'xl']}>
</Box>
<Box
border={theme.borders.sm}
borderRadius={'lg'}
px={5}
py={4}
bg={'rgba(235,245,255,0.4)'}
position={'relative'}
>
<Flex alignItems={'center'} py={2}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
{appDetail.name}
</Box>
<IconButton
className="delete"
position={'absolute'}
top={4}
right={4}
size={'sm'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'base'}
borderRadius={'md'}
aria-label={'delete'}
_hover={{
bg: 'myGray.100',
color: 'red.600'
}}
onClick={openConfirm(handleDelModel)}
/>
</Flex>
<Box className={styles.intro} py={3} wordBreak={'break-all'} color={'myGray.600'}>
{appDetail.intro || '快来给应用一个介绍~'}
</Box>
<Flex>
<Button
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
</Button>
<Button
mx={3}
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'shareLight'} w={'16px'} />}
onClick={() => {
router.replace({
query: {
appId,
currentTab: 'share'
}
});
}}
>
</Button>
<Button
size={['sm', 'md']}
variant={'base'}
leftIcon={<MyIcon name={'settingLight'} w={'16px'} />}
onClick={() => setSettingAppInfo(appDetail)}
>
</Button>
</Flex>
</Box>
</Box>
<Input
isDisabled={!isOwner}
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<Flex mt={5} alignItems={'flex-start'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}>
</Box>
<Textarea
rows={4}
maxLength={500}
placeholder={'给你的 AI 应用一个介绍'}
{...register('intro')}
></Textarea>
</Flex>
<Box>
<Box mb={2} fontSize={['md', 'xl']}>
7 Tokens
</Box>
<Box h={'150px'}>
<TokenUsage appId={appId} />
</Box>
</Box>
</Grid>
</Box>
<Box flex={'1 0 0'} position={'relative'}>
<AppEdit
app={appDetail}
onFullScreen={(val) => setFullScreen(val)}
fullScreen={fullScreen}
/>
</Box>
<Divider mt={5} />
{settingAppInfo && (
<InfoModal
defaultApp={settingAppInfo}
onClose={() => setSettingAppInfo(undefined)}
onSuccess={refetch}
/>
)}
<Flex mt={5} alignItems={'center'}>
<Box w={['60px', '100px', '140px']} flexShrink={0}></Box>
<Button
mr={3}
w={'120px'}
size={['sm', 'md']}
isLoading={btnLoading}
isDisabled={!isOwner}
onClick={async () => {
try {
await saveUpdateModel();
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {
console.log(error);
error;
}
}}
>
{isOwner ? '保存' : '仅读,无法修改'}
</Button>
<Button
mr={3}
w={'100px'}
size={['sm', 'md']}
variant={'base'}
color={'myBlue.600'}
borderColor={'myBlue.600'}
isLoading={btnLoading}
onClick={async () => {
try {
router.prefetch('/chat');
await saveUpdateModel();
} catch (error) {}
router.push(`/chat?appId=${appId}`);
}}
>
</Button>
{isOwner && (
<Button
colorScheme={'gray'}
variant={'base'}
size={['sm', 'md']}
isLoading={btnLoading}
_hover={{ color: 'red.600' }}
onClick={openConfirm(handleDelModel)}
>
</Button>
)}
</Flex>
<File onSelect={onSelectFile} />
<ConfirmChild />
<Loading loading={isLoading} fixed={false} />
</Box>
</Flex>
);
};