feat: app detail
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user