V4.8.15 feature (#3331)

* feat: add customize toolkit (#3205)

* chaoyang

* fix-auth

* add toolkit

* add order

* plugin usage

* fix

* delete console:

* Fix: Fix fullscreen preview top positioning and improve Markdown rendering logic (#3247)

* 完成任务:修复全屏预览顶部固定问题,优化 Markdown 渲染逻辑

* 有问题修改

* 问题再修改

* 修正问题

* fix: plugin standalone display issue (#3254)

* 4.8.15 test (#3246)

* o1 config

* perf: system plugin code

* 调整系统插件代码。增加html 渲染安全配置。 (#3258)

* perf: base64 picker

* perf: list app or dataset

* perf: plugin config code

* 小窗适配等问题 (#3257)

* 小窗适配等问题

* git问题

* 小窗剩余问题

* feat: system plugin auth and lock version (#3265)

* feat: system plugin auth and lock version

* update comment

* 4.8.15 test (#3267)

* tmp log

* perf: login direct

* perf: iframe html code

* remove log

* fix: plugin standalone display (#3277)

* refactor: 页面拆分&i18n拆分 (#3281)

* refactor: account组件拆成独立页面

* script: 新增i18n json文件创建脚本

* refactor: 页面i18n拆分

* i18n: add en&hant

* 4.8.15 test (#3285)

* tmp log

* remove log

* fix: watch avatar refresh

* perf: i18n code

* fix(plugin): use intro instead of userguide (#3290)

* Universal SSO (#3292)

* tmp log

* remove log

* feat: common oauth

* readme

* perf: sso provider

* remove sso code

* perf: refresh plugins

* feat: add api dataset (#3272)

* add api-dataset

* fix api-dataset

* fix api dataset

* fix ts

* perf: create collection code (#3301)

* tmp log

* remove log

* perf: i18n change

* update version doc

* feat: question guide from chatId

* perf: create collection code

* fix: request api

* fix: request api

* fix: tts auth and response type (#3303)

* perf: md splitter

* fix: tts auth and response type

* fix: api file dataset (#3307)

* perf: api dataset init (#3310)

* perf: collection schema

* perf: api dataset init

* refactor: 团队管理独立页面 (#3302)

* ui: 团队管理独立页面

* 代码优化

* fix

* perf: sync collection and ui check (#3314)

* perf: sync collection

* remove script

* perf: update api server

* perf: api dataset parent

* perf: team ui

* perf: team 18n

* update team ui

* perf: ui check

* perf: i18n

* fix: debug variables & cronjob & system plugin callback load (#3315)

* fix: debug variables & cronjob & system plugin callback load

* fix type

* fix

* fix

* fix: plugin dataset quote;perf: system variables init (#3316)

* fix: plugin dataset quote

* perf: system variables init

* perf: node templates ui;fix: dataset import ui (#3318)

* fix: dataset import ui

* perf: node templates ui

* perf: ui refresh

* feat:套餐改名和套餐跳转配置 (#3309)

* fixing:except Sidebar

* 去除了多余的代码

* 修正了套餐说明的代码

* 修正了误删除的show_git代码

* 修正了名字部分等代码

* 修正了问题,遗留了其他和ui讨论不一致的部分

* 4.8.15 test (#3319)

* remove log

* pref: bill ui

* pref: bill ui

* perf: log

* html渲染文档 (#3270)

* html渲染文档

* 文档有点小问题

* feat: doc (#3322)

* 集合重训练 (#3282)

* rebaser

* 一点补充

* 小问题

* 其他问题修正,删除集合保留文件的参数还没找到...

* reTraining

* delete uesless

* 删除了一行错误代码

* 集合重训练部分

* fixing

* 删除console代码

* feat: navbar item config (#3326)

* perf: custom navbar code;perf: retraining code;feat: api dataset and dataset api doc (#3329)

* feat: api dataset and dataset api doc

* perf: retraining code

* perf: custom navbar code

* fix: ts (#3330)

* fix: ts

* fix: ts

* retraining ui

* perf: api collection filter

* perf: retrining button

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
This commit is contained in:
Archer
2024-12-06 10:56:53 +08:00
committed by GitHub
parent b188544386
commit 1aebe5f185
307 changed files with 7383 additions and 3981 deletions

View File

@@ -0,0 +1,105 @@
import React, { useMemo } from 'react';
import { ModalBody, Box, Button, VStack, HStack, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import Icon from '@fastgpt/web/components/common/Icon';
import Tag from '@fastgpt/web/components/common/Tag';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { balanceConversion } from '@/web/support/wallet/bill/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { SUB_EXTRA_POINT_RATE } from '@fastgpt/global/support/wallet/bill/constants';
import { useRouter } from 'next/router';
const ConversionModal = ({
onClose,
onOpenContact
}: {
onClose: () => void;
onOpenContact: () => void;
}) => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const router = useRouter();
const points = useMemo(() => {
if (!userInfo?.team?.balance) return 0;
const balance = formatStorePrice2Read(userInfo?.team?.balance);
return Math.ceil((balance / 15) * SUB_EXTRA_POINT_RATE);
}, []);
const { runAsync: onConvert, loading } = useRequest2(balanceConversion, {
onSuccess() {
router.reload();
},
successToast: t('account_info:exchange_success'),
errorToast: t('account_info:exchange_failure')
});
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="support/bill/wallet"
iconColor="primary.600"
title={t('account_info:usage_balance')}
>
<ModalBody maxW={'450px'}>
<VStack px="2.25" gap={2} pb="6">
<HStack px="4" py="2" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" mr="1" />
<Box fontSize={'mini'} fontWeight={'500'}>
{t('account_info:usage_balance_notice')}
</Box>
</HStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('account_info:current_token_price')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
15/1000 {t('account_info:tokens')}/{t('account_info:month')}
</Box>
</VStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('account_info:balance')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
{formatStorePrice2Read(userInfo?.team?.balance)?.toFixed(2)}
</Box>
</VStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('account_info:you_can_convert')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
{points} {t('account_info:tokens')}
</Box>
<Tag fontSize={'xs'} fontWeight={'500'}>
{t('account_info:token_validity_period')}
</Tag>
</VStack>
<VStack mt="6">
<Button
variant={'primary'}
alignItems={'center'}
fontSize={'sm'}
minW={'10rem'}
onClick={onConvert}
isLoading={loading}
>
{t('account_info:exchange')}
</Button>
<Link fontSize={'sm'} color="primary" mt="2" onClick={onOpenContact}>
{t('account_info:contact_customer_service')}
</Link>
</VStack>
</VStack>
</ModalBody>
</MyModal>
);
};
export default ConversionModal;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import type { UserType } from '@fastgpt/global/support/user/type.d';
const OpenAIAccountModal = ({
defaultData,
onSuccess,
onClose
}: {
defaultData: UserType['openaiAccount'];
onSuccess: (e: UserType['openaiAccount']) => Promise<any>;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm({
defaultValues: defaultData
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: async (data: UserType['openaiAccount']) => onSuccess(data),
onSuccess(res) {
onClose();
},
errorToast: t('account_info:openai_account_setting_exception')
});
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="common/openai"
title={t('account_info:openai_account_configuration')}
>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('account_info:open_api_notice')}
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>
<Input flex={1} {...register('key')}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>BaseUrl:</Box>
<Input
flex={1}
{...register('baseUrl')}
placeholder={t('account_info:request_address_notice')}
></Input>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default OpenAIAccountModal;

View File

@@ -0,0 +1,120 @@
import React, { useCallback } from 'react';
import {
ModalBody,
Box,
Flex,
Input,
ModalFooter,
Button,
HStack,
useDisclosure
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateNotificationAccount } from '@/web/support/user/api';
import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
type FormType = {
account: string;
verifyCode: string;
};
const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { initUserInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const { register, handleSubmit, watch } = useForm<FormType>({
defaultValues: {
account: '',
verifyCode: ''
}
});
const account = watch('account');
const verifyCode = watch('verifyCode');
const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => {
return updateNotificationAccount(data);
},
{
onSuccess() {
initUserInfo();
onClose();
},
successToast: t('account_info:bind_notification_success'),
errorToast: t('account_info:bind_notification_error')
}
);
const { SendCodeBox } = useSendCode({ type: 'bindNotification' });
const placeholder = feConfigs?.bind_notification_method
?.map((item) => {
switch (item) {
case 'email':
return t('account_info:email_label');
case 'phone':
return t('account_info:phone_label');
}
})
.join('/');
return (
<>
<MyModal
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('account_info:notification_receiving_hint')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('account_info:bind_notification_hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('account_info:user_account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
placeholder={placeholder}
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('account_info:verification_code_required')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('account_info:code_required')}
></Input>
<SendCodeBox username={account} />
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default UpdateNotificationModal;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { updatePasswordByOld } from '@/web/support/user/api';
type FormType = {
oldPsw: string;
newPsw: string;
confirmPsw: string;
};
const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm<FormType>({
defaultValues: {
oldPsw: '',
newPsw: '',
confirmPsw: ''
}
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: (data: FormType) => {
if (data.newPsw !== data.confirmPsw) {
return Promise.reject(t('account_info:password_mismatch'));
}
return updatePasswordByOld(data);
},
onSuccess() {
onClose();
},
successToast: t('account_info:password_update_success'),
errorToast: t('account_info:password_update_error')
});
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="/imgs/modal/password.svg"
title={t('account_info:update_password')}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('account_info:old_password') + ':'}</Box>
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('account_info:new_password') + ':'}</Box>
<Input
flex={1}
type={'password'}
{...register('newPsw', {
required: true,
maxLength: {
value: 60,
message: t('account_info:password_length_error')
}
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('account_info:confirm_password') + ':'}</Box>
<Input
flex={1}
type={'password'}
{...register('confirmPsw', {
required: true,
maxLength: {
value: 60,
message: t('account_info:password_length_error')
}
})}
></Input>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default UpdatePswModal;

View File

@@ -0,0 +1,193 @@
import React, { useMemo } from 'react';
import {
ModalBody,
ModalFooter,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
ModalCloseButton,
HStack,
Box,
Flex
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { getTeamPlans } from '@/web/support/user/team/api';
import {
subTypeMap,
standardSubLevelMap,
SubTypeEnum
} from '@fastgpt/global/support/wallet/sub/constants';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
type packageStatus = 'active' | 'inactive' | 'expired';
const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const { subPlans } = useSystemStore();
const { data: teamPlans = [], loading: isLoading } = useRequest2(
() =>
getTeamPlans().then((res) => {
return [
...res.filter((plan) => plan.type === SubTypeEnum.standard),
...res.filter((plan) => plan.type === SubTypeEnum.extraDatasetSize),
...res.filter((plan) => plan.type === SubTypeEnum.extraPoints)
].map((item, index) => {
return {
...item,
status:
new Date(item.expiredTime).getTime() < new Date().getTime()
? 'expired'
: item.type === SubTypeEnum.standard
? index === 0
? 'active'
: 'inactive'
: 'active'
};
});
}),
{
manual: false
}
);
return (
<MyModal
isOpen
maxW={['90vw', '1200px']}
iconSrc="modal/teamPlans"
title={t('account_info:package_details')}
isCentered
>
<ModalCloseButton onClick={onClose} />
<ModalBody px={[4, 8]} py={[2, 6]}>
<TableContainer mt={2} position={'relative'} minH={'300px'}>
<Table>
<Thead>
<Tr>
<Th>{t('account_info:type')}</Th>
<Th>{t('account_info:storage_capacity')}</Th>
<Th>{t('account_info:ai_points')}</Th>
<Th>{t('account_info:effective_time')}</Th>
<Th>{t('account_info:expiration_time')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{teamPlans.map(
({
_id,
type,
currentSubLevel,
currentExtraDatasetSize,
surplusPoints = 0,
totalPoints = 0,
startTime,
expiredTime,
status
}) => {
const standardPlan = currentSubLevel
? subPlans?.standard?.[currentSubLevel]
: undefined;
const datasetSize = standardPlan?.maxDatasetSize || currentExtraDatasetSize;
return (
<Tr key={_id} fontWeight={500} fontSize={'mini'} color={'myGray.900'}>
<Td>
<Flex>
<Flex align={'center'}>
<MyIcon
mr={2}
name={subTypeMap[type]?.icon as any}
w={'20px'}
h={'20px'}
color={'myGray.600'}
fontWeight={500}
/>
</Flex>
<Flex align={'center'} color={'myGray.900'}>
{t(subTypeMap[type]?.label as any)}
{currentSubLevel &&
`(${t(standardSubLevelMap[currentSubLevel]?.label as any)})`}
</Flex>
<StatusTag status={status as packageStatus} />
</Flex>
</Td>
<Td>{datasetSize ? `${datasetSize + t('account_info:group')}` : '-'}</Td>
<Td>
{totalPoints
? `${Math.round(totalPoints - surplusPoints)} / ${totalPoints} ${t('account_info:ai_points_calculation_standard')}`
: '-'}
</Td>
<Td color={'myGray.600'}>{formatTime2YMDHM(startTime)}</Td>
<Td color={'myGray.600'}>{formatTime2YMDHM(expiredTime)}</Td>
</Tr>
);
}
)}
<Tr key={'_id'}></Tr>
</Tbody>
</Table>
<Loading loading={isLoading} fixed={false} />
</TableContainer>
<HStack mt={4} color={'primary.700'}>
<MyIcon name={'infoRounded'} w={'1rem'} />
<Box fontSize={'mini'} fontWeight={'500'}>
{t('account_info:package_usage_rules')}
</Box>
</HStack>
</ModalBody>
</MyModal>
);
};
function StatusTag({ status }: { status: packageStatus }) {
const { t } = useTranslation();
const statusText = useMemo(() => {
return {
inactive: t('account_info:pending_usage'),
active: t('account_info:active'),
expired: t('account_info:expired')
};
}, [t]);
const styleMap = useMemo(() => {
return {
inactive: {
color: 'adora.600',
bg: 'adora.50'
},
active: {
color: 'green.600',
bg: 'green.50'
},
expired: {
color: 'myGray.700',
bg: 'myGray.100'
}
};
}, []);
return (
<Box
py={'0.25rem'}
ml={'0.375rem'}
px={'0.5rem'}
fontSize={'0.625rem'}
fontWeight={500}
borderRadius={'sm'}
bg={styleMap[status]?.bg}
color={styleMap[status]?.color}
>
{statusText[status]}
</Box>
);
}
export default StandDetailModal;

View File

@@ -0,0 +1,737 @@
import React, { useCallback, useMemo, useRef } from 'react';
import {
Box,
Flex,
Button,
useDisclosure,
useTheme,
Input,
Link,
Progress,
Grid,
BoxProps,
FlexProps
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { putUpdateMemberName } from '@/web/support/user/team/api';
import { getDocPath } from '@/web/common/system/doc';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import {
StandardSubLevelEnum,
standardSubLevelMap
} from '@fastgpt/global/support/wallet/sub/constants';
import { formatTime2YMD } from '@fastgpt/global/common/string/time';
import { getExtraPlanCardRoute } from '@/web/support/wallet/sub/constants';
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import AccountContainer from '../components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useRouter } from 'next/router';
import TeamSelector from '../components/TeamSelector';
const StandDetailModal = dynamic(() => import('./components/standardDetailModal'));
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./components/UpdateNotificationModal'));
const OpenAIAccountModal = dynamic(() => import('./components/OpenAIAccountModal'));
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const AiPointsModal = dynamic(() =>
import('@/pages/price/components/Points').then((mod) => mod.AiPointsModal)
);
const Info = () => {
const { isPc } = useSystem();
const { teamPlanStatus } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants;
const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure();
const { initUserInfo } = useUserStore();
useQuery(['init'], initUserInfo);
return (
<AccountContainer>
<Box py={[3, '28px']} px={[5, 10]} mx={'auto'}>
{isPc ? (
<Flex justifyContent={'center'} maxW={'1080px'}>
<Box flex={'0 0 330px'}>
<MyInfo onOpenContact={onOpenContact} />
<Box mt={9}>
<Other onOpenContact={onOpenContact} />
</Box>
</Box>
{!!standardPlan && (
<Box ml={'45px'} flex={'1'} maxW={'600px'}>
<PlanUsage />
</Box>
)}
</Flex>
) : (
<>
<MyInfo onOpenContact={onOpenContact} />
{standardPlan && <PlanUsage />}
<Other onOpenContact={onOpenContact} />
</>
)}
</Box>
{isOpenContact && <CommunityModal onClose={onCloseContact} />}
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account', 'account_info', 'user']))
}
};
}
export default React.memo(Info);
const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo, updateUserInfo } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { teamPlanStatus } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants;
const { isPc } = useSystem();
const { toast } = useToast();
const router = useRouter();
const {
isOpen: isOpenConversionModal,
onClose: onCloseConversionModal,
onOpen: onOpenConversionModal
} = useDisclosure();
const {
isOpen: isOpenUpdatePsw,
onClose: onCloseUpdatePsw,
onOpen: onOpenUpdatePsw
} = useDisclosure();
const {
isOpen: isOpenUpdateNotification,
onClose: onCloseUpdateNotification,
onOpen: onOpenUpdateNotification
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onclickSave = useCallback(
async (data: UserType) => {
await updateUserInfo({
avatar: data.avatar,
timezone: data.timezone,
openaiAccount: data.openaiAccount
});
reset(data);
toast({
title: t('account_info:update_success_tip'),
status: 'success'
});
},
[reset, t, toast, updateUserInfo]
);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file || !userInfo) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.userAvatar,
file,
maxW: 300,
maxH: 300
});
onclickSave({
...userInfo,
avatar: src
});
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : t('account_info:avatar_selection_exception'),
status: 'warning'
});
}
},
[onclickSave, t, toast, userInfo]
);
const labelStyles: BoxProps = {
flex: '0 0 80px',
fontSize: 'sm',
color: 'myGray.900'
};
return (
<Box>
{/* user info */}
{isPc && (
<Flex alignItems={'center'} fontSize={'md'} h={'30px'}>
<MyIcon mr={2} name={'support/user/userLight'} w={'1.25rem'} />
{t('account_info:personal_information')}
</Flex>
)}
<Box mt={[0, 6]} fontSize={'sm'}>
{isPc ? (
<Flex alignItems={'center'} cursor={'pointer'}>
<Box {...labelStyles}>{t('account_info:avatar')}:&nbsp;</Box>
<MyTooltip label={t('account_info:select_avatar')}>
<Box
w={['44px', '56px']}
h={['44px', '56px']}
borderRadius={'50%'}
border={theme.borders.base}
overflow={'hidden'}
p={'2px'}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
mb={2}
onClick={onOpenSelectFile}
>
<Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
</Box>
</MyTooltip>
</Flex>
) : (
<Flex
flexDirection={'column'}
alignItems={'center'}
cursor={'pointer'}
onClick={onOpenSelectFile}
>
<MyTooltip label={t('account_info:choose_avatar')}>
<Box
w={['44px', '54px']}
h={['44px', '54px']}
borderRadius={'50%'}
border={theme.borders.base}
overflow={'hidden'}
p={'2px'}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
mb={2}
>
<Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
</Box>
</MyTooltip>
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
<MyIcon mr={1} name={'edit'} w={'14px'} />
{t('account_info:change')}
</Flex>
</Flex>
)}
{feConfigs?.isPlus && (
<Flex mt={[0, 4]} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:member_name')}:&nbsp;</Box>
<Input
flex={'1 0 0'}
defaultValue={userInfo?.team?.memberName || 'Member'}
title={t('account_info:click_modify_nickname')}
borderColor={'transparent'}
transform={'translateX(-11px)'}
maxLength={20}
onBlur={(e) => {
const val = e.target.value;
if (val === userInfo?.team?.memberName) return;
try {
putUpdateMemberName(val);
} catch (error) {}
}}
/>
</Flex>
)}
<Flex alignItems={'center'} mt={6}>
<Box {...labelStyles}>{t('account_info:user_account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box>
</Flex>
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdatePsw}>
{t('account_info:change')}
</Button>
</Flex>
)}
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:notification_receiving')}:&nbsp;</Box>
<Box
flex={1}
{...(!userInfo?.team.notificationAccount && userInfo?.permission.isOwner
? { color: 'red.600' }
: {})}
>
{userInfo?.team.notificationAccount
? userInfo?.team.notificationAccount
: userInfo?.permission.isOwner
? t('account_info:please_bind_notification_receiving_path')
: t('account_info:reminder_create_bound_notification_account')}
</Box>
{userInfo?.permission.isOwner && (
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdateNotification}>
{t('account_info:change')}
</Button>
)}
</Flex>
)}
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:user_team_team_name')}:&nbsp;</Box>
<Flex flex={'1 0 0'} w={0} align={'center'}>
<TeamSelector height={'28px'} w={'100%'} showManage />
</Flex>
</Flex>
{feConfigs?.isPlus && (userInfo?.team?.balance ?? 0) > 0 && (
<Box mt={6} whiteSpace={'nowrap'}>
<Flex alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:team_balance')}:&nbsp;</Box>
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>{' '}
{t('account_info:yuan')}
</Box>
{userInfo?.permission.hasManagePer && !!standardPlan && (
<Button variant={'primary'} size={'sm'} ml={5} onClick={onOpenConversionModal}>
{t('account_info:exchange')}
</Button>
)}
</Flex>
</Box>
)}
</Box>
{isOpenConversionModal && (
<ConversionModal onClose={onCloseConversionModal} onOpenContact={onOpenContact} />
)}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenUpdateNotification && <UpdateNotification onClose={onCloseUpdateNotification} />}
<File onSelect={onSelectFile} />
</Box>
);
};
const PlanUsage = () => {
const router = useRouter();
const { t } = useTranslation();
const { userInfo, initUserInfo, teamPlanStatus } = useUserStore();
const { subPlans } = useSystemStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const {
isOpen: isOpenStandardModal,
onClose: onCloseStandardModal,
onOpen: onOpenStandardModal
} = useDisclosure();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const planName = useMemo(() => {
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
return standardSubLevelMap[teamPlanStatus.standard.currentSubLevel].label;
}, [teamPlanStatus?.standard?.currentSubLevel]);
const standardPlan = teamPlanStatus?.standard;
const isFreeTeam = useMemo(() => {
if (!teamPlanStatus || !teamPlanStatus?.standardConstants) return false;
const hasExtraDatasetSize =
teamPlanStatus.datasetMaxSize > teamPlanStatus.standardConstants.maxDatasetSize;
const hasExtraPoints =
teamPlanStatus.totalPoints > teamPlanStatus.standardConstants.totalPoints;
if (
teamPlanStatus?.standard?.currentSubLevel === StandardSubLevelEnum.free &&
!hasExtraDatasetSize &&
!hasExtraPoints
) {
return true;
}
return false;
}, [teamPlanStatus]);
useQuery(['init'], initUserInfo, {
onSuccess(res) {
reset(res);
}
});
const datasetUsageMap = useMemo(() => {
if (!teamPlanStatus) {
return {
colorScheme: 'green',
value: 0,
maxSize: t('account_info:unlimited'),
usedSize: 0
};
}
const rate = teamPlanStatus.usedDatasetSize / teamPlanStatus.datasetMaxSize;
const colorScheme = (() => {
if (rate < 0.5) return 'green';
if (rate < 0.8) return 'yellow';
return 'red';
})();
return {
colorScheme,
value: rate * 100,
maxSize: teamPlanStatus.datasetMaxSize || t('account_info:unlimited'),
usedSize: teamPlanStatus.usedDatasetSize
};
}, [teamPlanStatus, t]);
const aiPointsUsageMap = useMemo(() => {
if (!teamPlanStatus) {
return {
colorScheme: 'green',
value: 0,
maxSize: t('account_info:unlimited'),
usedSize: 0
};
}
const rate = teamPlanStatus.usedPoints / teamPlanStatus.totalPoints;
const colorScheme = (() => {
if (rate < 0.5) return 'green';
if (rate < 0.8) return 'yellow';
return 'red';
})();
return {
colorScheme,
value: rate * 100,
max: teamPlanStatus.totalPoints ? teamPlanStatus.totalPoints : t('account_info:unlimited'),
used: teamPlanStatus.usedPoints ? Math.round(teamPlanStatus.usedPoints) : 0
};
}, [teamPlanStatus, t]);
return standardPlan ? (
<Box mt={[6, 0]}>
<Flex fontSize={['md', 'lg']} h={'30px'}>
<Flex alignItems={'center'}>
<MyIcon mr={2} name={'support/account/plans'} w={'20px'} />
{t('account_info:package_and_usage')}
</Flex>
<Button ml={4} size={'sm'} onClick={onOpenAiPointsModal}>
{t('account_info:billing_standard')}
</Button>
<Button ml={4} variant={'whitePrimary'} size={'sm'} onClick={onOpenStandardModal}>
{t('account_info:package_details')}
</Button>
</Flex>
<Box
mt={[3, 6]}
bg={'white'}
borderWidth={'1px'}
borderColor={'borderColor.low'}
borderRadius={'md'}
>
<Flex px={[5, 7]} pt={[3, 6]}>
<Box flex={'1 0 0'}>
<Box color={'myGray.600'} fontSize="sm">
{t('account_info:current_package')}
</Box>
<Box fontWeight={'bold'} fontSize="lg">
{t(planName as any)}
</Box>
</Box>
<Button
onClick={() => {
router.push(
subPlans?.planDescriptionUrl ? getDocPath(subPlans.planDescriptionUrl) : '/price'
);
}}
w={'8rem'}
size="sm"
>
{t('account_info:upgrade_package')}
</Button>
</Flex>
<Box px={[5, 7]} pb={[3, 6]}>
{isFreeTeam && (
<Box mt="2" color={'#485264'} fontSize="sm">
{t('account_info:account_knowledge_base_cleanup_warning')}
</Box>
)}
{standardPlan.currentSubLevel !== StandardSubLevelEnum.free && (
<Flex mt="2" color={'#485264'} fontSize="xs">
<Box>{t('account_info:package_expiry_time')}:</Box>
<Box ml={2}>{formatTime2YMD(standardPlan?.expiredTime)}</Box>
</Flex>
)}
</Box>
<Box py={3} borderTopWidth={'1px'} borderTopColor={'borderColor.base'}>
<Box py={[0, 3]} px={[5, 7]} overflow={'auto'}>
<StandardPlanContentList
level={standardPlan?.currentSubLevel}
mode={standardPlan.currentMode}
/>
</Box>
</Box>
</Box>
<Box
mt={6}
bg={'white'}
borderWidth={'1px'}
borderColor={'borderColor.low'}
borderRadius={'md'}
px={[5, 10]}
pt={4}
pb={[4, 7]}
>
<Flex>
<Flex flex={'1 0 0'} alignItems={'flex-end'}>
<Box fontSize={'md'} fontWeight={'bold'} color={'myGray.900'}>
{t('account_info:resource_usage')}
</Box>
<Box ml={1} display={['none', 'block']} fontSize={'xs'} color={'myGray.500'}>
{t('account_info:standard_package_and_extra_resource_package')}
</Box>
</Flex>
<Link
href={getWebReqUrl(getExtraPlanCardRoute())}
transform={'translateX(15px)'}
display={'flex'}
alignItems={'center'}
color={'primary.600'}
cursor={'pointer'}
fontSize={'sm'}
>
{t('account_info:purchase_extra_package')}
<MyIcon ml={1} name={'common/rightArrowLight'} w={'12px'} />
</Link>
</Flex>
<Box width={'100%'} mt={5} fontSize={'sm'}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'} color={'myGray.900'}>
{t('account_info:knowledge_base_capacity')}
</Box>
<Box color={'myGray.600'} ml={2}>
{datasetUsageMap.usedSize}/{datasetUsageMap.maxSize}
</Box>
</Flex>
</Flex>
<Box mt={3}>
<Progress
size={'sm'}
value={datasetUsageMap.value}
colorScheme={datasetUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.low'}
/>
</Box>
</Box>
<Box mt="9" width={'100%'} fontSize={'sm'}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'} color={'myGray.900'}>
{t('account_info:ai_points_usage')}
</Box>
<QuestionTip ml={1} label={t('account_info:ai_points_usage_tip')}></QuestionTip>
<Box color={'myGray.600'} ml={2}>
{aiPointsUsageMap.used}/{aiPointsUsageMap.max}
</Box>
</Flex>
</Flex>
<Box mt={3}>
<Progress
size={'sm'}
value={aiPointsUsageMap.value}
colorScheme={aiPointsUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.low'}
/>
</Box>
</Box>
</Box>
{isOpenStandardModal && <StandDetailModal onClose={onCloseStandardModal} />}
{isOpenAiPointsModal && <AiPointsModal onClose={onCloseAiPointsModal} />}
</Box>
) : null;
};
const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { isPc } = useSystem();
const { userInfo, updateUserInfo } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { isOpen: isOpenLaf, onClose: onCloseLaf, onOpen: onOpenLaf } = useDisclosure();
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = useDisclosure();
const onclickSave = useCallback(
async (data: UserType) => {
await updateUserInfo({
avatar: data.avatar,
timezone: data.timezone,
openaiAccount: data.openaiAccount
});
reset(data);
toast({
title: t('account_info:update_success_tip'),
status: 'success'
});
},
[reset, t, toast, updateUserInfo]
);
const buttonStyles = useRef<FlexProps>({
bg: 'white',
py: 3,
px: 6,
border: theme.borders.sm,
borderWidth: '1.5px',
borderRadius: 'md',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none',
fontSize: 'sm'
});
return (
<Box>
<Grid gridGap={4} mt={3}>
{feConfigs?.docUrl && (
<Link
bg={'white'}
href={getDocPath('/docs/intro')}
target="_blank"
display={'flex'}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
userSelect={'none'}
textDecoration={'none !important'}
fontSize={'sm'}
>
<MyIcon name={'common/courseLight'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{t('account_info:help_document')}
</Box>
</Link>
)}
{!isPc &&
feConfigs?.navbarItems
?.filter((item) => item.isActive)
.map((item) => (
<Flex
key={item.id}
{...buttonStyles.current}
onClick={() => window.open(item.url, '_blank')}
>
<Avatar src={item.avatar} w={'18px'} />
<Box ml={2} flex={1}>
{item.name}
</Box>
</Flex>
))}
{feConfigs?.lafEnv && userInfo?.team.role === TeamMemberRoleEnum.owner && (
<Flex {...buttonStyles.current} onClick={onOpenLaf}>
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<Box ml={2} flex={1}>
{'laf' + t('account_info:account_duplicate')}
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.team.lafAccount?.token ? '#67c13b' : 'myGray.500'}
/>
</Flex>
)}
{feConfigs?.show_openai_account && (
<Flex {...buttonStyles.current} onClick={onOpenOpenai}>
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{'OpenAI / OneAPI' + t('account_info:account_duplicate')}
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.openaiAccount?.key ? '#67c13b' : 'myGray.500'}
/>
</Flex>
)}
{feConfigs?.concatMd && (
<Button
variant={'whiteBase'}
justifyContent={'flex-start'}
leftIcon={<MyIcon name={'modal/concat'} w={'18px'} color={'myGray.600'} />}
onClick={onOpenContact}
h={'48px'}
fontSize={'sm'}
>
{t('account_info:contact_us')}
</Button>
)}
</Grid>
{isOpenLaf && userInfo && (
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLaf} />
)}
{isOpenOpenai && userInfo && (
<OpenAIAccountModal
defaultData={userInfo?.openaiAccount}
onSuccess={(data) =>
onclickSave({
...userInfo,
openaiAccount: data
})
}
onClose={onCloseOpenai}
/>
)}
</Box>
);
};