sub plan page (#885)

* perf: insert mongo dataset data session

* perf: dataset data index

* remove delay

* rename bill schema

* rename bill record

* perf: bill table

* perf: prompt

* perf: sub plan

* change the usage count

* feat: usage bill

* publish usages

* doc

* 新增团队聊天功能 (#20)

* perf: doc

* feat 添加标签部分

feat 信息团队标签配置

feat 新增团队同步管理

feat team分享页面

feat 完成team分享页面

feat 实现模糊搜索

style 格式化

fix 修复迷糊匹配

style 样式修改

fix 团队标签功能修复

* fix 修复鉴权功能

* merge 合并代码

* fix 修复引用错误

* fix 修复pr问题

* fix 修复ts格式问题

---------

Co-authored-by: archer <545436317@qq.com>
Co-authored-by: liuxingwan <liuxingwan.lxw@alibaba-inc.com>

* update extra plan

* fix: ts

* format

* perf: bill field

* feat: standard plan

* fix: ts

* feat 个人账号页面修改 (#22)

* feat 添加标签部分

feat 信息团队标签配置

feat 新增团队同步管理

feat team分享页面

feat 完成team分享页面

feat 实现模糊搜索

style 格式化

fix 修复迷糊匹配

style 样式修改

fix 团队标签功能修复

* fix 修复鉴权功能

* merge 合并代码

* fix 修复引用错误

* fix 修复pr问题

* fix 修复ts格式问题

* feat 修改个人账号页

---------

Co-authored-by: liuxingwan <liuxingwan.lxw@alibaba-inc.com>

* fix chunk index; error page text

* feat: dataset process Integral prediction

* feat: stand plan field

* feat: sub plan limit

* perf: index

* query extension

* perf: share link push app name

* perf: plan point unit

* perf: get sub plan

* perf: account page

---------

Co-authored-by: yst <77910600+yu-and-liu@users.noreply.github.com>
Co-authored-by: liuxingwan <liuxingwan.lxw@alibaba-inc.com>
This commit is contained in:
Archer
2024-02-23 17:47:34 +08:00
committed by GitHub
parent 7a87f13aa8
commit 443ad37b6a
246 changed files with 6277 additions and 4272 deletions

View File

@@ -46,7 +46,7 @@ type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
name: '',
limit: {
credit: -1
maxUsagePoints: -1
}
};
@@ -153,16 +153,16 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ _id, name, usage, limit, apiKey, createTime, lastUsedTime }) => (
{apiKeys.map(({ _id, name, usagePoints, limit, apiKey, createTime, lastUsedTime }) => (
<Tr key={_id}>
<Td>{name}</Td>
<Td>{apiKey}</Td>
<Td>{usage}</Td>
<Td>{Math.round(usagePoints)}</Td>
{feConfigs?.isPlus && (
<>
<Td>
{limit?.credit && limit?.credit > -1
? `${limit?.credit}`
{limit?.maxUsagePoints && limit?.maxUsagePoints > -1
? `${limit?.maxUsagePoints}`
: t('common.Unlimited')}
</Td>
<Td whiteSpace={'pre-wrap'}>
@@ -334,15 +334,15 @@ function EditKeyModal({
<>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Max credit')}:
<MyTooltip label={t('common.Max credit tips' || '')}>
{t('support.outlink.Max usage points')}:
<MyTooltip label={t('support.outlink.Max usage points tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
{...register('limit.credit', {
{...register('limit.maxUsagePoints', {
min: -1,
max: 1000,
max: 10000000,
valueAsNumber: true,
required: true
})}

View File

@@ -2,13 +2,16 @@ import React, { useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { DragHandleIcon } from '@chakra-ui/icons';
import {
getTeamList,
getTeamMembers,
putSwitchTeam,
putUpdateMember,
delRemoveMember,
delLeaveTeam
delLeaveTeam,
getTeamsTags,
insertTeamsTags
} from '@/web/support/user/team/api';
import {
Box,
@@ -20,12 +23,15 @@ import {
Tbody,
Tr,
Th,
Tag,
Td,
TableContainer,
useTheme,
useDisclosure,
MenuButton
MenuButton,
HStack
} from '@chakra-ui/react';
import { SpinnerIcon } from '@chakra-ui/icons';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { useUserStore } from '@/web/support/user/useUserStore';
@@ -46,12 +52,14 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
const EditModal = dynamic(() => import('./EditModal'));
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagsAsync = dynamic(() => import('../TeamTagsAsync'));
const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const theme = useTheme();
const { t } = useTranslation();
const { Loading } = useLoading();
const { toast } = useToast();
const [teamsTags, setTeamTags] = useState<any>();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
@@ -61,6 +69,11 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const { userInfo, initUserInfo } = useUserStore();
const [editTeamData, setEditTeamData] = useState<FormDataType>();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const {
data: myTeams = [],
@@ -76,6 +89,8 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
mutationFn: async (teamId: string) => {
const token = await putSwitchTeam(teamId);
token && setToken(token);
// get team tags
await getTeamsTags(teamId);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
@@ -86,6 +101,11 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
['getMembers', userInfo?.team?.teamId],
() => {
if (!userInfo?.team?.teamId) return [];
// get team tags
getTeamsTags(userInfo.team.teamId).then((res: any) => {
setTeamTags(res);
});
return getTeamMembers(userInfo.team.teamId);
}
);
@@ -108,7 +128,9 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
mutationFn: async (teamId?: string) => {
if (!teamId) return;
// change to personal team
// get members
await onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
@@ -184,6 +206,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
bg: 'myGray.100'
}
})}
onClick={() => onSwitchTeam(team.teamId)}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
@@ -196,6 +219,17 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
: {})}
>
{team.teamName}
{/* {userInfo?.team?.teamId === team.teamId && (
<HStack spacing={1}>
{teamsTags.slice(0, 3).map((item: any, index) => {
return (
<Tag key={index} size={'sm'} variant="outline" colorScheme="blue">
{item.label}
</Tag>
);
})}
</HStack>
)} */}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
@@ -229,7 +263,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
borderBottomColor={'myGray.100'}
mb={3}
>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'}>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'} alignItems={'center'}>
{userInfo.team.teamName}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
@@ -279,6 +313,27 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
{t('user.team.Invite Member')}
</Button>
)}
{userInfo.team.role === TeamMemberRoleEnum.owner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
if (userInfo.team.maxSize <= members.length) {
toast({
status: 'warning',
title: t('user.team.Team Tags Async', { max: userInfo.team.maxSize })
});
} else {
onOpenTeamTagsAsync();
}
}}
>
{t('user.team.Team Tags Async')}
</Button>
)}
<Box flex={1} />
{userInfo.team.role !== TeamMemberRoleEnum.owner && (
<Button
@@ -435,6 +490,13 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && (
<TeamTagsAsync
teamInfo={teamsTags?.tagsUrl}
teamsTags={teamsTags?.list || []}
onClose={onCloseTeamTagsAsync}
/>
)}
<ConfirmRemoveMemberModal />
<ConfirmLeaveTeamModal />
</>

View File

@@ -0,0 +1,182 @@
import React, { useEffect, useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import {
Box,
Button,
Flex,
ModalBody,
Tag,
ModalFooter,
Input,
HStack,
Avatar
} from '@chakra-ui/react';
import { AttachmentIcon, CopyIcon, DragHandleIcon } from '@chakra-ui/icons';
import { putUpdateTeamTags, updateTags } from '@/web/support/user/team/api';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import type { TeamTagsSchema } from '@fastgpt/global/support/user/team/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import { RepeatIcon } from '@chakra-ui/icons';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useCopyData } from '@/web/common/hooks/useCopyData';
const TeamTagsAsync = ({
teamsTags,
teamInfo,
onClose
}: {
teamsTags: Array<TeamTagsSchema>;
teamInfo: any;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [_teamsTags, setTeamTags] = useState<Array<TeamTagsSchema>>(teamsTags);
const { register, setValue, getValues, handleSubmit } = useForm<any>({
defaultValues: { ...teamInfo }
});
const { copyData } = useCopyData();
const baseUrl = global.feConfigs?.customSharePageDomain || location?.origin;
const linkUrl = `${baseUrl}/chat/team?teamId=${teamInfo?._id}${
getValues('showHistory') ? '' : '&showHistory=0'
}`;
// tags Async
const { mutate: onclickAsync, isLoading: creating } = useRequest({
mutationFn: async (data: any) => {
return putUpdateTeamTags({ tagsUrl: data.tagsUrl, teamId: teamInfo?._id });
},
onSuccess(id: string) {
onClose();
},
successToast: t('user.team.Team Tags Async Success'),
errorToast: t('common.Create Failed')
});
const asyncTags = async () => {
console.log('getValues', getValues());
const res: Array<TeamTagsSchema> = await updateTags(teamInfo?._id, getValues().tagsUrl);
setTeamTags(res);
toast({ status: 'success', title: '团队标签同步成功' });
};
useEffect(() => {
console.log('teamInfo', teamInfo);
}, []);
// 获取
return (
<>
<MyModal
isOpen
onClose={onClose}
maxW={['70vw', '1000px']}
w={'100%'}
h={'550px'}
iconSrc="/imgs/modal/team.svg"
isCentered
bg={'white'}
overflow={'hidden'}
title={
<Box>
<Box>{teamInfo?.name}</Box>
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
{'填写标签同步链接,点击同步按钮即可同步'}
</Box>
</Box>
}
>
<ModalBody style={{ padding: '10rpx' }}>
<Flex mt={3} alignItems={'center'}>
<Box mb={2} fontWeight="semibold">
{t('同步链接')}
</Box>
<Input
flex={1}
ml={4}
autoFocus
bg={'myWhite.600'}
placeholder="请输入同步标签"
{...register('tagsUrl', {
required: t('core.app.error.App name can not be empty')
})}
/>
</Flex>
<Flex mt={3} alignItems={'center'}>
<Box mb={2} fontWeight="semibold">
{t('分享链接')}
</Box>
{/* code */}
<Box ml={4} borderRadius={'md'} overflow={'hidden'}>
<Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'} bg={'myWhite.600'} color="blue">
{linkUrl}
</Box>
<MyIcon
name={'copy'}
w={'16px'}
p={3}
bg={'primary.500'}
color={'myWhite.600'}
cursor={'pointer'}
_hover={{ bg: 'primary.400' }}
onClick={() => {
copyData(linkUrl);
}}
/>
</Flex>
</Box>
</Flex>
<Flex mt={3} alignItems={'center'}>
<Box mb={2} fontWeight="semibold">
{t('标签列表')}
</Box>
<HStack
ml={4}
maxHeight={'250'}
bg={'myWhite.600'}
style={{
border: 'solid 2px #f3f3f377',
borderRadius: '5px',
padding: '10px',
maxWidth: '70%',
flexWrap: 'wrap',
overflow: 'scroll'
}}
spacing={1}
>
{_teamsTags.map((item, index) => {
return (
<Tag key={index} mt={2} size={'md'} colorScheme="red" borderRadius="full">
<Avatar
src="https://bit.ly/sage-adeb"
size="xs"
name={item.label}
ml={-2}
mr={2}
/>
{item.label}
</Tag>
);
})}
</HStack>
<Button ml={4} size="md" leftIcon={<RepeatIcon />} onClick={asyncTags}>
</Button>
</Flex>
</ModalBody>
<ModalFooter mb={2}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button isLoading={creating} onClick={handleSubmit((data) => onclickAsync(data))}>
{t('user.team.Tags Async')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default React.memo(TeamTagsAsync);

View File

@@ -1,97 +0,0 @@
import React from 'react';
import { Box, CloseButton } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import ReactDOM from 'react-dom';
import Markdown from '@/components/Markdown';
const Price = ({ onClose }: { onClose: () => void }) => {
const { llmModelList, vectorModelList, audioSpeechModelList, whisperModel } = useSystemStore();
const list = [
{
title: 'AI语言模型',
describe: '',
md: `
| 模型 | 输入价格(¥) | 输出价格(¥) |
| --- | --- | --- |
${llmModelList
?.map((item) => `| ${item.name} | ${item.inputPrice}/1k tokens | ${item.outputPrice}/1k tokens |`)
.join('\n')}`
},
{
title: '索引模型(文档训练 & 文档检索)',
describe: '',
md: `
| 模型 | 价格(¥) |
| --- | --- |
${vectorModelList?.map((item) => `| ${item.name} | ${item.inputPrice}/1k 字符 |`).join('\n')}
`
},
{
title: '语音播放',
describe: '',
md: `
| 模型 | 价格(¥) |
| --- | --- |
${audioSpeechModelList
?.map((item) => `| ${item.name} | ${item.inputPrice}/1k 字符 | - |`)
.join('\n')}`
},
...(whisperModel
? [
{
title: '语音输入',
describe: '',
md: `
| 模型 | 价格(¥) |
| --- | --- |
| ${whisperModel.name} | ${whisperModel.inputPrice}/分钟 | - |`
}
]
: [])
];
return ReactDOM.createPortal(
<Box position={'fixed'} top={0} right={0} bottom={0} left={0} zIndex={99999} bg={'white'}>
<CloseButton
position={'absolute'}
top={'10px'}
right={'20px'}
bg={'myGray.200'}
w={'30px'}
h={'30px'}
borderRadius={'50%'}
onClick={onClose}
/>
<Box overflow={'overlay'} h={'100%'}>
<Box py={[0, 10]} px={5} mx={'auto'} maxW={'1200px'}>
{list.map((item) => (
<Box
display={['block', 'flex']}
key={item.title}
w={'100%'}
mb={4}
pb={6}
_notLast={{
borderBottom: '1px',
borderBottomColor: 'borderColor.high'
}}
>
<Box fontSize={'xl'} fontWeight={'bold'} mb={1} flex={'1 0 0'}>
{item.title}
</Box>
<Box w={['100%', '410px']}>
<Markdown source={item.md}></Markdown>
</Box>
</Box>
))}
</Box>
</Box>
</Box>,
// @ts-ignore
document.querySelector('body')
);
};
export default Price;

View File

@@ -0,0 +1,84 @@
import MyModal from '@/components/MyModal';
import React, { useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, ModalBody, ModalFooter } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { checkBalancePayResult } from '@/web/support/wallet/bill/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router';
import { getErrText } from '@fastgpt/global/common/error/utils';
export type QRPayProps = {
readPrice: number;
codeUrl: string;
billId: string;
};
const QRCodePayModal = ({
readPrice,
codeUrl,
billId,
onSuccess
}: QRPayProps & { onSuccess?: () => any }) => {
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const dom = document.getElementById('payQRCode');
useEffect(() => {
if (dom && window.QRCode) {
new window.QRCode(dom, {
text: codeUrl,
width: 128,
height: 128,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
}, [dom]);
useQuery(
[billId],
() => {
if (!billId) return null;
return checkBalancePayResult(billId);
},
{
enabled: !!billId,
refetchInterval: 3000,
onSuccess: async (res) => {
if (!res) return;
try {
await onSuccess?.();
toast({
title: res,
status: 'success'
});
} catch (error) {
toast({
title: getErrText(error),
status: 'error'
});
}
setTimeout(() => {
router.reload();
}, 1000);
}
}
);
return (
<MyModal isOpen title={t('user.Pay')} iconSrc="/imgs/modal/pay.svg">
<ModalBody textAlign={'center'}>
<Box mb={3}>: {readPrice}</Box>
<Box id={'payQRCode'} display={'inline-block'} h={'128px'}></Box>
</ModalBody>
<ModalFooter />
</MyModal>
);
};
export default React.memo(QRCodePayModal);

View File

@@ -0,0 +1,116 @@
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/wallet/sub/constants';
import React, { useMemo } from 'react';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { Box, Flex, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
const StandardPlanContentList = ({
level,
mode
}: {
level: `${StandardSubLevelEnum}`;
mode: `${SubModeEnum}`;
}) => {
const { t } = useTranslation();
const { subPlans } = useSystemStore();
const planContent = useMemo(() => {
const plan = subPlans?.standard?.[level];
if (!plan) return;
return {
price: plan.price * (mode === SubModeEnum.month ? 1 : 10),
level: level as `${StandardSubLevelEnum}`,
...standardSubLevelMap[level as `${StandardSubLevelEnum}`],
maxTeamMember: plan.maxTeamMember,
maxAppAmount: plan.maxAppAmount,
maxDatasetAmount: plan.maxDatasetAmount,
chatHistoryStoreDuration: plan.chatHistoryStoreDuration,
maxDatasetSize: plan.maxDatasetSize,
permissionCustomApiKey: plan.permissionCustomApiKey,
permissionCustomCopyright: plan.permissionCustomCopyright,
trainingWeight: plan.trainingWeight,
permissionReRank: plan.permissionReRank,
totalPoints: plan.totalPoints * (mode === SubModeEnum.month ? 1 : 12),
permissionWebsiteSync: plan.permissionWebsiteSync
};
}, [subPlans?.standard, level, mode]);
return planContent ? (
<Grid gap={4}>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max members', {
amount: planContent.maxTeamMember
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max app', {
amount: planContent.maxAppAmount
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max dataset', {
amount: planContent.maxDatasetAmount
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.History store', {
amount: planContent.chatHistoryStoreDuration
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box fontWeight={'bold'}>
{t('support.wallet.subscription.function.Max dataset size', {
amount: planContent.maxDatasetSize
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>
{t('support.wallet.subscription.function.Points', {
amount: planContent.totalPoints
})}
</Box>
</Flex>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.Training weight', {
weight: planContent.trainingWeight
})}
</Box>
</Flex>
{!!planContent.permissionReRank && (
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}></Box>
</Flex>
)}
{!!planContent.permissionWebsiteSync && (
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>Web站点同步</Box>
</Flex>
)}
</Grid>
) : null;
};
export default StandardPlanContentList;

View File

@@ -1,240 +0,0 @@
import React, { useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import {
Box,
Flex,
ModalBody,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
ModalFooter,
Button
} from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import {
getTeamDatasetValidSub,
posCheckTeamDatasetSizeSub,
postUpdateTeamDatasetSizeSub,
putTeamDatasetSubStatus
} from '@/web/support/wallet/sub/api';
import Markdown from '@/components/Markdown';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import MySelect from '@/components/Select';
import {
SubStatusEnum,
SubTypeEnum,
subSelectMap
} from '@fastgpt/global/support/wallet/sub/constants';
import { SubDatasetSizePreviewCheckResponse } from '@fastgpt/global/support/wallet/sub/api.d';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { useUserStore } from '@/web/support/user/useUserStore';
const SubDatasetModal = ({ onClose }: { onClose: () => void }) => {
const { subPlans } = useSystemStore();
const datasetStorePrice = subPlans?.extraDatasetSize?.price || 0;
const { t } = useTranslation();
const router = useRouter();
const { ConfirmModal, openConfirm } = useConfirm({});
const { userInfo } = useUserStore();
const [datasetSize, setDatasetSize] = useState(0);
const [isRenew, setIsRenew] = useState('false');
const { data: teamSubPlan } = useQuery(['getTeamDatasetValidSub'], getTeamDatasetValidSub, {
onSuccess(res) {
setIsRenew(res?.extraDatasetSize?.status === SubStatusEnum.active ? 'true' : 'false');
setDatasetSize((res?.extraDatasetSize?.nextExtraDatasetSize || 0) / 1000);
}
});
const { mutate: onClickUpdateSub, isLoading: isPaying } = useRequest({
mutationFn: () => postUpdateTeamDatasetSizeSub({ size: datasetSize }),
onSuccess() {
setTimeout(() => {
router.reload();
}, 100);
},
successToast: t('common.Update success'),
errorToast: t('common.error.Update error')
});
const { mutate: onClickPreviewCheck, isLoading: isFetchingPreviewCheck } = useRequest({
mutationFn: () =>
posCheckTeamDatasetSizeSub({
size: datasetSize
}),
onSuccess(res: SubDatasetSizePreviewCheckResponse) {
if (!res.payForNewSub) {
onClickUpdateSub('');
return;
} else {
openConfirm(
() => {
if (!res.balanceEnough) return;
onClickUpdateSub('');
},
undefined,
<Box>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{teamSubPlan?.extraDatasetSize?.currentExtraDatasetSize || 0}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{res.newSubSize}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{formatStorePrice2Read(res.newPlanPrice)}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{formatStorePrice2Read(res.payPrice)}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>30</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</Box>
</Flex>
{!res.balanceEnough && (
<Box mt={1} color={'red.600'}>
</Box>
)}
</Box>
)();
}
},
errorToast: t('common.error.Update error')
});
const { mutate: onUpdateStatus } = useRequest({
mutationFn: (e: 'true' | 'false') => {
setIsRenew(e);
return putTeamDatasetSubStatus({
status: subSelectMap[e],
type: SubTypeEnum.extraDatasetSize
});
},
successToast: t('common.Update success'),
errorToast: t('common.error.Update error')
});
const isLoading = isPaying || isFetchingPreviewCheck;
return (
<MyModal
isOpen
iconSrc="/imgs/module/db.png"
title={t('support.wallet.subscription.Dataset store')}
>
<ModalBody>
<>
<Flex alignItems={'center'}>
{t('support.user.Price')}
<MyTooltip label={t('support.wallet.subscription.Dataset store price tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Markdown
source={`
| 套餐知识库容量 | ${teamSubPlan?.standardMaxDatasetSize || Infinity}条 |
| --- | --- |
| 额外知识库 | ${datasetStorePrice}元/1000条/月 |
`}
/>
</>
<Flex mt={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Current dataset store')}: </Box>
<Box ml={2} fontWeight={'bold'} flex={1}>
{teamSubPlan?.extraDatasetSize?.currentExtraDatasetSize || 0}
{t('core.dataset.data.unit')}
</Box>
</Flex>
{teamSubPlan?.extraDatasetSize?.nextExtraDatasetSize !== undefined && (
<Flex mt={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Next sub dataset size')}: </Box>
<Box ml={2} fontWeight={'bold'} flex={1}>
{teamSubPlan?.extraDatasetSize?.nextExtraDatasetSize || 0}
{t('core.dataset.data.unit')}
</Box>
</Flex>
)}
{!!teamSubPlan?.extraDatasetSize?.startTime && (
<Flex mt={3}>
<Box flex={'0 0 120px'}>: </Box>
<Box ml={2}>{formatTime2YMDHM(teamSubPlan?.extraDatasetSize?.startTime)}</Box>
</Flex>
)}
{!!teamSubPlan?.extraDatasetSize?.expiredTime && (
<Flex mt={3}>
<Box flex={'0 0 120px'}>: </Box>
<Box ml={2}>{formatTime2YMDHM(teamSubPlan?.extraDatasetSize?.expiredTime)}</Box>
</Flex>
)}
<Flex mt={3} alignItems={'center'}>
<Box flex={'0 0 120px'}>: </Box>
<MySelect
ml={2}
value={isRenew}
size={'sm'}
w={'150px'}
list={[
{ label: '自动续费', value: 'true' },
{ label: '不自动续费', value: 'false' }
]}
onchange={onUpdateStatus}
/>
</Flex>
<Box mt={4}>
<Box>{t('support.wallet.subscription.Update extra dataset size')}</Box>
<Flex alignItems={'center'} mt={1}>
<NumberInput
flex={1}
min={0}
max={10000}
step={1}
value={datasetSize}
position={'relative'}
onChange={(e) => {
setDatasetSize(Number(e));
}}
>
<NumberInputField value={datasetSize} step={1} min={0} max={10000} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<Box ml={2}>000{t('core.dataset.data.unit')}</Box>
</Flex>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Close')}
</Button>
{datasetSize * 1000 !== teamSubPlan?.extraDatasetSize?.nextExtraDatasetSize && (
<Button ml={3} isLoading={isLoading} onClick={onClickPreviewCheck}>
{t('common.Confirm')}
</Button>
)}
</ModalFooter>
<ConfirmModal />
</MyModal>
);
};
export default SubDatasetModal;