Revert "sub plan page (#885)" (#886)

This reverts commit 443ad37b6a.
This commit is contained in:
Archer
2024-02-23 17:48:15 +08:00
committed by GitHub
parent 443ad37b6a
commit fd9b6291af
246 changed files with 4281 additions and 6286 deletions

View File

@@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box } from '@chakra-ui/react';
function Error() {
const router = useRouter();
@@ -25,12 +24,11 @@ function Error() {
}, []);
return (
<Box whiteSpace={'pre-wrap'}>
{`出现未捕获的异常。
1. 私有部署用户90%由于配置文件不正确导致。
2. 部分系统不兼容相关API。大部分是苹果的safari 浏览器导致,可以尝试更换 chrome。
3. 请关闭浏览器翻译功能,部分翻译导致页面崩溃。`}
</Box>
<p>
safari chrome
</p>
);
}

View File

@@ -0,0 +1,160 @@
import React, { useMemo } from 'react';
import {
ModalBody,
Flex,
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { BillItemType } from '@fastgpt/global/support/wallet/bill/type.d';
import dayjs from 'dayjs';
import { BillSourceMap } from '@fastgpt/global/support/wallet/bill/constants';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
const BillDetail = ({ bill, onClose }: { bill: BillItemType; onClose: () => void }) => {
const { t } = useTranslation();
const filterBillList = useMemo(
() => bill.list.filter((item) => item && item.moduleName),
[bill.list]
);
const {
hasModel,
hasTokens,
hasInputTokens,
hasOutputTokens,
hasCharsLen,
hasDuration,
hasDataLen,
hasDatasetSize
} = useMemo(() => {
let hasModel = false;
let hasTokens = false;
let hasInputTokens = false;
let hasOutputTokens = false;
let hasCharsLen = false;
let hasDuration = false;
let hasDataLen = false;
let hasDatasetSize = false;
bill.list.forEach((item) => {
if (item.model !== undefined) {
hasModel = true;
}
if (typeof item.tokenLen === 'number') {
hasTokens = true;
}
if (typeof item.inputTokens === 'number') {
hasInputTokens = true;
}
if (typeof item.outputTokens === 'number') {
hasOutputTokens = true;
}
if (typeof item.charsLength === 'number') {
hasCharsLen = true;
}
if (typeof item.duration === 'number') {
hasDuration = true;
}
if (typeof item.datasetSize === 'number') {
hasDatasetSize = true;
}
});
return {
hasModel,
hasTokens,
hasInputTokens,
hasOutputTokens,
hasCharsLen,
hasDuration,
hasDataLen,
hasDatasetSize
};
}, [bill.list]);
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('user.Bill Detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
{/* <Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('wallet.bill.bill username')}:</Box>
<Box>{t(bill.memberName)}</Box>
</Flex> */}
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('wallet.bill.Number')}:</Box>
<Box>{bill.id}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('wallet.bill.Time')}:</Box>
<Box>{dayjs(bill.time).format('YYYY/MM/DD HH:mm:ss')}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('wallet.bill.App name')}:</Box>
<Box>{t(bill.appName) || '-'}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('wallet.bill.Source')}:</Box>
<Box>{t(BillSourceMap[bill.source]?.label)}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('wallet.bill.Total')}:</Box>
<Box fontWeight={'bold'}>{bill.total}</Box>
</Flex>
<Box pb={4}>
<Box flex={'0 0 80px'} mb={1}>
{t('wallet.bill.Bill Module')}
</Box>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>{t('wallet.bill.Module name')}</Th>
{hasModel && <Th>{t('wallet.bill.Ai model')}</Th>}
{hasTokens && <Th>{t('wallet.bill.Token Length')}</Th>}
{hasInputTokens && <Th>{t('wallet.bill.Input Token Length')}</Th>}
{hasOutputTokens && <Th>{t('wallet.bill.Output Token Length')}</Th>}
{hasCharsLen && <Th>{t('wallet.bill.Text Length')}</Th>}
{hasDuration && <Th>{t('wallet.bill.Duration')}</Th>}
{hasDatasetSize && (
<Th>{t('support.wallet.subscription.type.extraDatasetSize')}</Th>
)}
<Th>()</Th>
</Tr>
</Thead>
<Tbody>
{filterBillList.map((item, i) => (
<Tr key={i}>
<Td>{t(item.moduleName)}</Td>
{hasModel && <Td>{item.model ?? '-'}</Td>}
{hasTokens && <Td>{item.tokenLen ?? '-'}</Td>}
{hasInputTokens && <Td>{item.inputTokens ?? '-'}</Td>}
{hasOutputTokens && <Td>{item.outputTokens ?? '-'}</Td>}
{hasCharsLen && <Td>{item.charsLength ?? '-'}</Td>}
{hasDuration && <Td>{item.duration ?? '-'}</Td>}
{hasDatasetSize && <Td>{item.datasetSize ?? '-'}</Td>}
<Td>{formatStorePrice2Read(item.amount)}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</ModalBody>
</MyModal>
);
};
export default BillDetail;

View File

@@ -1,6 +1,5 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Button,
Table,
Thead,
Tbody,
@@ -10,38 +9,43 @@ import {
TableContainer,
Flex,
Box,
ModalBody
Button
} from '@chakra-ui/react';
import { getBills, checkBalancePayResult } from '@/web/support/wallet/bill/api';
import type { BillSchemaType } from '@fastgpt/global/support/wallet/bill/type.d';
import { BillSourceEnum, BillSourceMap } from '@fastgpt/global/support/wallet/bill/constants';
import { getUserBills } from '@/web/support/wallet/bill/api';
import type { BillItemType } from '@fastgpt/global/support/wallet/bill/type';
import { usePagination } from '@/web/common/hooks/usePagination';
import { useLoading } from '@/web/common/hooks/useLoading';
import dayjs from 'dayjs';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
import { addDays } from 'date-fns';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import MySelect from '@/components/Select';
import {
BillTypeEnum,
billPayWayMap,
billStatusMap,
billTypeMap
} from '@fastgpt/global/support/wallet/bill/constants';
import { usePagination } from '@/web/common/hooks/usePagination';
import MyBox from '@/components/common/MyBox';
import { useRequest } from '@/web/common/hooks/useRequest';
import MyModal from '@/components/MyModal';
import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamMembers } from '@/web/support/user/team/api';
import Avatar from '@/components/Avatar';
const BillDetail = dynamic(() => import('./BillDetail'));
const BillTable = () => {
const { t } = useTranslation();
const { toast } = useToast();
const [billType, setBillType] = useState<`${BillTypeEnum}` | ''>('');
const [billDetail, setBillDetail] = useState<BillSchemaType>();
const { Loading } = useLoading();
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const [billSource, setBillSource] = useState<`${BillSourceEnum}` | ''>('');
const { isPc } = useSystemStore();
const { userInfo } = useUserStore();
const [billDetail, setBillDetail] = useState<BillItemType>();
const billTypeList = useMemo(
const sourceList = useMemo(
() => [
{ label: t('common.All'), value: '' },
...Object.entries(billTypeMap).map(([key, value]) => ({
...Object.entries(BillSourceMap).map(([key, value]) => ({
label: t(value.label),
value: key
}))
@@ -49,193 +53,134 @@ const BillTable = () => {
[t]
);
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
if (!userInfo?.team?.teamId) return [];
return getTeamMembers(userInfo.team.teamId);
});
const tmbList = useMemo(
() =>
members.map((item) => ({
label: (
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'16px'} mr={1} />
{item.memberName}
</Flex>
),
value: item.tmbId
})),
[members]
);
const {
data: bills,
isLoading,
Pagination,
getData,
total
} = usePagination<BillSchemaType>({
api: getBills,
pageSize: 20,
getData
} = usePagination<BillItemType>({
api: getUserBills,
pageSize: isPc ? 20 : 10,
params: {
type: billType
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
source: billSource,
teamMemberId: selectTmbId
},
defaultRequest: false
});
const { mutate: handleRefreshPayOrder, isLoading: isRefreshing } = useRequest({
mutationFn: async (payId: string) => {
try {
const data = await checkBalancePayResult(payId);
toast({
title: data,
status: 'success'
});
} catch (error: any) {
toast({
title: error?.message,
status: 'warning'
});
console.log(error);
}
try {
getData(1);
} catch (error) {}
}
});
useEffect(() => {
getData(1);
}, [billType]);
}, [billSource, selectTmbId]);
return (
<MyBox
isLoading={isLoading || isRefreshing}
position={'relative'}
h={'100%'}
overflow={'overlay'}
py={[0, 5]}
px={[3, 8]}
>
<TableContainer>
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Flex
flexDir={['column', 'row']}
gap={2}
w={'100%'}
px={[3, 8]}
alignItems={['flex-end', 'center']}
>
{tmbList.length > 1 && userInfo?.team?.canWrite && (
<Flex alignItems={'center'}>
<Box mr={2} flexShrink={0}>
{t('support.user.team.member')}
</Box>
<MySelect
size={'sm'}
minW={'100px'}
list={tmbList}
value={selectTmbId}
onchange={setSelectTmbId}
/>
</Flex>
)}
<Box flex={'1'} />
<Flex alignItems={'center'} gap={3}>
<DateRangePicker
defaultDate={dateRange}
position="bottom"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Flex>
</Flex>
<TableContainer px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
<Table>
<Thead>
<Tr>
<Th>#</Th>
{/* <Th>{t('user.team.Member Name')}</Th> */}
<Th>{t('user.Time')}</Th>
<Th>
<MySelect
list={billTypeList}
value={billType}
list={sourceList}
value={billSource}
size={'sm'}
onchange={(e) => {
setBillType(e);
setBillSource(e);
}}
w={'130px'}
></MySelect>
</Th>
<Th>{t('user.Time')}</Th>
<Th>{t('support.wallet.Amount')}</Th>
<Th>{t('support.wallet.bill.Status')}</Th>
<Th>{t('user.Application Name')}</Th>
<Th>{t('user.Total Amount')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{bills.map((item, i) => (
<Tr key={item._id}>
<Td>{i + 1}</Td>
<Td>{t(billTypeMap[item.type]?.label)}</Td>
{bills.map((item) => (
<Tr key={item.id}>
{/* <Td>{item.memberName}</Td> */}
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>{t(BillSourceMap[item.source]?.label)}</Td>
<Td>{t(item.appName) || '-'}</Td>
<Td>{item.total}</Td>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{formatStorePrice2Read(item.price)}</Td>
<Td>{t(billStatusMap[item.status]?.label)}</Td>
<Td>
{item.status === 'NOTPAY' && (
<Button mr={4} onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
{t('common.Update')}
</Button>
)}
<Button variant={'whiteBase'} size={'sm'} onClick={() => setBillDetail(item)}>
{t('common.Detail')}
<Button size={'sm'} variant={'whitePrimary'} onClick={() => setBillDetail(item)}>
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{total >= 20 && (
<Flex mt={3} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
{!isLoading && bills.length === 0 && (
<Flex
mt={'20vh'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('support.wallet.noBill')}
</Box>
</Flex>
)}
</TableContainer>
{!!billDetail && (
<BillDetailModal bill={billDetail} onClose={() => setBillDetail(undefined)} />
{!isLoading && bills.length === 0 && (
<Flex flex={'1 0 0'} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
使~
</Box>
</Flex>
)}
</MyBox>
<Loading loading={isLoading} fixed={false} />
{!!billDetail && <BillDetail bill={billDetail} onClose={() => setBillDetail(undefined)} />}
</Flex>
);
};
export default BillTable;
function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) {
const { t } = useTranslation();
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('support.wallet.usage.Usage Detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.bill.Number')}:</Box>
<Box>{bill.orderId}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.usage.Time')}:</Box>
<Box>{dayjs(bill.createTime).format('YYYY/MM/DD HH:mm:ss')}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.bill.Status')}:</Box>
<Box>{t(billStatusMap[bill.status]?.label)}</Box>
</Flex>
{!!bill.metadata?.payWay && (
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.bill.payWay.Way')}:</Box>
<Box>{t(billPayWayMap[bill.metadata.payWay]?.label)}</Box>
</Flex>
)}
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.Amount')}:</Box>
<Box>{formatStorePrice2Read(bill.price)}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.bill.Type')}:</Box>
<Box>{t(billTypeMap[bill.type]?.label)}</Box>
</Flex>
{!!bill.metadata?.subMode && (
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.mode.Period')}:</Box>
<Box>{t(subModeMap[bill.metadata.subMode]?.label)}</Box>
</Flex>
)}
{!!bill.metadata?.standSubLevel && (
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Stand plan level')}:</Box>
<Box>{t(standardSubLevelMap[bill.metadata.standSubLevel]?.label)}</Box>
</Flex>
)}
{bill.metadata?.datasetSize !== undefined && (
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Extra dataset size')}:</Box>
<Box>{bill.metadata?.datasetSize}</Box>
</Flex>
)}
{bill.metadata?.extraPoints !== undefined && (
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Extra ai points')}:</Box>
<Box>{bill.metadata.extraPoints}</Box>
</Flex>
)}
</ModalBody>
</MyModal>
);
}
export default React.memo(BillTable);

View File

@@ -1,14 +1,15 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import {
Box,
Flex,
Button,
useDisclosure,
useTheme,
Divider,
Select,
Input,
Link,
Progress,
Grid
Progress
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
@@ -21,68 +22,35 @@ 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 { timezoneList } from '@fastgpt/global/common/time/timezone';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import { langMap, setLngStore } from '@/web/common/utils/i18n';
import { useRouter } from 'next/router';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import MySelect from '@/components/Select';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { putUpdateMemberName } from '@/web/support/user/team/api';
import { getDocPath } from '@/web/common/system/doc';
import { getTeamDatasetValidSub } from '@/web/support/wallet/sub/api';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import { AI_POINT_USAGE_CARD_ROUTE } from '@/web/support/wallet/sub/constants';
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
const PayModal = dynamic(() => import('./PayModal'));
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'));
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'));
const SubDatasetModal = dynamic(() => import('@/components/support/wallet/SubDatasetModal'));
const Account = () => {
const { isPc } = useSystemStore();
const { initUserInfo } = useUserStore();
useQuery(['init'], initUserInfo);
return (
<Box py={[3, '28px']} px={['5vw', '64px']}>
{isPc ? (
<Flex justifyContent={'center'}>
<Box flex={'0 0 330px'}>
<MyInfo />
<Box mt={9}>
<Other />
</Box>
</Box>
<Box ml={'45px'} flex={'1 0 0'} maxW={'600px'}>
<PlanUsage />
</Box>
</Flex>
) : (
<>
<MyInfo />
<PlanUsage />
<Other />
</>
)}
</Box>
);
};
export default React.memo(Account);
const MyInfo = () => {
const UserInfo = () => {
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo, updateUserInfo } = useUserStore();
const router = useRouter();
const { feConfigs, systemVersion } = useSystemStore();
const { t, i18n } = useTranslation();
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
const timezones = useRef(timezoneList());
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { isPc } = useSystemStore();
const { toast } = useToast();
const {
@@ -95,11 +63,13 @@ const MyInfo = () => {
onClose: onCloseUpdatePsw,
onOpen: onOpenUpdatePsw
} = useDisclosure();
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = useDisclosure();
const {
isOpen: isOpenStandardModal,
onClose: onCloseStandardModal,
onOpen: onOpenStandardModal
isOpen: isOpenSubDatasetModal,
onClose: onCloseSubDatasetModal,
onOpen: onOpenSubDatasetModal
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
@@ -147,73 +117,81 @@ const MyInfo = () => {
[onclickSave, t, toast, userInfo]
);
useQuery(['init'], initUserInfo, {
onSuccess(res) {
reset(res);
}
});
const {
data: teamSubPlan = { totalPoints: 0, usedPoints: 0, datasetMaxSize: 800, usedDatasetSize: 0 }
} = useQuery(['getTeamDatasetValidSub'], getTeamDatasetValidSub);
const datasetUsageMap = useMemo(() => {
const rate = teamSubPlan.usedDatasetSize / teamSubPlan.datasetMaxSize;
const colorScheme = (() => {
if (rate < 0.5) return 'green';
if (rate < 0.8) return 'yellow';
return 'red';
})();
return {
colorScheme,
value: rate * 100,
maxSize: teamSubPlan.datasetMaxSize || t('common.Unlimited'),
usedSize: teamSubPlan.usedDatasetSize
};
}, [teamSubPlan.usedDatasetSize, teamSubPlan.datasetMaxSize, t]);
return (
<Box>
{/* user info */}
{isPc && (
<Flex alignItems={'center'} fontSize={'xl'} h={'30px'}>
<MyIcon mr={2} name={'acount/user'} w={'20px'} />
{t('support.user.User self info')}
</Flex>
)}
<Box mt={[0, 6]}>
{isPc ? (
<Flex alignItems={'center'} cursor={'pointer'}>
<Box flex={'0 0 80px'}>{t('support.user.Avatar')}:&nbsp;</Box>
<MyTooltip label={t('common.avatar.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}
<Box
display={['block', 'flex']}
py={[2, 10]}
justifyContent={'center'}
alignItems={'flex-start'}
>
<Flex
flexDirection={'column'}
alignItems={'center'}
cursor={'pointer'}
onClick={onOpenSelectFile}
>
<MyTooltip label={'更换头像'}>
<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}
>
<MyTooltip label={'更换头像'}>
<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>
<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('user.Replace')}
</Flex>
</Flex>
)}
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
<MyIcon mr={1} name={'edit'} w={'14px'} />
{t('user.Replace')}
</Flex>
</Flex>
<Box
display={['flex', 'block']}
flexDirection={'column'}
alignItems={'center'}
ml={[0, 10]}
mt={[6, 0]}
>
{feConfigs.isPlus && (
<Flex mt={[0, 4]} alignItems={'center'}>
<Flex mb={4} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Member Name')}:&nbsp;</Box>
<Input
flex={'1 0 0'}
flex={1}
defaultValue={userInfo?.team?.memberName || 'Member'}
title={t('user.Edit name')}
borderColor={'transparent'}
pl={'10px'}
transform={'translateX(-11px)'}
maxLength={20}
onBlur={(e) => {
@@ -226,265 +204,109 @@ const MyInfo = () => {
/>
</Flex>
)}
<Flex alignItems={'center'} mt={6}>
<Flex alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box>
</Flex>
{feConfigs.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box flex={'0 0 80px'}>{t('user.Password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdatePsw}>
{t('user.Change')}
</Button>
</Flex>
)}
<Flex mt={6} alignItems={'center'}>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Team')}:&nbsp;</Box>
<Box flex={1}>
<TeamMenu />
</Box>
</Flex>
{feConfigs.isPlus && (
<Box mt={6} whiteSpace={'nowrap'}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} fontSize={'md'}>
{t('user.team.Balance')}:&nbsp;
</Box>
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>
</Box>
{feConfigs?.show_pay && userInfo?.team?.canWrite && (
<Button size={'sm'} ml={5} onClick={onOpenPayModal}>
{t('user.Pay')}
</Button>
)}
</Flex>
</Box>
)}
</Box>
{isOpenPayModal && <PayModal onClose={onClosePayModal} />}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
<File onSelect={onSelectFile} />
</Box>
);
};
const PlanUsage = () => {
const { isPc } = useSystemStore();
const router = useRouter();
const { t } = useTranslation();
const { userInfo, initUserInfo, teamPlanStatus } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const planName = useMemo(() => {
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
return standardSubLevelMap[teamPlanStatus.standard.currentSubLevel].label;
}, [teamPlanStatus?.standard?.currentSubLevel]);
const standardPlan = teamPlanStatus?.standard;
useQuery(['init'], initUserInfo, {
onSuccess(res) {
reset(res);
}
});
const datasetUsageMap = useMemo(() => {
if (!teamPlanStatus) {
return {
colorScheme: 'green',
value: 0,
maxSize: t('common.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('common.Unlimited'),
usedSize: teamPlanStatus.usedDatasetSize
};
}, [teamPlanStatus, t]);
const aiPointsUsageMap = useMemo(() => {
if (!teamPlanStatus) {
return {
colorScheme: 'green',
value: 0,
maxSize: t('common.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,
maxSize: teamPlanStatus.totalPoints || t('common.Unlimited'),
usedSize: teamPlanStatus.usedPoints
};
}, [teamPlanStatus, t]);
return standardPlan ? (
<Box mt={[6, 0]}>
<Flex fontSize={'xl'} h={'30px'}>
<Flex alignItems={'center'}>
<MyIcon mr={2} name={'acount/plans'} w={'20px'} />
{t('support.wallet.subscription.Team plan and usage')}
</Flex>
<Button
ml={4}
variant={'whitePrimary'}
size={'sm'}
onClick={() => router.push(AI_POINT_USAGE_CARD_ROUTE)}
>
{t('support.user.Price')}
</Button>
{/* <Button ml={4} variant={'whitePrimary'} size={'sm'}>
套餐详情
</Button> */}
</Flex>
<Box
mt={[3, 6]}
bg={'white'}
borderWidth={'1px'}
borderColor={'borderColor.low'}
borderRadius={'md'}
>
<Flex px={[5, 10]} py={[3, 6]}>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Language')}:&nbsp;</Box>
<Box flex={'1 0 0'}>
<Box color={'myGray.600'} fontSize="sm">
{t('support.wallet.subscription.Current plan')}
</Box>
<Box fontWeight={'bold'} fontSize="xl">
{t(planName)}
</Box>
<Flex mt="3" color={'#485264'} fontSize="sm">
<Box>{t('common.Expired Time')}:</Box>
<Box ml={2}>{formatTime2YMDHM(standardPlan?.expiredTime)}</Box>
</Flex>
<MySelect
value={i18n.language}
list={Object.entries(langMap).map(([key, lang]) => ({
label: lang.label,
value: key
}))}
onchange={(val: any) => {
const lang = val;
setLngStore(lang);
router.replace(router.basePath, router.asPath, { locale: lang });
}}
/>
</Box>
<Button onClick={() => router.push('/price')}>
{t('support.wallet.subscription.Upgrade plan')}
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Timezone')}:&nbsp;</Box>
<Select
value={userInfo?.timezone}
onChange={(e) => {
if (!userInfo) return;
onclickSave({ ...userInfo, timezone: e.target.value });
}}
>
{timezones.current.map((item) => (
<option key={item.value} value={item.value}>
{item.name}
</option>
))}
</Select>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
<Button size={['sm', 'md']} variant={'whitePrimary'} ml={5} onClick={onOpenUpdatePsw}>
{t('user.Change')}
</Button>
</Flex>
<Box py={3} borderTopWidth={'1px'} borderTopColor={'borderColor.base'}>
<Box py={[0, 3]} px={[5, 10]} 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]}
py={[4, 7]}
>
<Box width={'100%'}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{t('support.user.team.Dataset usage')}</Box>
<Box color={'myGray.600'} ml={2}>
{datasetUsageMap.usedSize}/{datasetUsageMap.maxSize}
{feConfigs.isPlus && (
<>
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} fontSize={'md'}>
{t('user.team.Balance')}:&nbsp;
</Box>
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>
</Box>
{feConfigs?.show_pay && userInfo?.team?.canWrite && (
<Button size={['sm', 'md']} ml={5} onClick={onOpenPayModal}>
{t('user.Pay')}
</Button>
)}
</Flex>
</Box>
{feConfigs?.show_pay && (
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} fontSize={'md'}>
{t('support.user.team.Dataset usage')}:&nbsp;{datasetUsageMap.usedSize}/
{datasetUsageMap.maxSize}
</Box>
{userInfo?.team?.canWrite && (
<Button size={'sm'} onClick={onOpenSubDatasetModal}>
{t('support.wallet.Buy more')}
</Button>
)}
</Flex>
<Box mt={1}>
<Progress
value={datasetUsageMap.value}
colorScheme={datasetUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.base'}
/>
</Box>
</Box>
</Flex>
</Flex>
<Box mt={3}>
<Progress
value={datasetUsageMap.value}
colorScheme={datasetUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.low'}
/>
</Box>
</Box>
<Box mt="9" width={'100%'}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{t('support.wallet.subscription.AI points')}</Box>
<Box color={'myGray.600'} ml={2}>
{Math.round(teamPlanStatus.usedPoints)}/{teamPlanStatus.totalPoints}
</Box>
</Flex>
</Flex>
<Box mt={3}>
<Progress
value={aiPointsUsageMap.value}
colorScheme={aiPointsUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.low'}
/>
</Box>
</Box>
<Flex></Flex>
</Box>
</Box>
) : null;
};
const Other = () => {
const theme = useTheme();
const { toast } = useToast();
const { feConfigs, systemVersion } = useSystemStore();
const { t } = useTranslation();
const { userInfo, updateUserInfo, initUserInfo, teamPlanStatus } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
)}
</>
)}
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: '更新数据成功',
status: 'success'
});
},
[reset, toast, updateUserInfo]
);
return (
<Box>
<Grid gridGap={4} mt={3}>
{feConfigs?.docUrl && (
<Link
bg={'white'}
href={getDocPath('/docs/intro')}
target="_blank"
display={'flex'}
mt={4}
w={['85%', '300px']}
py={3}
px={6}
border={theme.borders.sm}
@@ -504,53 +326,64 @@ const Other = () => {
</Box>
</Link>
)}
<Link
href={feConfigs.chatbotUrl}
target="_blank"
display={'flex'}
py={3}
px={6}
bg={'white'}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
userSelect={'none'}
textDecoration={'none !important'}
>
<MyIcon name={'core/app/aiLight'} w={'18px'} />
<Box ml={2} flex={1}>
{t('common.system.Help Chatbot')}
</Box>
</Link>
{feConfigs?.show_openai_account && (
<Flex
bg={'white'}
py={4}
{feConfigs?.chatbotUrl && (
<Link
href={feConfigs.chatbotUrl}
target="_blank"
display={'flex'}
mt={4}
w={['85%', '300px']}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
cursor={'pointer'}
userSelect={'none'}
onClick={onOpenOpenai}
textDecoration={'none !important'}
>
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
<MyIcon name={'core/app/aiLight'} w={'18px'} />
<Box ml={2} flex={1}>
OpenAI/OneAPI
{t('common.system.Help Chatbot')}
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.openaiAccount?.key ? '#67c13b' : 'myGray.500'}
/>
</Flex>
</Link>
)}
</Grid>
{feConfigs?.show_openai_account && (
<>
<Divider my={3} />
<MyTooltip label={'点击配置账号'}>
<Flex
w={['85%', '300px']}
py={4}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
bg={'myWhite.300'}
alignItems={'center'}
cursor={'pointer'}
userSelect={'none'}
onClick={onOpenOpenai}
>
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
OpenAI/OneAPI
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.openaiAccount?.key ? '#67c13b' : 'myGray.500'}
/>
</Flex>
</MyTooltip>
</>
)}
</Box>
{isOpenPayModal && <PayModal onClose={onClosePayModal} />}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenOpenai && userInfo && (
<OpenAIAccountModal
defaultData={userInfo?.openaiAccount}
@@ -563,6 +396,10 @@ const Other = () => {
onClose={onCloseOpenai}
/>
)}
{isOpenSubDatasetModal && <SubDatasetModal onClose={onCloseSubDatasetModal} />}
<File onSelect={onSelectFile} />
</Box>
);
};
export default React.memo(UserInfo);

View File

@@ -1,439 +0,0 @@
import React, { useCallback, useMemo, useRef } from 'react';
import {
Box,
Flex,
Button,
useDisclosure,
useTheme,
Divider,
Select,
Input,
Link,
Progress
} 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 { timezoneList } from '@fastgpt/global/common/time/timezone';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@/components/MyTooltip';
import { langMap, setLngStore } from '@/web/common/utils/i18n';
import { useRouter } from 'next/router';
import MySelect from '@/components/Select';
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 { getTeamPlanStatus } from '@/web/support/wallet/sub/api';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
const PayModal = dynamic(() => import('./PayModal'));
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'));
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'));
const UserInfo = () => {
const theme = useTheme();
const router = useRouter();
const { feConfigs, systemVersion } = useSystemStore();
const { t, i18n } = useTranslation();
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
const timezones = useRef(timezoneList());
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { toast } = useToast();
const {
isOpen: isOpenPayModal,
onClose: onClosePayModal,
onOpen: onOpenPayModal
} = useDisclosure();
const {
isOpen: isOpenUpdatePsw,
onClose: onCloseUpdatePsw,
onOpen: onOpenUpdatePsw
} = useDisclosure();
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = 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: '更新数据成功',
status: 'success'
});
},
[reset, 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('common.error.Select avatar failed'),
status: 'warning'
});
}
},
[onclickSave, t, toast, userInfo]
);
useQuery(['init'], initUserInfo, {
onSuccess(res) {
reset(res);
}
});
const {
data: teamSubPlan = {
totalPoints: 0,
usedPoints: 0,
datasetMaxSize: 800,
usedDatasetSize: 0
}
} = useQuery(['getTeamPlanStatus'], getTeamPlanStatus);
const datasetUsageMap = useMemo(() => {
const rate = teamSubPlan.usedDatasetSize / teamSubPlan.datasetMaxSize;
const colorScheme = (() => {
if (rate < 0.5) return 'green';
if (rate < 0.8) return 'yellow';
return 'red';
})();
return {
colorScheme,
value: rate * 100,
maxSize: teamSubPlan.datasetMaxSize || t('common.Unlimited'),
usedSize: teamSubPlan.usedDatasetSize
};
}, [teamSubPlan.usedDatasetSize, teamSubPlan.datasetMaxSize, t]);
const aiPointsUsageMap = useMemo(() => {
const rate = teamSubPlan.usedPoints / teamSubPlan.totalPoints;
const colorScheme = (() => {
if (rate < 0.5) return 'green';
if (rate < 0.8) return 'yellow';
return 'red';
})();
return {
colorScheme,
value: rate * 100,
maxSize: teamSubPlan.totalPoints || t('common.Unlimited'),
usedSize: teamSubPlan.usedPoints
};
}, [teamSubPlan.usedPoints, teamSubPlan.totalPoints, t]);
return (
<Box
display={['block', 'flex']}
py={[2, 10]}
justifyContent={'center'}
alignItems={'flex-start'}
>
<Flex
flexDirection={'column'}
alignItems={'center'}
cursor={'pointer'}
onClick={onOpenSelectFile}
>
<MyTooltip label={'更换头像'}>
<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('user.Replace')}
</Flex>
</Flex>
<Box
display={['flex', 'block']}
flexDirection={'column'}
alignItems={'center'}
ml={[0, 10]}
mt={[6, 0]}
>
{feConfigs.isPlus && (
<Flex mb={4} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Member Name')}:&nbsp;</Box>
<Input
flex={1}
defaultValue={userInfo?.team?.memberName || 'Member'}
title={t('user.Edit name')}
borderColor={'transparent'}
pl={'10px'}
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'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Team')}:&nbsp;</Box>
<Box flex={1}>
<TeamMenu />
</Box>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Language')}:&nbsp;</Box>
<Box flex={'1 0 0'}>
<MySelect
value={i18n.language}
list={Object.entries(langMap).map(([key, lang]) => ({
label: lang.label,
value: key
}))}
onchange={(val: any) => {
const lang = val;
setLngStore(lang);
router.replace(router.basePath, router.asPath, { locale: lang });
}}
/>
</Box>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Timezone')}:&nbsp;</Box>
<Select
value={userInfo?.timezone}
onChange={(e) => {
if (!userInfo) return;
onclickSave({ ...userInfo, timezone: e.target.value });
}}
>
{timezones.current.map((item) => (
<option key={item.value} value={item.value}>
{item.name}
</option>
))}
</Select>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
<Box flex={'0 0 80px'}>{t('user.Password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
<Button size={['sm', 'md']} variant={'whitePrimary'} ml={5} onClick={onOpenUpdatePsw}>
{t('user.Change')}
</Button>
</Flex>
{feConfigs.isPlus && (
<>
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'} fontSize={'md'}>
{t('user.team.Balance')}:&nbsp;
</Box>
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>
</Box>
{feConfigs?.show_pay && userInfo?.team?.canWrite && (
<Button size={['sm', 'md']} ml={5} onClick={onOpenPayModal}>
{t('user.Pay')}
</Button>
)}
</Flex>
</Box>
{feConfigs?.show_pay && (
<>
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} fontSize={'md'}>
{t('support.user.team.Dataset usage')}:&nbsp;{datasetUsageMap.usedSize}/
{datasetUsageMap.maxSize}
</Box>
{userInfo?.team?.canWrite && (
<Button size={'sm'} onClick={() => router.push('/price')}>
{t('support.wallet.Buy more')}
</Button>
)}
</Flex>
<Box mt={1}>
<Progress
value={datasetUsageMap.value}
colorScheme={datasetUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.base'}
/>
</Box>
</Box>
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} fontSize={'md'}>
AI积分: {Math.round(teamSubPlan.usedPoints)}/{teamSubPlan.totalPoints}
</Box>
</Flex>
<Box mt={1}>
<Progress
value={aiPointsUsageMap.value}
colorScheme={aiPointsUsageMap.colorScheme}
borderRadius={'md'}
isAnimated
hasStripe
borderWidth={'1px'}
borderColor={'borderColor.base'}
/>
</Box>
</Box>
</>
)}
</>
)}
{feConfigs?.docUrl && (
<Link
href={getDocPath('/docs/intro')}
target="_blank"
display={'flex'}
mt={4}
w={['85%', '300px']}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
userSelect={'none'}
textDecoration={'none !important'}
>
<MyIcon name={'common/courseLight'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{t('system.Help Document')}
</Box>
<Box w={'8px'} h={'8px'} borderRadius={'50%'} bg={'#67c13b'} />
<Box fontSize={'md'} ml={2}>
V{systemVersion}
</Box>
</Link>
)}
{feConfigs?.chatbotUrl && (
<Link
href={feConfigs.chatbotUrl}
target="_blank"
display={'flex'}
mt={4}
w={['85%', '300px']}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
userSelect={'none'}
textDecoration={'none !important'}
>
<MyIcon name={'core/app/aiLight'} w={'18px'} />
<Box ml={2} flex={1}>
{t('common.system.Help Chatbot')}
</Box>
</Link>
)}
{feConfigs?.show_openai_account && (
<>
<Divider my={3} />
<MyTooltip label={'点击配置账号'}>
<Flex
w={['85%', '300px']}
py={4}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
bg={'myWhite.300'}
alignItems={'center'}
cursor={'pointer'}
userSelect={'none'}
onClick={onOpenOpenai}
>
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
OpenAI/OneAPI
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.openaiAccount?.key ? '#67c13b' : 'myGray.500'}
/>
</Flex>
</MyTooltip>
</>
)}
</Box>
{isOpenPayModal && <PayModal onClose={onClosePayModal} />}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenOpenai && userInfo && (
<OpenAIAccountModal
defaultData={userInfo?.openaiAccount}
onSuccess={(data) =>
onclickSave({
...userInfo,
openaiAccount: data
})
}
onClose={onCloseOpenai}
/>
)}
<File onSelect={onSelectFile} />
</Box>
);
};
export default React.memo(UserInfo);

View File

@@ -1,45 +1,37 @@
import React, { useState, useCallback } from 'react';
import { ModalFooter, ModalBody, Button, Input, Box, Grid } from '@chakra-ui/react';
import { getWxPayQRCode } from '@/web/support/wallet/bill/api';
import { getPayCode, checkPayResult } from '@/web/support/wallet/pay/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import Markdown from '@/components/Markdown';
import MyModal from '@/components/MyModal';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
const PayModal = ({
onClose,
defaultValue,
onSuccess
}: {
defaultValue?: number;
onClose: () => void;
onSuccess?: () => any;
}) => {
const PayModal = ({ onClose }: { onClose: () => void }) => {
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const [inputVal, setInputVal] = useState<number | undefined>(defaultValue);
const [inputVal, setInputVal] = useState<number | ''>('');
const [loading, setLoading] = useState(false);
const [qrPayData, setQRPayData] = useState<QRPayProps>();
const [payId, setPayId] = useState('');
const handleClickPay = useCallback(async () => {
if (!inputVal || inputVal <= 0 || isNaN(+inputVal)) return;
setLoading(true);
try {
// 获取支付二维码
const res = await getWxPayQRCode({
type: BillTypeEnum.balance,
balance: inputVal
});
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
const res = await getPayCode(inputVal);
new window.QRCode(document.getElementById('payQRCode'), {
text: res.codeUrl,
width: 128,
height: 128,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
setPayId(res.payId);
} catch (err) {
toast({
title: getErrText(err),
@@ -49,48 +41,84 @@ const PayModal = ({
setLoading(false);
}, [inputVal, toast]);
useQuery(
[payId],
() => {
if (!payId) return null;
return checkPayResult(payId);
},
{
enabled: !!payId,
refetchInterval: 3000,
onSuccess(res) {
if (!res) return;
toast({
title: res,
status: 'success'
});
router.reload();
}
}
);
return (
<MyModal isOpen={true} onClose={onClose} title={t('user.Pay')} iconSrc="/imgs/modal/pay.svg">
<MyModal
isOpen={true}
onClose={payId ? undefined : onClose}
title={t('user.Pay')}
iconSrc="/imgs/modal/pay.svg"
>
<ModalBody px={0} display={'flex'} flexDirection={'column'}>
<Grid gridTemplateColumns={'repeat(3,1fr)'} gridGap={5} mb={4} px={6}>
{[10, 20, 50, 100, 200, 500].map((item) => (
<Button
key={item}
variant={item === inputVal ? 'solid' : 'outline'}
onClick={() => setInputVal(item)}
>
{item}
</Button>
))}
</Grid>
<Box px={6}>
<Input
value={inputVal}
type={'number'}
step={1}
placeholder={'其他金额,请取整数'}
onChange={(e) => {
setInputVal(Math.floor(+e.target.value));
}}
></Input>
{!payId && (
<>
<Grid gridTemplateColumns={'repeat(3,1fr)'} gridGap={5} mb={4} px={6}>
{[10, 20, 50, 100, 200, 500].map((item) => (
<Button
key={item}
variant={item === inputVal ? 'solid' : 'outline'}
onClick={() => setInputVal(item)}
>
{item}
</Button>
))}
</Grid>
<Box px={6}>
<Input
value={inputVal}
type={'number'}
step={1}
placeholder={'其他金额,请取整数'}
onChange={(e) => {
setInputVal(Math.floor(+e.target.value));
}}
></Input>
</Box>
</>
)}
{/* 付费二维码 */}
<Box textAlign={'center'}>
{payId && <Box mb={3}>: {inputVal}</Box>}
<Box id={'payQRCode'} display={'inline-block'}></Box>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Close')}
</Button>
<Button
ml={3}
isLoading={loading}
isDisabled={!inputVal || inputVal === 0}
onClick={handleClickPay}
>
</Button>
{!payId && (
<>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Close')}
</Button>
<Button
ml={3}
isLoading={loading}
isDisabled={!inputVal || inputVal === 0}
onClick={handleClickPay}
>
</Button>
</>
)}
</ModalFooter>
{!!qrPayData && <QRCodePayModal {...qrPayData} onSuccess={onSuccess} />}
</MyModal>
);
};

View File

@@ -0,0 +1,108 @@
import React, { useState, useCallback } from 'react';
import {
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box
} from '@chakra-ui/react';
import { getPayOrders, checkPayResult } from '@/web/support/wallet/pay/api';
import type { PaySchema } from '@fastgpt/global/support/wallet/pay/type.d';
import dayjs from 'dayjs';
import { useQuery } from '@tanstack/react-query';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useLoading } from '@/web/common/hooks/useLoading';
import MyIcon from '@fastgpt/web/components/common/Icon';
const PayRecordTable = () => {
const { Loading, setIsLoading } = useLoading();
const [payOrders, setPayOrders] = useState<PaySchema[]>([]);
const { toast } = useToast();
const { isInitialLoading, refetch } = useQuery(['initPayOrder'], getPayOrders, {
onSuccess(res) {
setPayOrders(res);
}
});
const handleRefreshPayOrder = useCallback(
async (payId: string) => {
setIsLoading(true);
try {
const data = await checkPayResult(payId);
toast({
title: data,
status: 'success'
});
} catch (error: any) {
toast({
title: error?.message,
status: 'warning'
});
console.log(error);
}
try {
refetch();
} catch (error) {}
setIsLoading(false);
},
[refetch, setIsLoading, toast]
);
return (
<Box position={'relative'} h={'100%'} overflow={'overlay'}>
{!isInitialLoading && payOrders.length === 0 ? (
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} justifyContent={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
~
</Box>
</Flex>
) : (
<TableContainer py={[0, 5]} px={[3, 8]}>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{payOrders.map((item) => (
<Tr key={item._id}>
<Td>{item.orderId}</Td>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{formatStorePrice2Read(item.price)}</Td>
<Td>{item.status}</Td>
<Td>
{item.status === 'NOTPAY' && (
<Button onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
</Button>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)}
<Loading loading={isInitialLoading} fixed={false} />
</Box>
);
};
export default PayRecordTable;

View File

@@ -1,119 +0,0 @@
import React, { useMemo } from 'react';
import {
ModalBody,
Flex,
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { UsageItemType } from '@fastgpt/global/support/wallet/usage/type.d';
import dayjs from 'dayjs';
import { UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import { formatNumber } from '@fastgpt/global/common/math/tools';
const UsageDetail = ({ usage, onClose }: { usage: UsageItemType; onClose: () => void }) => {
const { t } = useTranslation();
const filterBillList = useMemo(
() => usage.list.filter((item) => item && item.moduleName),
[usage.list]
);
const { hasModel, hasCharsLen, hasDuration } = useMemo(() => {
let hasModel = false;
let hasCharsLen = false;
let hasDuration = false;
let hasDataLen = false;
usage.list.forEach((item) => {
if (item.model !== undefined) {
hasModel = true;
}
if (typeof item.charsLength === 'number') {
hasCharsLen = true;
}
if (typeof item.duration === 'number') {
hasDuration = true;
}
});
return {
hasModel,
hasCharsLen,
hasDuration,
hasDataLen
};
}, [usage.list]);
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('support.wallet.usage.Usage Detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('support.wallet.bill.Number')}:</Box>
<Box>{usage.id}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('support.wallet.usage.Time')}:</Box>
<Box>{dayjs(usage.time).format('YYYY/MM/DD HH:mm:ss')}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('support.wallet.usage.App name')}:</Box>
<Box>{t(usage.appName) || '-'}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('support.wallet.usage.Source')}:</Box>
<Box>{t(UsageSourceMap[usage.source]?.label)}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<Box flex={'0 0 80px'}>{t('support.wallet.usage.Total points')}:</Box>
<Box fontWeight={'bold'}>{formatNumber(usage.totalPoints)}</Box>
</Flex>
<Box pb={4}>
<Box flex={'0 0 80px'} mb={1}>
{t('support.wallet.usage.Bill Module')}
</Box>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>{t('support.wallet.usage.Module name')}</Th>
{hasModel && <Th>{t('support.wallet.usage.Ai model')}</Th>}
{hasCharsLen && <Th>{t('support.wallet.usage.Text Length')}</Th>}
{hasDuration && <Th>{t('support.wallet.usage.Duration')}</Th>}
<Th>{t('support.wallet.usage.Total points')}</Th>
</Tr>
</Thead>
<Tbody>
{filterBillList.map((item, i) => (
<Tr key={i}>
<Td>{t(item.moduleName)}</Td>
{hasModel && <Td>{item.model ?? '-'}</Td>}
{hasCharsLen && <Td>{item.charsLength ?? '-'}</Td>}
{hasDuration && <Td>{item.duration ?? '-'}</Td>}
<Td>{formatNumber(item.amount)}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</ModalBody>
</MyModal>
);
};
export default UsageDetail;

View File

@@ -1,189 +0,0 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box,
Button
} from '@chakra-ui/react';
import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import { getUserUsages } from '@/web/support/wallet/usage/api';
import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import { usePagination } from '@/web/common/hooks/usePagination';
import { useLoading } from '@/web/common/hooks/useLoading';
import dayjs from 'dayjs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
import { addDays } from 'date-fns';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import MySelect from '@/components/Select';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamMembers } from '@/web/support/user/team/api';
import Avatar from '@/components/Avatar';
import { formatNumber } from '../../../../../../packages/global/common/math/tools';
const UsageDetail = dynamic(() => import('./UsageDetail'));
const UsageTable = () => {
const { t } = useTranslation();
const { Loading } = useLoading();
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const [usageSource, setUsageSource] = useState<`${UsageSourceEnum}` | ''>('');
const { isPc } = useSystemStore();
const { userInfo } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const sourceList = useMemo(
() => [
{ label: t('common.All'), value: '' },
...Object.entries(UsageSourceMap).map(([key, value]) => ({
label: t(value.label),
value: key
}))
],
[t]
);
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
if (!userInfo?.team?.teamId) return [];
return getTeamMembers(userInfo.team.teamId);
});
const tmbList = useMemo(
() =>
members.map((item) => ({
label: (
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'16px'} mr={1} />
{item.memberName}
</Flex>
),
value: item.tmbId
})),
[members]
);
const {
data: usages,
isLoading,
Pagination,
getData
} = usePagination<UsageItemType>({
api: getUserUsages,
pageSize: isPc ? 20 : 10,
params: {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
source: usageSource,
teamMemberId: selectTmbId
},
defaultRequest: false
});
useEffect(() => {
getData(1);
}, [usageSource, selectTmbId]);
return (
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Flex
flexDir={['column', 'row']}
gap={2}
w={'100%'}
px={[3, 8]}
alignItems={['flex-end', 'center']}
>
{tmbList.length > 1 && userInfo?.team?.canWrite && (
<Flex alignItems={'center'}>
<Box mr={2} flexShrink={0}>
{t('support.user.team.member')}
</Box>
<MySelect
size={'sm'}
minW={'100px'}
list={tmbList}
value={selectTmbId}
onchange={setSelectTmbId}
/>
</Flex>
)}
<Box flex={'1'} />
<Flex alignItems={'center'} gap={3}>
<DateRangePicker
defaultDate={dateRange}
position="bottom"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Flex>
</Flex>
<TableContainer px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
<Table>
<Thead>
<Tr>
{/* <Th>{t('user.team.Member Name')}</Th> */}
<Th>{t('user.Time')}</Th>
<Th>
<MySelect
list={sourceList}
value={usageSource}
size={'sm'}
onchange={(e) => {
setUsageSource(e);
}}
w={'130px'}
></MySelect>
</Th>
<Th>{t('user.Application Name')}</Th>
<Th>{t('support.wallet.usage.Total points')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{usages.map((item) => (
<Tr key={item.id}>
{/* <Td>{item.memberName}</Td> */}
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>{t(UsageSourceMap[item.source]?.label) || '-'}</Td>
<Td>{t(item.appName) || '-'}</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button size={'sm'} variant={'whitePrimary'} onClick={() => setUsageDetail(item)}>
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{!isLoading && usages.length === 0 && (
<Flex flex={'1 0 0'} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
使~
</Box>
</Flex>
)}
<Loading loading={isLoading} fixed={false} />
{!!usageDetail && (
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
)}
</Flex>
);
};
export default React.memo(UsageTable);

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/web/common/hooks/useRequest';
import type { UserType } from '@fastgpt/global/support/user/type.d';
const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="acount/plansBlue"
title={t('user.OpenAI Account Setting')}
>
<ModalBody></ModalBody>
<ModalFooter></ModalFooter>
</MyModal>
);
};
export default StandDetailModal;

View File

@@ -14,16 +14,18 @@ import { useTranslation } from 'next-i18next';
import Script from 'next/script';
const Promotion = dynamic(() => import('./components/Promotion'));
const UsageTable = dynamic(() => import('./components/UsageTable'));
const BillTable = dynamic(() => import('./components/BillTable'));
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'));
const InformTable = dynamic(() => import('./components/InformTable'));
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
const PriceBox = dynamic(() => import('@/components/support/wallet/Price'));
enum TabEnum {
'info' = 'info',
'promotion' = 'promotion',
'usage' = 'usage',
'bill' = 'bill',
'price' = 'price',
'pay' = 'pay',
'inform' = 'inform',
'apikey' = 'apikey',
'loginout' = 'loginout'
@@ -43,18 +45,27 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
...(feConfigs?.isPlus
? [
{
icon: 'support/usage/usageRecordLight',
icon: 'support/bill/billRecordLight',
label: t('user.Usage Record'),
id: TabEnum.usage
id: TabEnum.bill
}
]
: []),
...(feConfigs?.show_pay && userInfo?.team.canWrite
? [
{
icon: 'support/bill/payRecordLight',
label: t('support.wallet.Bills'),
id: TabEnum.bill
icon: 'support/pay/payRecordLight',
label: t('user.Recharge Record'),
id: TabEnum.pay
}
]
: []),
...(feConfigs?.show_pay
? [
{
icon: 'support/pay/priceLight',
label: t('support.user.Price'),
id: TabEnum.price
}
]
: []),
@@ -97,6 +108,11 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
const { openConfirm, ConfirmModal } = useConfirm({
content: '确认退出登录?'
});
const {
isOpen: isOpenPriceBox,
onOpen: onOpenPriceBox,
onClose: onClosePriceBox
} = useDisclosure();
const router = useRouter();
const theme = useTheme();
@@ -108,6 +124,8 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
setUserInfo(null);
router.replace('/login');
})();
} else if (tab === TabEnum.price) {
onOpenPriceBox();
} else {
router.replace({
query: {
@@ -116,7 +134,7 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
});
}
},
[openConfirm, router, setUserInfo]
[onOpenPriceBox, openConfirm, router, setUserInfo]
);
return (
@@ -160,14 +178,16 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]}>
{currentTab === TabEnum.info && <UserInfo />}
{currentTab === TabEnum.promotion && <Promotion />}
{currentTab === TabEnum.usage && <UsageTable />}
{currentTab === TabEnum.bill && <BillTable />}
{currentTab === TabEnum.pay && <PayRecordTable />}
{currentTab === TabEnum.inform && <InformTable />}
{currentTab === TabEnum.apikey && <ApiKeyTable />}
</Box>
</Flex>
<ConfirmModal />
</PageContainer>
{isOpenPriceBox && <PriceBox onClose={onClosePriceBox} />}
</>
);
};

View File

@@ -1,99 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { PgClient } from '@fastgpt/service/common/vectorStore/pg';
import { PgDatasetTableName } from '@fastgpt/global/common/vectorStore/constants';
import { MongoImage } from '@fastgpt/service/common/file/image/schema';
import { MongoImageSchemaType } from '@fastgpt/global/common/file/image/type';
import { delay } from '@fastgpt/global/common/system/utils';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { DYNAMIC_INPUT_KEY, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
let success = 0;
let deleteImg = 0;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// 设置所有app为 inited = false
const result = await MongoApp.updateMany({}, { $set: { inited: false } });
console.log(result);
await initApp();
jsonRes(res, {
message: 'success'
});
} catch (error) {
console.log(error);
jsonRes(res, {
code: 500,
error
});
}
}
const systemKeys: string[] = [
ModuleInputKeyEnum.switch,
ModuleInputKeyEnum.httpMethod,
ModuleInputKeyEnum.httpReqUrl,
ModuleInputKeyEnum.httpHeaders,
DYNAMIC_INPUT_KEY,
ModuleInputKeyEnum.addInputParam
];
const initApp = async (): Promise<any> => {
const app = await MongoApp.findOne({ inited: false }).sort({ updateTime: -1 });
if (!app) {
return;
}
try {
const modules = JSON.parse(JSON.stringify(app.modules)) as ModuleItemType[];
let update = false;
// 找到http模块
modules.forEach((module) => {
if (module.flowType === 'httpRequest') {
const method = module.inputs.find((input) => input.key === ModuleInputKeyEnum.httpMethod);
if (method?.value === 'POST') {
module.inputs.forEach((input) => {
// 更新非系统字段的key
if (!systemKeys.includes(input.key)) {
// 更新output的target
modules.forEach((item) => {
item.outputs.forEach((output) => {
output.targets.forEach((target) => {
if (target.moduleId === module.moduleId && target.key === input.key) {
target.key = `data.${input.key}`;
}
});
});
});
// 更新key
input.key = `data.${input.key}`;
update = true;
}
});
}
}
});
if (update) {
console.log('update http app');
app.modules = modules;
}
app.inited = true;
await app.save();
console.log(++success);
return initApp();
} catch (error) {
console.log(error);
await delay(1000);
return initApp();
}
};

View File

@@ -1,35 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUsage } from '@fastgpt/service/support/wallet/usage/schema';
import { connectionMongo } from '@fastgpt/service/common/mongo';
/* pg 中的数据搬到 mongo dataset.datas 中,并做映射 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
await authCert({ req, authRoot: true });
// 检查 usage 是否有记录
const totalUsages = await MongoUsage.countDocuments();
if (totalUsages === 0) {
// 重命名 bills 集合成 usages
await connectionMongo.connection.db.renameCollection('bills', 'usages', {
// 强制
dropTarget: true
});
}
jsonRes(res, {
message: 'success'
});
} catch (error) {
console.log(error);
jsonRes(res, {
code: 500,
error
});
}
}

View File

@@ -16,7 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
let filePaths: string[] = [];
try {
const { teamId, tmbId } = await authCert({ req, authToken: true });
const { userId, teamId, tmbId } = await authCert({ req, authToken: true });
const { file, bucketName, metadata } = await upload.doUpload(req, res);

View File

@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateQuestionGuideParams } from '@/global/core/ai/api.d';
import { pushQuestionGuideUsage } from '@/service/support/wallet/usage/push';
import { pushQuestionGuideBill } from '@/service/support/wallet/bill/push';
import { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
import { authCertOrShareId } from '@fastgpt/service/support/permission/auth/common';
@@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const qgModel = global.llmModels[0];
const { result, charsLength } = await createQuestionGuide({
const { result, inputTokens, outputTokens } = await createQuestionGuide({
messages,
model: qgModel.model
});
@@ -28,8 +28,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
data: result
});
pushQuestionGuideUsage({
charsLength,
pushQuestionGuideBill({
inputTokens,
outputTokens,
teamId,
tmbId
});

View File

@@ -6,7 +6,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { SimpleModeTemplate_FastGPT_Universal } from '@/global/core/app/constants';
import { checkTeamAppLimit } from '@/service/support/permission/teamLimit';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -26,7 +25,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
// 上限校验
await checkTeamAppLimit(teamId);
const authCount = await MongoApp.countDocuments({
teamId
});
if (authCount >= 50) {
throw new Error('每个团队上限 50 个应用');
}
// 创建模型
const response = await MongoApp.create({

View File

@@ -0,0 +1,51 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { Types } from '@fastgpt/service/common/mongo';
import { MongoBill } from '@fastgpt/service/support/wallet/bill/schema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { appId, start, end } = req.body as { appId: string; start: number; end: number };
const { userId } = await authCert({ req, authToken: true });
const result = await MongoBill.aggregate([
{
$match: {
appId: new Types.ObjectId(appId),
userId: new Types.ObjectId(userId),
time: { $gte: new Date(start) }
}
},
{
$group: {
_id: {
year: { $year: '$time' },
month: { $month: '$time' },
day: { $dayOfMonth: '$time' }
},
total: { $sum: '$total' }
}
},
{
$project: {
_id: 0,
date: { $dateFromParts: { year: '$_id.year', month: '$_id.month', day: '$_id.day' } },
total: 1
}
},
{ $sort: { date: 1 } }
]);
jsonRes(res, {
data: result
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -92,7 +92,7 @@ function simpleChatTemplate({ formData, maxToken }: Props): ModuleItemType[] {
},
{
key: 'model',
type: 'selectLLMModel',
type: 'selectChatModel',
label: 'core.module.input.label.aiModel',
required: true,
valueType: 'string',
@@ -471,7 +471,7 @@ function datasetTemplate({ formData, maxToken }: Props): ModuleItemType[] {
},
{
key: 'model',
type: 'selectLLMModel',
type: 'selectChatModel',
label: 'core.module.input.label.aiModel',
required: true,
valueType: 'string',

View File

@@ -88,7 +88,7 @@ function simpleChatTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
},
{
key: 'model',
type: 'selectLLMModel',
type: 'selectChatModel',
label: 'core.module.input.label.aiModel',
required: true,
valueType: 'string',
@@ -498,7 +498,7 @@ function datasetTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
},
{
key: 'model',
type: 'selectLLMModel',
type: 'selectChatModel',
label: 'core.module.input.label.aiModel',
required: true,
valueType: 'string',

View File

@@ -12,7 +12,7 @@ import { getLLMModel } from '@/service/core/ai/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { name, avatar, type, simpleTemplateId, intro, modules, permission, teamTags } =
const { name, avatar, type, simpleTemplateId, intro, modules, permission } =
req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
@@ -65,7 +65,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
avatar,
intro,
permission,
teamTags: teamTags,
...(modules && {
modules
})

View File

@@ -1,82 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { AppUpdateParams } from '@fastgpt/global/core/app/api';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { getLLMModel } from '@/service/core/ai/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { name, avatar, type, simpleTemplateId, intro, modules, permission, teamTags } =
req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
if (!appId) {
throw new Error('appId is empty');
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: permission ? 'owner' : 'w' });
// check modules
// 1. dataset search limit, less than model quoteMaxToken
if (modules) {
let maxTokens = 3000;
modules.forEach((item) => {
if (item.flowType === FlowNodeTypeEnum.chatNode) {
const model =
item.inputs.find((item) => item.key === ModuleInputKeyEnum.aiModel)?.value || '';
const chatModel = getLLMModel(model);
const quoteMaxToken = chatModel.quoteMaxToken || 3000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}
});
modules.forEach((item) => {
if (item.flowType === FlowNodeTypeEnum.datasetSearchNode) {
item.inputs.forEach((input) => {
if (input.key === ModuleInputKeyEnum.datasetMaxTokens) {
const val = input.value as number;
if (val > maxTokens) {
input.value = maxTokens;
}
}
});
}
});
}
// 更新模型
await MongoApp.findOneAndUpdate(
{
_id: appId
},
{
name,
type,
simpleTemplateId,
avatar,
intro,
permission,
teamTags: teamTags,
...(modules && {
modules
})
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,13 +4,13 @@ import { sseErrRes } from '@fastgpt/service/common/response';
import { sseResponseEventEnum } from '@fastgpt/service/common/response/constant';
import { responseWrite } from '@fastgpt/service/common/response';
import type { ModuleItemType } from '@fastgpt/global/core/module/type.d';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { pushChatBill } from '@/service/support/wallet/bill/push';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { dispatchModules } from '@/service/moduleDispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserAndAuthBalance } from '@fastgpt/service/support/user/controller';
export type Props = {
history: ChatItemType[];
@@ -50,10 +50,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
]);
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const user = await getUserAndAuthBalance({
tmbId,
minBalance: 0
});
/* start process */
const { responseData, moduleDispatchBills } = await dispatchModules({
const { responseData } = await dispatchModules({
res,
mode: 'test',
teamId,
@@ -82,13 +85,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
res.end();
pushChatUsage({
pushChatBill({
appName,
appId,
teamId,
tmbId,
source: UsageSourceEnum.fastgpt,
moduleDispatchBills
source: BillSourceEnum.fastgpt,
response: responseData
});
} catch (err: any) {
res.status(500);

View File

@@ -14,13 +14,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
const { appId, shareId, outLinkUid } = req.query as ClearHistoriesProps;
let chatAppId = appId;
const match = await (async () => {
if (shareId && outLinkUid) {
const { appId, uid } = await authOutLink({ shareId, outLinkUid });
const { uid } = await authOutLink({ shareId, outLinkUid });
chatAppId = appId;
return {
shareId,
outLinkUid: uid
@@ -44,11 +41,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const idList = list.map((item) => item.chatId);
await MongoChatItem.deleteMany({
appId: chatAppId,
appId,
chatId: { $in: idList }
});
await MongoChat.deleteMany({
appId: chatAppId,
appId,
chatId: { $in: idList }
});

View File

@@ -28,13 +28,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
};
}
if (appId && outLinkUid) {
return {
shareId,
outLinkUid: outLinkUid,
source: ChatSourceEnum.team
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true });
return {
@@ -43,7 +36,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
source: ChatSourceEnum.online
};
}
return Promise.reject('Params are error');
})();

View File

@@ -3,9 +3,9 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { GetChatSpeechProps } from '@/global/core/chat/api.d';
import { text2Speech } from '@fastgpt/service/core/ai/audio/speech';
import { pushAudioSpeechUsage } from '@/service/support/wallet/usage/push';
import { pushAudioSpeechBill } from '@/service/support/wallet/bill/push';
import { authCertOrShareId } from '@fastgpt/service/support/permission/auth/common';
import { authType2UsageSource } from '@/service/support/wallet/usage/utils';
import { authType2BillSource } from '@/service/support/wallet/bill/utils';
import { getAudioSpeechModel } from '@/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
@@ -54,12 +54,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
speed: ttsConfig.speed,
onSuccess: async ({ model, buffer }) => {
try {
pushAudioSpeechUsage({
pushAudioSpeechBill({
model: model,
charsLength: input.length,
tmbId,
teamId,
source: authType2UsageSource({ authType })
source: authType2BillSource({ authType })
});
await MongoTTSBuffer.create({

View File

@@ -1,37 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { chatByTeamProps } from '@/global/core/chat/api.d';
import axios from 'axios';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { selectShareResponse } from '@/utils/service/core/chat';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
let { teamId, appId, outLinkUid } = req.query as chatByTeamProps;
const history = await MongoChatItem.find({
appId: appId,
outLinkUid: outLinkUid,
teamId: teamId
});
jsonRes(res, {
data: history
});
} catch (err) {
jsonRes(res, {
code: 500,
data: req.query,
error: err
});
}
}
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@@ -1,91 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { getChatModelNameListByModules } from '@/service/core/app/module';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
let { appId, chatId, outLinkUid } = req.query as {
chatId?: string;
appId?: string;
outLinkUid?: string;
};
if (!appId) {
return jsonRes(res, {
code: 501,
message: "You don't have an app yet"
});
}
// auth app permission
const [chat, app] = await Promise.all([
// authApp({
// req,
// authToken: false,
// appId,
// per: 'r'
// }),
chatId ? MongoChat.findOne({ appId, chatId }) : undefined,
MongoApp.findById(appId).lean()
]);
if (!app) {
throw new Error(AppErrEnum.unExist);
}
// auth chat permission
// if (chat && chat.outLinkUid !== outLinkUid) {
// throw new Error(ChatErrEnum.unAuthChat);
// }
// // auth chat permission
// if (chat && !app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
// throw new Error(ChatErrEnum.unAuthChat);
// }
// get app and history
const { history } = await getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${ModuleOutputKeyEnum.responseData}`
});
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@@ -1,81 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { getGuideModule } from '@fastgpt/global/core/module/utils';
import { getChatModelNameListByModules } from '@/service/core/app/module';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
let { appId, chatId, loadCustomFeedbacks } = req.query as InitChatProps;
if (!appId) {
return jsonRes(res, {
code: 501,
message: "You don't have an app yet"
});
}
// auth app permission
const [{ app, tmbId }, chat] = await Promise.all([
authApp({
req,
authToken: true,
appId,
per: 'r'
}),
chatId ? MongoChat.findOne({ appId, chatId }) : undefined
]);
// // auth chat permission
// if (chat && !app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
// throw new Error(ChatErrEnum.unAuthChat);
// }
// get app and history
const { history } = await getChatItems({
appId,
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
ModuleOutputKeyEnum.responseData
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
});
jsonRes<InitChatResponse>(res, {
data: {
chatId,
appId,
title: chat?.title || '新对话',
userAvatar: undefined,
variables: chat?.variables || {},
history,
app: {
userGuideModule: getGuideModule(app.modules),
chatModels: getChatModelNameListByModules(app.modules),
name: app.name,
avatar: app.avatar,
intro: app.intro
}
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@@ -24,7 +24,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await MongoChat.findOneAndUpdate(
{ appId, chatId },
{
updateTime: new Date(),
...(customTitle !== undefined && { customTitle }),
...(top !== undefined && { top })
}

View File

@@ -11,12 +11,13 @@ import {
TrainingModeEnum,
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { checkDatasetLimit } from '@/service/support/permission/teamLimit';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/limit/dataset';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { createTrainingBill } from '@fastgpt/service/support/wallet/bill/controller';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { getLLMModel, getVectorModel } from '@/service/core/ai/model';
import { reloadCollectionChunks } from '@fastgpt/service/core/dataset/collection/utils';
import { getStandardSubPlan } from '@/service/support/wallet/sub/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -42,7 +43,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 1. check dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, new Array(10))
insertLen: predictDataLimitLength(trainingType, new Array(10)),
standardPlans: getStandardSubPlan()
});
const { _id: collectionId } = await mongoSessionRun(async (session) => {
@@ -64,11 +66,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
});
// 3. create bill and start sync
const { billId } = await createTrainingUsage({
const { billId } = await createTrainingBill({
teamId,
tmbId,
appName: 'core.dataset.collection.Sync Collection',
billSource: UsageSourceEnum.training,
billSource: BillSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel).name,
agentModel: getLLMModel(dataset.agentModel).name,
session

View File

@@ -12,13 +12,14 @@ import {
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
import { checkDatasetLimit } from '@/service/support/permission/teamLimit';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/limit/dataset';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataToTrainingQueue } from '@/service/core/dataset/data/controller';
import { hashStr } from '@fastgpt/global/common/string/tools';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { createTrainingBill } from '@fastgpt/service/support/wallet/bill/controller';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { getLLMModel, getVectorModel } from '@/service/core/ai/model';
import { getStandardSubPlan } from '@/service/support/wallet/sub/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -52,7 +53,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 2. check dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, chunks)
insertLen: predictDataLimitLength(trainingType, chunks),
standardPlans: getStandardSubPlan()
});
// 3. create collection and training bill
@@ -72,11 +74,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
hashRawText: hashStr(text),
rawTextLength: text.length
}),
createTrainingUsage({
createTrainingBill({
teamId,
tmbId,
appName: name,
billSource: UsageSourceEnum.training,
billSource: BillSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel)?.name,
agentModel: getLLMModel(dataset.agentModel)?.name
})

View File

@@ -12,8 +12,8 @@ import {
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { createTrainingBill } from '@fastgpt/service/support/wallet/bill/controller';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { getLLMModel, getVectorModel } from '@/service/core/ai/model';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
@@ -56,11 +56,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await mongoSessionRun(async (session) => {
// create training bill
const { billId } = await createTrainingUsage({
const { billId } = await createTrainingBill({
teamId: collection.teamId,
tmbId,
appName: 'core.dataset.collection.Sync Collection',
billSource: UsageSourceEnum.training,
billSource: BillSourceEnum.training,
vectorModel: vectorModelData.name,
agentModel: agentModelData.name,
session

View File

@@ -7,7 +7,6 @@ import { createDefaultCollection } from '@fastgpt/service/core/dataset/collectio
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { getLLMModel, getVectorModel, getDatasetModel } from '@/service/core/ai/model';
import { checkTeamDatasetLimit } from '@/service/support/permission/teamLimit';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -32,7 +31,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
}
// check limit
await checkTeamDatasetLimit(teamId);
const authCount = await MongoDataset.countDocuments({
teamId,
type: DatasetTypeEnum.dataset
});
if (authCount >= 50) {
throw new Error('每个团队上限 50 个知识库');
}
const { _id } = await MongoDataset.create({
name,

View File

@@ -3,7 +3,8 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { connectToDatabase } from '@/service/mongo';
import { authDatasetData } from '@/service/support/permission/auth/dataset';
import { deleteDatasetData } from '@/service/core/dataset/data/controller';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { deleteDatasetDataVector } from '@fastgpt/service/common/vectorStore/controller';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -25,7 +26,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
per: 'w'
});
await deleteDatasetData(datasetData);
// update mongo data update time
await MongoDatasetData.findByIdAndUpdate(dataId, {
updateTime: new Date()
});
// delete vector data
await deleteDatasetDataVector({
teamId,
idList: datasetData.indexes.map((item) => item.dataId)
});
// delete mongo data
await MongoDatasetData.findByIdAndDelete(dataId);
jsonRes(res, {
data: 'success'

View File

@@ -12,10 +12,12 @@ import { hasSameValue } from '@/service/core/dataset/data/utils';
import { insertData2Dataset } from '@/service/core/dataset/data/controller';
import { authDatasetCollection } from '@fastgpt/service/support/permission/auth/dataset';
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { authTeamBalance } from '@/service/support/permission/auth/bill';
import { pushGenerateVectorBill } from '@/service/support/wallet/bill/push';
import { InsertOneDatasetDataProps } from '@/global/core/dataset/api';
import { simpleText } from '@fastgpt/global/common/string/tools';
import { checkDatasetLimit } from '@/service/support/permission/teamLimit';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/limit/dataset';
import { getStandardSubPlan } from '@/service/support/wallet/sub/utils';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -41,7 +43,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
await checkDatasetLimit({
teamId,
insertLen: 1
insertLen: 1,
standardPlans: getStandardSubPlan()
});
// auth collection and get dataset
@@ -49,7 +52,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
{
datasetId: { _id: datasetId, vectorModel }
}
] = await Promise.all([getCollectionWithDataset(collectionId)]);
] = await Promise.all([getCollectionWithDataset(collectionId), authTeamBalance(teamId)]);
// format data
const formatQ = simpleText(q);
@@ -87,7 +90,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
indexes: formatIndexes
});
pushGenerateVectorUsage({
pushGenerateVectorBill({
teamId,
tmbId,
charsLength,

View File

@@ -8,9 +8,10 @@ import type {
PushDatasetDataResponse
} from '@fastgpt/global/core/dataset/api.d';
import { authDatasetCollection } from '@fastgpt/service/support/permission/auth/dataset';
import { checkDatasetLimit } from '@/service/support/permission/teamLimit';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/limit/dataset';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataToTrainingQueue } from '@/service/core/dataset/data/controller';
import { getStandardSubPlan } from '@/service/support/wallet/sub/utils';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -37,7 +38,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
// auth dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(collection.trainingType, data)
insertLen: predictDataLimitLength(collection.trainingType, data),
standardPlans: getStandardSubPlan()
});
jsonRes<PushDatasetDataResponse>(res, {

View File

@@ -4,9 +4,9 @@ import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { connectToDatabase } from '@/service/mongo';
import { updateData2Dataset } from '@/service/core/dataset/data/controller';
import { authDatasetData } from '@/service/support/permission/auth/dataset';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { authTeamBalance } from '@/service/support/permission/auth/bill';
import { pushGenerateVectorBill } from '@/service/support/wallet/bill/push';
import { UpdateDatasetDataProps } from '@/global/core/dataset/api';
import { checkDatasetLimit } from '@/service/support/permission/teamLimit';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -29,10 +29,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
// auth team balance
await checkDatasetLimit({
teamId,
insertLen: 1
});
await authTeamBalance(teamId);
const { charsLength } = await updateData2Dataset({
dataId: id,
@@ -42,7 +39,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
model: vectorModel
});
pushGenerateVectorUsage({
pushGenerateVectorBill({
teamId,
tmbId,
charsLength,

View File

@@ -4,16 +4,14 @@ import { withNextCors } from '@fastgpt/service/common/middle/cors';
import type { SearchTestProps, SearchTestResponse } from '@/global/core/dataset/api.d';
import { connectToDatabase } from '@/service/mongo';
import { authDataset } from '@fastgpt/service/support/permission/auth/dataset';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { authTeamBalance } from '@/service/support/permission/auth/bill';
import { pushGenerateVectorBill } from '@/service/support/wallet/bill/push';
import { searchDatasetData } from '@/service/core/dataset/data/controller';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { getLLMModel } from '@/service/core/ai/model';
import { queryExtension } from '@fastgpt/service/core/ai/functions/queryExtension';
import { datasetSearchQueryExtension } from '@fastgpt/service/core/dataset/search/utils';
import {
checkTeamAIPoints,
checkTeamReRankPermission
} from '@/service/support/permission/teamLimit';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -45,7 +43,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
per: 'r'
});
// auth balance
await checkTeamAIPoints(teamId);
await authTeamBalance(teamId);
// query extension
const extensionModel =
@@ -67,27 +65,28 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
similarity,
datasetIds: [datasetId],
searchMode,
usingReRank: usingReRank && (await checkTeamReRankPermission(teamId))
usingReRank
});
// push bill
const { totalPoints } = pushGenerateVectorUsage({
const { total } = pushGenerateVectorBill({
teamId,
tmbId,
charsLength,
model: dataset.vectorModel,
source: apikey ? UsageSourceEnum.api : UsageSourceEnum.fastgpt,
source: apikey ? BillSourceEnum.api : BillSourceEnum.fastgpt,
...(aiExtensionResult &&
extensionModel && {
extensionModel: extensionModel.name,
extensionCharsLength: aiExtensionResult.charsLength
extensionInputTokens: aiExtensionResult.inputTokens,
extensionOutputTokens: aiExtensionResult.outputTokens
})
});
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints: totalPoints
usage: total
});
}

View File

@@ -4,7 +4,6 @@ import { connectToDatabase } from '@/service/mongo';
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { checkTeamPluginLimit } from '@/service/support/permission/teamLimit';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -12,8 +11,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
const body = req.body as CreateOnePluginParams;
await checkTeamPluginLimit(teamId);
const { _id } = await MongoPlugin.create({
...body,
teamId,

View File

@@ -34,6 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
});
const textResult = replaceVariable(text, obj);
res.json({
text: textResult
});

View File

@@ -5,7 +5,6 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { UserUpdateParams } from '@/types/user';
import { getAIApi, openaiBaseUrl } from '@fastgpt/service/core/ai/config';
import { connectToDatabase } from '@/service/mongo';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
/* update user info */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -13,12 +12,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const { avatar, timezone, openaiAccount } = req.body as UserUpdateParams;
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) {
throw new Error('can not find it');
}
const userId = tmb.userId;
const { userId } = await authCert({ req, authToken: true });
// auth key
if (openaiAccount?.key) {
console.log('auth user openai key', openaiAccount?.key);

View File

@@ -3,7 +3,6 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { connectToDatabase } from '@/service/mongo';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -14,12 +13,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
throw new Error('Params is missing');
}
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) {
throw new Error('can not find it');
}
const userId = tmb.userId;
const { userId } = await authCert({ req, authToken: true });
// auth old password
const user = await MongoUser.findOne({
_id: userId,

View File

@@ -2,7 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { checkDatasetLimit } from '@/service/support/permission/teamLimit';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/limit/dataset';
import { getStandardSubPlan } from '@/service/support/wallet/sub/utils';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -22,7 +23,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await checkDatasetLimit({
teamId,
insertLen: numberSize
insertLen: numberSize,
standardPlans: getStandardSubPlan()
});
jsonRes(res);

View File

@@ -1,16 +1,16 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { CreateTrainingUsageProps } from '@fastgpt/global/support/wallet/usage/api.d';
import { BillSourceEnum } from '@fastgpt/global/support/wallet/bill/constants';
import { CreateTrainingBillProps } from '@fastgpt/global/support/wallet/bill/api.d';
import { getLLMModel, getVectorModel } from '@/service/core/ai/model';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { createTrainingBill } from '@fastgpt/service/support/wallet/bill/controller';
import { authDataset } from '@fastgpt/service/support/permission/auth/dataset';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { name, datasetId } = req.body as CreateTrainingUsageProps;
const { name, datasetId } = req.body as CreateTrainingBillProps;
const { teamId, tmbId, dataset } = await authDataset({
req,
@@ -20,11 +20,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
per: 'w'
});
const { billId } = await createTrainingUsage({
const { billId } = await createTrainingBill({
teamId,
tmbId,
appName: name,
billSource: UsageSourceEnum.training,
billSource: BillSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel).name,
agentModel: getLLMModel(dataset.agentModel).name
});

View File

@@ -2,21 +2,22 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getTeamSubPlans } from '@fastgpt/service/support/wallet/sub/utils';
import { getTeamSubPlanStatus } from '@fastgpt/service/support/wallet/sub/utils';
import { getStandardSubPlan } from '@/service/support/wallet/sub/utils';
import { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { FeTeamSubType } from '@fastgpt/global/support/wallet/sub/type';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
// 凭证校验
const { teamId } = await authCert({
req,
authToken: true
});
jsonRes<FeTeamPlanStatusType>(res, {
data: await getTeamSubPlans({
jsonRes<FeTeamSubType>(res, {
data: await getTeamSubPlanStatus({
teamId,
standardPlans: getStandardSubPlan()
})

View File

@@ -6,7 +6,7 @@ import { getUploadModel } from '@fastgpt/service/common/file/multer';
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import fs from 'fs';
import { getAIApi } from '@fastgpt/service/core/ai/config';
import { pushWhisperUsage } from '@/service/support/wallet/usage/push';
import { pushWhisperBill } from '@/service/support/wallet/bill/push';
const upload = getUploadModel({
maxSize: 2
@@ -40,7 +40,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
model: global.whisperModel.model
});
pushWhisperUsage({
pushWhisperBill({
teamId,
tmbId,
duration

View File

@@ -13,16 +13,16 @@ import { gptMessage2ChatType, textAdaptGptResponse } from '@/utils/adapt';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { saveChat } from '@/service/utils/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { pushChatBill } from '@/service/support/wallet/bill/push';
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';
import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
import { pushResult2Remote, updateOutLinkUsage } from '@fastgpt/service/support/outLink/tools';
import requestIp from 'request-ip';
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
import { getBillSourceByAuthType } from '@fastgpt/global/support/wallet/bill/tools';
import { selectShareResponse } from '@/utils/service/core/chat';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { connectToDatabase } from '@/service/mongo';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserAndAuthBalance } from '@fastgpt/service/support/user/controller';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { autChatCrud } from '@/service/support/permission/auth/chat';
@@ -35,14 +35,9 @@ type FastGptShareChatProps = {
shareId?: string;
outLinkUid?: string;
};
type FastGptTeamShareChatProps = {
teamId?: string;
outLinkUid?: string;
};
export type Props = ChatCompletionCreateParams &
FastGptWebChatProps &
FastGptShareChatProps &
FastGptTeamShareChatProps & {
FastGptShareChatProps & {
messages: ChatMessageItemType[];
stream?: boolean;
detail?: boolean;
@@ -65,7 +60,6 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const {
chatId,
appId,
teamId,
shareId,
outLinkUid,
stream = false,
@@ -103,96 +97,90 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
/* auth app permission */
const { teamId, tmbId, user, app, responseDetail, authType, apikey, canWrite, outLinkUserId } =
await (async () => {
if (shareId && outLinkUid) {
const { teamId, tmbId, user, appId, authType, responseDetail, uid } =
await authOutLinkChatStart({
shareId,
ip: originIp,
outLinkUid,
question: question.value
});
const app = await MongoApp.findById(appId);
if (!app) {
return Promise.reject('app is empty');
}
return {
teamId,
tmbId,
user,
app,
responseDetail,
apikey: '',
authType,
canWrite: false,
outLinkUserId: uid
};
}
const {
appId: apiKeyAppId,
teamId,
tmbId,
authType,
apikey
} = await authCert({
req,
authToken: true,
authApiKey: true
const { user, app, responseDetail, authType, apikey, canWrite, uid } = await (async () => {
if (shareId && outLinkUid) {
const { user, appId, authType, responseDetail, uid } = await authOutLinkChatStart({
shareId,
ip: originIp,
outLinkUid,
question: question.value
});
const app = await MongoApp.findById(appId);
const user = await getUserChatInfoAndAuthTeamPoints(tmbId);
// openapi key
if (authType === AuthUserTypeEnum.apikey) {
if (!apiKeyAppId) {
return Promise.reject(
'Key is error. You need to use the app key rather than the account key.'
);
}
const app = await MongoApp.findById(apiKeyAppId);
if (!app) {
return Promise.reject('app is empty');
}
return {
teamId,
tmbId,
user,
app,
responseDetail: detail,
apikey,
authType,
canWrite: true
};
if (!app) {
return Promise.reject('app is empty');
}
return {
user,
app,
responseDetail,
apikey: '',
authType,
canWrite: false,
uid
};
}
const {
appId: apiKeyAppId,
tmbId,
authType,
apikey
} = await authCert({
req,
authToken: true,
authApiKey: true
});
const user = await getUserAndAuthBalance({
tmbId,
minBalance: 0
});
// openapi key
if (authType === AuthUserTypeEnum.apikey) {
if (!apiKeyAppId) {
return Promise.reject(
'Key is error. You need to use the app key rather than the account key.'
);
}
const app = await MongoApp.findById(apiKeyAppId);
if (!app) {
return Promise.reject('app is empty');
}
// token auth
if (!appId) {
return Promise.reject('appId is empty');
}
const { app, canWrite } = await authApp({
req,
authToken: true,
appId,
per: 'r'
});
return {
teamId,
tmbId,
user,
app,
responseDetail: detail,
apikey,
authType,
canWrite: canWrite || false
canWrite: true
};
})();
}
// token auth
if (!appId) {
return Promise.reject('appId is empty');
}
const { app, canWrite } = await authApp({
req,
authToken: true,
appId,
per: 'r'
});
return {
user,
app,
responseDetail: detail,
apikey,
authType,
canWrite: canWrite || false
};
})();
// auth chat permission
await autChatCrud({
@@ -213,17 +201,16 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
limit: 30,
field: `dataId obj value`
});
const concatHistories = history.concat(chatMessages);
const responseChatItemId: string | undefined = messages[messages.length - 1].dataId;
/* start flow controller */
const { responseData, moduleDispatchBills, answerText } = await dispatchModules({
const { responseData, answerText } = await dispatchModules({
res,
mode: 'chat',
user,
teamId: String(teamId),
tmbId: String(tmbId),
teamId: String(user.team.teamId),
tmbId: String(user.team.tmbId),
appId: String(app._id),
chatId,
responseChatItemId,
@@ -236,19 +223,18 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
stream,
detail
});
console.log('af');
// save chat
if (chatId) {
await saveChat({
chatId,
appId: app._id,
teamId: teamId,
tmbId: tmbId,
teamId: user.team.teamId,
tmbId: user.team.tmbId,
variables,
updateUseTime: !shareId && String(tmbId) === String(app.tmbId), // owner update use time
updateUseTime: !shareId && String(user.team.tmbId) === String(app.tmbId), // owner update use time
shareId,
outLinkUid: outLinkUserId,
outLinkUid: uid,
source: (() => {
if (shareId) {
return ChatSourceEnum.share;
@@ -319,29 +305,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
// add record
const { totalPoints } = pushChatUsage({
const { total } = pushChatBill({
appName: app.name,
appId: app._id,
teamId: teamId,
tmbId: tmbId,
source: getUsageSourceByAuthType({ shareId, authType }),
moduleDispatchBills
teamId: user.team.teamId,
tmbId: user.team.tmbId,
source: getBillSourceByAuthType({ shareId, authType }),
response: responseData
});
if (shareId) {
pushResult2Remote({ outLinkUid, shareId, appName: app.name, responseData });
addOutLinkUsage({
pushResult2Remote({ outLinkUid, shareId, responseData });
updateOutLinkUsage({
shareId,
totalPoints
total
});
}
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints
usage: total
});
}
} catch (err) {
} catch (err: any) {
if (stream) {
sseErrRes(res, err);
res.end();

View File

@@ -2,13 +2,13 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { pushGenerateVectorBill } from '@/service/support/wallet/bill/push';
import { connectToDatabase } from '@/service/mongo';
import { authTeamBalance } from '@/service/support/permission/auth/bill';
import { getVectorsByText } from '@fastgpt/service/core/ai/embedding';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
import { getBillSourceByAuthType } from '@fastgpt/global/support/wallet/bill/tools';
import { getVectorModel } from '@/service/core/ai/model';
import { checkTeamAIPoints } from '@/service/support/permission/teamLimit';
type Props = {
input: string | string[];
@@ -34,7 +34,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
authApiKey: true
});
await checkTeamAIPoints(teamId);
await authTeamBalance(teamId);
const { charsLength, vectors } = await getVectorsByText({
input: query,
@@ -55,19 +55,19 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
}
});
const { totalPoints } = pushGenerateVectorUsage({
const { total } = pushGenerateVectorBill({
teamId,
tmbId,
charsLength,
model,
billId,
source: getUsageSourceByAuthType({ authType })
source: getBillSourceByAuthType({ authType })
});
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints: totalPoints
usage: total
});
}
} catch (err) {

View File

@@ -0,0 +1,50 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { withNextCors } from '@fastgpt/service/common/middle/cors';
import { pushReRankBill } from '@/service/support/wallet/bill/push';
import { connectToDatabase } from '@/service/mongo';
import { authTeamBalance } from '@/service/support/permission/auth/bill';
import { PostReRankProps, PostReRankResponse } from '@fastgpt/global/core/ai/api';
import { reRankRecall } from '@/service/core/ai/rerank';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
let { query, inputs } = req.body as PostReRankProps;
try {
await connectToDatabase();
const { teamId, tmbId, apikey } = await authCert({
req,
authApiKey: true
});
await authTeamBalance(teamId);
// max 150 length
inputs = inputs.slice(0, 150);
const result = await reRankRecall({ query, inputs });
const { total } = pushReRankBill({
teamId,
tmbId,
source: 'api',
inputs
});
if (apikey) {
updateApiKeyUsage({
apikey,
usage: total
});
}
jsonRes<PostReRankResponse>(res, {
data: result
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
});

View File

@@ -0,0 +1,201 @@
import React, { useEffect, useMemo, useRef } from 'react';
import * as echarts from 'echarts';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getAppTotalUsage } from '@/web/core/app/api';
import { useQuery } from '@tanstack/react-query';
import dayjs from 'dayjs';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import Loading from '@/components/Loading';
import { Box } from '@chakra-ui/react';
const map = {
blue: {
backgroundColor: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(3, 190, 232, 0.42)' // 0% 处的颜色
},
{
offset: 1,
color: 'rgba(0, 182, 240, 0)'
}
],
global: false // 缺省为 false
},
lineColor: '#36ADEF'
},
deepBlue: {
backgroundColor: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(47, 112, 237, 0.42)' // 0% 处的颜色
},
{
offset: 1,
color: 'rgba(94, 159, 235, 0)'
}
],
global: false
},
lineColor: '#3293EC'
},
purple: {
backgroundColor: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(211, 190, 255, 0.42)' // 0% 处的颜色
},
{
offset: 1,
color: 'rgba(52, 60, 255, 0)'
}
],
global: false // 缺省为 false
},
lineColor: '#8172D8'
},
green: {
backgroundColor: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(4, 209, 148, 0.42)' // 0% 处的颜色
},
{
offset: 1,
color: 'rgba(19, 217, 181, 0)'
}
],
global: false // 缺省为 false
},
lineColor: '#00A9A6',
max: 100
}
};
const TokenUsage = ({ appId }: { appId: string }) => {
const { screenWidth } = useSystemStore();
const Dom = useRef<HTMLDivElement>(null);
const myChart = useRef<echarts.ECharts>();
const { data = [] } = useQuery(['init'], () => getAppTotalUsage({ appId }));
const option = useMemo(
() => ({
xAxis: {
type: 'category',
show: false,
boundaryGap: false,
data: data.map((item) => item.date)
},
yAxis: {
type: 'value',
splitNumber: 3,
min: 0
},
grid: {
show: false,
left: 5,
right: 5,
top: 0,
bottom: 5
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line'
},
formatter: (e: any[]) => {
const data = e[0];
if (!data) return '';
return `
<div>
<div>${dayjs(data.axisValue).format('YYYY/MM/DD')}</div>
<div>${formatStorePrice2Read(e[0]?.value || 0)}元</div>
</div>
`;
}
},
series: [
{
data: data.map((item) => item.total),
type: 'line',
showSymbol: true,
animationDuration: 1000,
animationEasingUpdate: 'linear',
areaStyle: {
color: map['blue'].backgroundColor
},
lineStyle: {
width: '1',
color: map['blue'].lineColor
},
itemStyle: {
width: 1.5,
color: map['blue'].lineColor
},
emphasis: {
// highlight
disabled: true
}
}
]
}),
[data]
);
// init chart
useEffect(() => {
if (!Dom.current || myChart?.current?.getOption()) return;
myChart.current = echarts.init(Dom.current);
myChart.current && myChart.current.setOption(option);
setTimeout(() => {
myChart.current?.resize();
}, 500);
}, []);
// data changed, update
useEffect(() => {
if (!myChart.current || !myChart?.current?.getOption()) return;
myChart.current.setOption(option);
}, [data, option]);
// resize chart
useEffect(() => {
if (!myChart.current || !myChart.current.getOption()) return;
myChart.current.resize();
}, [screenWidth]);
return (
<Box ref={Dom} w={'100%'} flex={'1 0 0'} h={'100%'} position={'relative'}>
<Loading fixed={false} />
</Box>
);
};
export default React.memo(TokenUsage);

View File

@@ -36,6 +36,7 @@ import { useForm } from 'react-hook-form';
import { defaultOutLinkForm } from '@/constants/app';
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d';
import { useRequest } from '@/web/common/hooks/useRequest';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { OutLinkTypeEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -93,7 +94,7 @@ const Share = ({ appId }: { appId: string }) => {
<Thead>
<Tr>
<Th>{t('common.Name')}</Th>
<Th>{t('support.outlink.Usage points')}</Th>
<Th>{t('common.Price used')}</Th>
<Th>{t('core.app.share.Is response quote')}</Th>
{feConfigs?.isPlus && (
<>
@@ -111,11 +112,11 @@ const Share = ({ appId }: { appId: string }) => {
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>
{Math.round(item.usagePoints)}
{formatStorePrice2Read(item.total)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
item.limit && item.limit.credit > -1
? ` / ${item.limit.credit}`
: ` / ${t('common.Unlimited')}`
}`
: ''}
@@ -314,15 +315,15 @@ function EditLinkModal({
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('support.outlink.Max usage points')}
<MyTooltip label={t('support.outlink.Max usage points tip')}>
{t('common.Max credit')}
<MyTooltip label={t('common.Max credit tips' || '')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
{...register('limit.credit', {
min: -1,
max: 10000000,
max: 1000,
valueAsNumber: true,
required: true
})}

View File

@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
import { DragHandleIcon } from '@chakra-ui/icons';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useRouter } from 'next/router';
@@ -13,7 +12,6 @@ import PermissionIconText from '@/components/support/permission/IconText';
import dynamic from 'next/dynamic';
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from './tagsEditModal';
const InfoModal = dynamic(() => import('../InfoModal'));
const AppCard = ({ appId }: { appId: string }) => {
@@ -22,7 +20,6 @@ const AppCard = ({ appId }: { appId: string }) => {
const { toast } = useToast();
const { appDetail } = useAppStore();
const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
content: t('app.Confirm Del App Tip')
@@ -126,17 +123,6 @@ const AppCard = ({ appId }: { appId: string }) => {
>
{t('core.app.navbar.Publish')}
</Button>
{appDetail.isOwner && (
<Button
mr={3}
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<DragHandleIcon w={'16px'} />}
onClick={() => setTeamTagsSet(appDetail)}
>
{t('common.Team Tags Set')}
</Button>
)}
{appDetail.isOwner && (
<Button
size={['sm', 'md']}
@@ -150,13 +136,11 @@ const AppCard = ({ appId }: { appId: string }) => {
</Flex>
</Box>
</Box>
<ConfirmDelModal />
{settingAppInfo && (
<InfoModal defaultApp={settingAppInfo} onClose={() => setSettingAppInfo(undefined)} />
)}
{TeamTagsSet && (
<TagsEditModal appDetail={appDetail} onClose={() => setTeamTagsSet(undefined)} />
)}
</>
);
};

View File

@@ -1,103 +0,0 @@
import React, { useCallback, useState, useEffect } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import { Button, Flex, Box, ModalFooter, ModalBody } from '@chakra-ui/react';
import TagsEdit from '@/components/TagEdit';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { TeamTagsSchema } from '@fastgpt/global/support/user/team/type';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import { useRequest } from '@/web/common/hooks/useRequest';
import { getTeamsTags } from '@/web/support/user/team/api';
const TagsEditModal = ({ appDetail, onClose }: { appDetail?: any; onClose: () => void }) => {
const { t } = useTranslation();
const [teamsTags, setTeamTags] = useState<Array<TeamTagsSchema>>([]);
const [selectedTags, setSelectedTags] = useState(appDetail?.teamTags);
const { toast } = useToast();
const { replaceAppDetail } = useAppStore();
// submit config
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
mutationFn: async () => {
await replaceAppDetail(appDetail._id, {
teamTags: selectedTags
});
},
onSuccess() {
onClose();
toast({
title: t('common.Update Success'),
status: 'success'
});
},
errorToast: t('common.Update Failed')
});
//
// // 点击选择标签
// const clickTag = (tagId :Number) => {
// const index = selectedTags.indexOf(tagId);
// if (index === -1) {
// // 如果 num 不在数组 arr 中,添加它
// setSelectedTags([tagId,...selectedTags])
// } else {
// const _selectedTags = [...selectedTags];
// _selectedTags.splice(index, 1);
// console.log('_selectedTags',_selectedTags);
// // 如果 num 已经在数组 arr 中,移除它
// setSelectedTags(_selectedTags);
// }
// }
useEffect(() => {
// get team tags
getTeamsTags(appDetail?.teamId).then((res: any) => {
setTeamTags(res?.list);
});
}, []);
return (
<MyModal
style={{ width: '900px' }}
isOpen
onClose={onClose}
iconSrc="/imgs/module/ai.svg"
title={'标签管理'}
>
<ModalBody>
{/* <HStack spacing={2}>
{teamsTags.map((item,index) => {
return <Tag
key={index}
size={'md'}
variant='outline'
colorScheme={selectedTags.indexOf(item._id) > -1 ? 'green':'blue' }
onClick={() => clickTag(item._id)}
>
{item.label}
</Tag>
})}
</HStack> */}
<Flex width={'100%'} alignItems={'center'}>
<Box mb={3} mr={3} fontWeight="semibold">
{t('团队标签')}
</Box>
<TagsEdit
defaultValues={selectedTags}
teamsTags={teamsTags}
setSelectedTags={(item: Array<string>) => setSelectedTags(item)}
/>
</Flex>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button isLoading={btnLoading} onClick={(e) => saveSubmitSuccess(e)}>
{t('common.Save')}
</Button>
</ModalFooter>
</ModalBody>
</MyModal>
);
};
export default TagsEditModal;

View File

@@ -8,12 +8,8 @@ import {
Input,
Grid,
useTheme,
Card,
Text,
HStack,
Tag
Card
} from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useEffect } from 'react';
import React, { useCallback } from 'react';
import { Box, Grid, Flex, IconButton, Button, useDisclosure } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
@@ -8,6 +8,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
@@ -23,7 +24,6 @@ const MyApps = () => {
const router = useRouter();
const { userInfo } = useUserStore();
const { myApps, loadMyApps } = useAppStore();
const [teamsTags, setTeamTags] = useState([]);
const { openConfirm, ConfirmModal } = useConfirm({
title: '删除提示',
content: '确认删除该应用所有信息?'
@@ -65,9 +65,11 @@ const MyApps = () => {
<Box letterSpacing={1} fontSize={['20px', '24px']} color={'myGray.900'}>
{t('app.My Apps')}
</Box>
<Button leftIcon={<AddIcon />} variant={'primaryOutline'} onClick={onOpenCreateModal}>
{t('common.New Create')}
</Button>
{userInfo?.team?.canWrite && (
<Button leftIcon={<AddIcon />} variant={'primaryOutline'} onClick={onOpenCreateModal}>
{t('common.New Create')}
</Button>
)}
</Flex>
<Grid
py={[4, 6]}
@@ -169,10 +171,6 @@ const MyApps = () => {
</MyTooltip>
))}
</Grid>
{/* (
<ShareBox></ShareBox>
) */}
{myApps.length === 0 && (
<Flex mt={'35vh'} flexDirection={'column'} alignItems={'center'}>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />

View File

@@ -1,521 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Head from 'next/head';
import { getTeamChatInfo } from '@/web/core/chat/api';
import { useRouter } from 'next/router';
import {
Box,
Flex,
useDisclosure,
Drawer,
DrawerOverlay,
DrawerContent,
useTheme
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useQuery } from '@tanstack/react-query';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SideBar from '@/components/SideBar';
import PageContainer from '@/components/PageContainer';
import { getChatListById } from '@/web/core/chat/api';
import ChatHistorySlider from './components/ChatHistorySlider';
import ChatHeader from './components/ChatHeader';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import { checkChatSupportSelectFileByChatModels } from '@/web/core/chat/utils';
import { useChatStore } from '@/web/core/chat/storeChat';
import { customAlphabet } from 'nanoid';
import { useLoading } from '@/web/common/hooks/useLoading';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
import ChatBox, { type ComponentRef, type StartChatFnProps } from '@/components/ChatBox';
import { streamFetch } from '@/web/common/api/fetch';
import { useTeamShareChatStore } from '@/web/core/chat/storeTeamChat';
import type {
ChatHistoryItemType,
chatAppListSchema,
teamInfoType
} from '@fastgpt/global/core/chat/type.d';
import { chatContentReplaceBlock } from '@fastgpt/global/core/chat/utils';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { POST } from '@/web/common/api/request';
const OutLink = ({
teamId,
appId,
chatId,
authToken
}: {
teamId: string;
appId: string;
chatId: string;
authToken: string;
}) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const theme = useTheme();
const [myApps, setMyApps] = useState<Array<any>>([]);
const { isPc } = useSystemStore();
const ChatBoxRef = useRef<ComponentRef>(null);
const [teamInfo, setTeamInfo] = useState<teamInfoType>();
const { Loading, setIsLoading } = useLoading();
const forbidRefresh = useRef(false);
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const {
histories,
loadHistories,
lastChatAppId,
setLastChatAppId,
lastChatId,
setLastChatId,
pushHistory,
updateHistory,
delOneHistory,
chatData,
setChatData,
delOneHistoryItem,
clearHistories
} = useChatStore();
const {
localUId,
teamShareChatHistory, // abandon
clearLocalHistory // abandon
} = useTeamShareChatStore();
const outLinkUid: string = authToken || localUId;
// 纯网络获取流程
const loadApps = useCallback(async () => {
try {
if (!teamId) {
toast({
status: 'error',
title: t('core.chat.You need to a chat app')
});
return;
}
// 根据teamId 获取教研token以及用户tags然后通过是否为
// 根据获取历史记录列表
const res = await getChatListById({ teamId, authToken });
const { apps = [], teamInfo } = res;
setMyApps(apps);
setTeamInfo(teamInfo);
if (apps.length <= 0) {
toast({
status: 'error',
title: t('core.chat.You need to a chat app')
});
}
//
return null;
} catch (error: any) {
toast({
status: 'warning',
title: error?.message
});
}
}, [outLinkUid, router, t, toast]);
const startChat = useCallback(
async ({ messages, controller, generatingMessage, variables }: StartChatFnProps) => {
console.log('res', 13);
const prompts = messages.slice(-2);
const completionChatId = chatId ? chatId : nanoid();
const { responseText, responseData } = await streamFetch({
data: {
messages: prompts,
variables,
appId,
teamId: teamId,
outLinkUid: outLinkUid,
chatId: completionChatId
},
onMessage: generatingMessage,
abortCtrl: controller
});
const newTitle =
chatContentReplaceBlock(prompts[0].content).slice(0, 20) ||
prompts[1]?.value?.slice(0, 20) ||
t('core.chat.New Chat');
// new chat
if (completionChatId !== chatId) {
const newHistory: ChatHistoryItemType = {
chatId: completionChatId,
updateTime: new Date(),
title: newTitle,
appId,
top: false
};
pushHistory(newHistory);
if (controller.signal.reason !== 'leave') {
forbidRefresh.current = true;
router.replace({
query: {
chatId: completionChatId,
appId,
teamId: teamId,
authToken: authToken
}
});
}
} else {
// update chat
const currentChat = histories.find((item) => item.chatId === chatId);
currentChat &&
updateHistory({
...currentChat,
updateTime: new Date(),
title: newTitle
});
}
// update chat window
setChatData((state) => ({
...state,
title: newTitle,
history: ChatBoxRef.current?.getChatHistories() || state.history
}));
return { responseText, responseData, isNewChat: forbidRefresh.current };
},
[appId, chatId, histories, pushHistory, router, setChatData, updateHistory]
);
const { isFetching } = useQuery(['init', appId, teamId], async () => {
console.log('res', 3);
if (!teamId) {
toast({
status: 'error',
title: t('core.chat.You need to a chat app')
});
return;
}
return teamId && loadApps();
});
useQuery(['loadHistories', appId], () => {
console.log('res', 1);
teamId && appId ? loadHistories({ appId, outLinkUid }) : null;
});
// 初始化聊天框
useQuery(['init', { appId, chatId }], () => {
if (!teamId) {
toast({
status: 'error',
title: t('core.chat.You need to a chat app')
});
return;
}
// pc: redirect to latest model chat
if (!appId && lastChatAppId) {
return router.replace({
query: {
appId: lastChatAppId,
chatId: lastChatId,
teamId: teamId,
authToken: authToken
}
});
}
if (!appId && myApps[0]) {
return router.replace({
query: {
appId: myApps[0]._id,
chatId: lastChatId,
teamId: teamId,
authToken: authToken
}
});
}
if (!appId) {
(async () => {
const { apps = [] } = await getChatListById({ teamId, authToken });
setMyApps(apps);
if (apps.length === 0) {
toast({
status: 'error',
title: t('core.chat.You need to a chat app')
});
} else {
router.replace({
query: {
appId: apps[0]._id,
chatId: lastChatId,
teamId: teamId,
authToken: authToken
}
});
}
})();
return;
}
// store id
appId && setLastChatAppId(appId);
setLastChatId(chatId);
return loadChatInfo({
appId,
chatId,
loading: appId !== chatData.appId
});
});
// get chat app info
const loadChatInfo = useCallback(
async ({
appId,
chatId,
loading = false
}: {
appId: string;
chatId: string;
loading?: boolean;
}) => {
try {
if (!teamId) {
toast({
status: 'error',
title: t('core.chat.You need to a chat app')
});
return;
}
loading && setIsLoading(true);
const res = await getTeamChatInfo({ appId, chatId, outLinkUid });
console.log('res', res);
const history = res.history.map((item) => ({
...item,
status: ChatStatusEnum.finish
}));
setChatData({
...res,
history
});
// have records.
ChatBoxRef.current?.resetHistory(history);
ChatBoxRef.current?.resetVariables(res.variables);
if (res.history.length > 0) {
setTimeout(() => {
ChatBoxRef.current?.scrollToBottom('auto');
}, 500);
}
} catch (e: any) {
// reset all chat tore
setLastChatAppId('');
setLastChatId('');
toast({
title: t('core.chat.Failed to initialize chat'),
status: 'error'
});
if (e?.code === 501) {
//router.replace('/app/list');
} else if (chatId) {
router.replace({
query: {
...router.query,
chatId: ''
}
});
}
}
setIsLoading(false);
return null;
},
[setIsLoading, setChatData, router, setLastChatAppId, setLastChatId, toast]
);
// 监测路由改变
useEffect(() => {
const activeHistory = teamShareChatHistory.filter((item) => !item.delete);
if (!localUId || !teamId || activeHistory.length === 0) return;
(async () => {
try {
await POST('/core/chat/initLocalShareHistoryV464', {
outLinkUid: localUId,
chatIds: teamShareChatHistory.map((item) => item.chatId)
});
clearLocalHistory();
// router.reload();
} catch (error) {
toast({
status: 'warning',
title: t('core.shareChat.Init Error')
});
}
})();
}, [clearLocalHistory, localUId, router, teamShareChatHistory, teamId, t, toast]);
return (
<Flex h={'100%'}>
{/* pc show myself apps */}
<Box borderRight={theme.borders.base} w={'220px'} flexShrink={0}>
<Flex flexDirection={'column'} h={'100%'}>
<Box flex={'1 0 0'} h={0} px={5} py={4} overflow={'overlay'}>
{myApps.map((item) => (
<Flex
key={item._id}
py={2}
px={3}
mb={3}
cursor={'pointer'}
borderRadius={'md'}
alignItems={'center'}
{...(item._id === appId
? {
bg: 'white',
boxShadow: 'md'
}
: {
_hover: {
bg: 'myGray.200'
},
onClick: () => {
router.replace({
query: {
appId: item._id,
teamId: teamId,
authToken: authToken
}
});
}
})}
>
<Avatar src={item.avatar} w={'24px'} />
<Box ml={2} className={'textEllipsis'}>
{item.name}
</Box>
</Flex>
))}
</Box>
</Flex>
</Box>
<PageContainer flex={'1 0 0'} w={0} p={[0, '16px']} position={'relative'}>
<Flex h={'100%'} flexDirection={['column', 'row']} bg={'white'}>
{((children: React.ReactNode) => {
return isPc || !appId ? (
<SideBar>{children}</SideBar>
) : (
<Drawer
isOpen={isOpenSlider}
placement="left"
autoFocus={false}
size={'xs'}
onClose={onCloseSlider}
>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}>{children}</DrawerContent>
</Drawer>
);
})(
<ChatHistorySlider
appId={appId}
appName={chatData.app.name}
appAvatar={chatData.app.avatar}
activeChatId={chatId}
onClose={onCloseSlider}
history={histories.map((item, i) => ({
id: item.chatId,
title: item.title,
customTitle: item.customTitle,
top: item.top
}))}
onChangeChat={(chatId) => {
router.replace({
query: {
chatId: chatId || '',
appId,
teamId: teamId,
authToken: authToken
}
});
if (!isPc) {
onCloseSlider();
}
}}
onDelHistory={(e) => delOneHistory({ ...e, appId })}
onClearHistory={() => {
clearHistories({ appId });
router.replace({
query: {
appId,
teamId: teamId,
authToken: authToken
}
});
}}
onSetHistoryTop={(e) => {
updateHistory({ ...e, appId });
}}
onSetCustomTitle={async (e) => {
updateHistory({
appId,
chatId: e.chatId,
title: e.title,
customTitle: e.title
});
}}
/>
)}
{/* chat container */}
<Flex
position={'relative'}
h={[0, '100%']}
w={['100%', 0]}
flex={'1 0 0'}
flexDirection={'column'}
>
{/* header */}
<ChatHeader
appAvatar={chatData.app.avatar}
appName={chatData.app.name}
history={chatData.history}
showHistory={true}
onOpenSlider={onOpenSlider}
/>
{/* chat box */}
<Box flex={1}>
<ChatBox
active={!!chatData.app.name}
ref={ChatBoxRef}
appAvatar={chatData.app.avatar}
userAvatar={chatData.userAvatar}
userGuideModule={chatData.app?.userGuideModule}
showFileSelector={checkChatSupportSelectFileByChatModels(chatData.app.chatModels)}
feedbackType={'user'}
onUpdateVariable={(e) => {}}
onStartChat={startChat}
onDelMessage={(e) =>
delOneHistoryItem({ ...e, appId: chatData.appId, chatId, outLinkUid })
}
appId={chatData.appId}
chatId={chatId}
outLinkUid={outLinkUid}
/>
</Box>
</Flex>
</Flex>
</PageContainer>
</Flex>
);
};
export async function getServerSideProps(context: any) {
const teamId = context?.query?.teamId || '';
const appId = context?.query?.appId || '';
const chatId = context?.query?.chatId || '';
const authToken: string = context?.query?.authToken || '';
return {
props: {
teamId,
appId,
chatId,
authToken,
...(await serviceSideProps(context))
}
};
}
export default OutLink;

View File

@@ -1,5 +1,6 @@
import React, { useContext, useCallback, createContext, useState, useMemo, useEffect } from 'react';
import { formatModelPrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
@@ -33,7 +34,7 @@ type useImportStoreType = {
totalChunkChars: number;
totalChunks: number;
chunkSize: number;
predictPoints: number;
predictPrice: number;
priceTip: string;
uploadRate: number;
splitSources2Chunks: () => void;
@@ -53,7 +54,7 @@ const StateContext = createContext<useImportStoreType>({
totalChunkChars: 0,
totalChunks: 0,
chunkSize: 0,
predictPoints: 0,
predictPrice: 0,
priceTip: '',
uploadRate: 50,
splitSources2Chunks: () => {}
@@ -104,9 +105,10 @@ const Provider = ({
chunkSize: embeddingChunkSize,
showChunkInput: true,
showPromptInput: false,
charsPointsPrice: vectorModel.charsPointsPrice,
inputPrice: vectorModel.inputPrice,
outputPrice: 0,
priceTip: t('core.dataset.import.Embedding Estimated Price Tips', {
price: vectorModel.charsPointsPrice
price: vectorModel.inputPrice
}),
uploadRate: 150
},
@@ -118,9 +120,10 @@ const Provider = ({
chunkSize: agentModel.maxContext * 0.55 || 6000,
showChunkInput: false,
showPromptInput: true,
charsPointsPrice: agentModel.charsPointsPrice,
inputPrice: agentModel.inputPrice,
outputPrice: agentModel.outputPrice,
priceTip: t('core.dataset.import.QA Estimated Price Tips', {
price: agentModel?.charsPointsPrice
price: agentModel?.inputPrice
}),
uploadRate: 30
}
@@ -148,12 +151,15 @@ const Provider = ({
() => sources.reduce((sum, file) => sum + file.chunkChars, 0),
[sources]
);
const predictPoints = useMemo(() => {
const predictPrice = useMemo(() => {
if (mode === TrainingModeEnum.qa) {
return +(((totalChunkChars * 1.5) / 1000) * agentModel.charsPointsPrice).toFixed(2);
const inputTotal = totalChunkChars * selectModelStaticParam.inputPrice;
const outputTotal = totalChunkChars * 0.5 * selectModelStaticParam.inputPrice;
return formatModelPrice2Read(inputTotal + outputTotal);
}
return +((totalChunkChars / 1000) * vectorModel.charsPointsPrice).toFixed(2);
}, [agentModel.charsPointsPrice, mode, totalChunkChars, vectorModel.charsPointsPrice]);
return formatModelPrice2Read(totalChunkChars * selectModelStaticParam.inputPrice);
}, [mode, selectModelStaticParam.inputPrice, totalChunkChars]);
const totalChunks = useMemo(
() => sources.reduce((sum, file) => sum + file.chunks.length, 0),
[sources]
@@ -172,8 +178,7 @@ const Provider = ({
return {
...file,
chunkChars: chars,
chunks: chunks.map((chunk, i) => ({
chunkIndex: i,
chunks: chunks.map((chunk) => ({
q: chunk,
a: ''
}))
@@ -193,7 +198,7 @@ const Provider = ({
totalChunkChars,
totalChunks,
chunkSize,
predictPoints,
predictPrice,
splitSources2Chunks
};
return <StateContext.Provider value={value}>{children}</StateContext.Provider>;

View File

@@ -46,7 +46,7 @@ function DataProcess({
maxChunkSize,
totalChunkChars,
totalChunks,
predictPoints,
predictPrice,
showRePreview,
splitSources2Chunks,
priceTip
@@ -275,7 +275,7 @@ function DataProcess({
{feConfigs?.show_pay && (
<MyTooltip label={priceTip}>
<Tag colorSchema={'gray'} py={'6px'} borderRadius={'md'} px={3}>
{t('core.dataset.import.Estimated points', { points: predictPoints })}
{t('core.dataset.import.Estimated Price', { amount: predictPrice, unit: '元' })}
</Tag>
</MyTooltip>
)}

View File

@@ -16,7 +16,7 @@ import { useImportStore, type FormType } from '../Provider';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest } from '@/web/common/hooks/useRequest';
import { postCreateTrainingUsage } from '@/web/support/wallet/usage/api';
import { postCreateTrainingBill } from '@/web/support/wallet/bill/api';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { chunksUpload, fileCollectionCreate } from '@/web/core/dataset/utils';
import { ImportSourceItemType } from '@/web/core/dataset/type';
@@ -54,7 +54,7 @@ const Upload = ({ showPreviewChunks }: { showPreviewChunks: boolean }) => {
// Batch create collection and upload chunks
for await (const item of uploadList) {
const billId = await postCreateTrainingUsage({
const billId = await postCreateTrainingBill({
name: item.sourceName,
datasetId: datasetDetail._id
});

View File

@@ -19,6 +19,7 @@ import { useRequest } from '@/web/common/hooks/useRequest';
import { countPromptTokens } from '@fastgpt/global/common/string/tiktoken';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { getDefaultIndex } from '@fastgpt/global/core/dataset/utils';
import { DatasetDataIndexTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { DatasetDataIndexItemType } from '@fastgpt/global/core/dataset/type';
import SideTabs from '@/components/SideTabs';
import DeleteIcon from '@fastgpt/web/components/common/Icon/delete';
@@ -161,10 +162,9 @@ const InputDataModal = ({
q: e.q,
a: e.a,
// remove dataId
indexes: e.indexes.map((index) => ({
...index,
dataId: undefined
}))
indexes: e.indexes.map((index) =>
index.defaultIndex ? getDefaultIndex({ q: e.q, a: e.a }) : index
)
});
return {
@@ -195,7 +195,7 @@ const InputDataModal = ({
id: dataId,
...e,
indexes: e.indexes.map((index) =>
index.defaultIndex ? getDefaultIndex({ q: e.q, a: e.a, dataId: index.dataId }) : index
index.defaultIndex ? getDefaultIndex({ q: e.q, a: e.a }) : index
)
});
@@ -278,7 +278,7 @@ const InputDataModal = ({
bg={i % 2 !== 0 ? 'myWhite.400' : ''}
_hover={{
'& .delete': {
display: index.defaultIndex ? 'none' : 'block'
display: index.defaultIndex && indexes.length === 1 ? 'none' : 'block'
}
}}
>
@@ -331,6 +331,7 @@ const InputDataModal = ({
onClick={() =>
appendIndexes({
defaultIndex: false,
type: DatasetDataIndexTypeEnum.chunk,
text: '',
dataId: `${Date.now()}`
})
@@ -382,47 +383,45 @@ const InputTab = ({
const [inputType, setInputType] = useState(InputTypeEnum.q);
return (
<Flex flexDirection={'column'} h={'100%'}>
<Box>
<RowTabs
list={[
{
label: (
<Flex alignItems={'center'}>
<Box as="span" color={'red.600'}>
*
</Box>
{t('core.dataset.data.Main Content')}
<MyTooltip label={t('core.dataset.data.Data Content Tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
),
value: InputTypeEnum.q
},
{
label: (
<Flex alignItems={'center'}>
{t('core.dataset.data.Auxiliary Data')}
<MyTooltip label={t('core.dataset.data.Auxiliary Data Tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
),
value: InputTypeEnum.a
}
]}
value={inputType}
onChange={(e) => setInputType(e as InputTypeEnum)}
/>
</Box>
<Box>
<RowTabs
list={[
{
label: (
<Flex alignItems={'center'}>
<Box as="span" color={'red.600'}>
*
</Box>
{t('core.dataset.data.Main Content')}
<MyTooltip label={t('core.dataset.data.Data Content Tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
),
value: InputTypeEnum.q
},
{
label: (
<Flex alignItems={'center'}>
{t('core.dataset.data.Auxiliary Data')}
<MyTooltip label={t('core.dataset.data.Auxiliary Data Tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
),
value: InputTypeEnum.a
}
]}
value={inputType}
onChange={(e) => setInputType(e as InputTypeEnum)}
/>
<Box mt={3} flex={'1 0 0'}>
<Box mt={3}>
{inputType === InputTypeEnum.q && (
<Textarea
placeholder={t('core.dataset.data.Data Content Placeholder', { maxToken })}
maxLength={maxToken}
h={'100%'}
rows={isPc ? 24 : 12}
bg={'myWhite.400'}
{...register(`q`, {
required: true
@@ -434,7 +433,6 @@ const InputTab = ({
placeholder={t('core.dataset.data.Auxiliary Data Placeholder', {
maxToken: maxToken * 1.5
})}
h={'100%'}
bg={'myWhite.400'}
rows={isPc ? 24 : 12}
maxLength={maxToken * 1.5}
@@ -442,6 +440,6 @@ const InputTab = ({
/>
)}
</Box>
</Flex>
</Box>
);
};

View File

@@ -27,7 +27,6 @@ import {
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useRequest } from '@/web/common/hooks/useRequest';
import DatasetTypeTag from '@/components/core/dataset/DatasetTypeTag';
import Head from 'next/head';
const DataCard = dynamic(() => import('./components/DataCard'));
const Test = dynamic(() => import('./components/Test'));
@@ -146,9 +145,7 @@ const Detail = ({ datasetId, currentTab }: { datasetId: string; currentTab: `${T
return (
<>
<Head>
<title>{datasetDetail?.name}</title>
</Head>
<Script src="/js/pdf.js" strategy="lazyOnload"></Script>
<PageContainer>
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
{isPc ? (

View File

@@ -7,111 +7,92 @@ import {
NumberIncrementStepper,
NumberInputField,
NumberInputStepper,
Button
Button,
useDisclosure,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import { TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
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 { useRequest } from '@/web/common/hooks/useRequest';
import {
posCheckTeamDatasetSizeSub,
postUpdateTeamDatasetSizeSub,
putTeamDatasetSubStatus
} from '@/web/support/wallet/sub/api';
import { SubDatasetSizePreviewCheckResponse } from '@fastgpt/global/support/wallet/sub/api.d';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { getWxPayQRCode } from '@/web/support/wallet/bill/api';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyModal from '@/components/MyModal';
const ExtraPlan = () => {
const ExtraPlan = ({ extraDatasetSize }: { extraDatasetSize?: TeamSubSchema }) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const { subPlans } = useSystemStore();
const [loading, setLoading] = useState(false);
const [qrPayData, setQRPayData] = useState<QRPayProps>();
// extra dataset
const extraDatasetPrice = subPlans?.extraDatasetSize?.price || 0;
const { register: registerDatasetSize, handleSubmit: handleSubmitDatasetSize } = useForm({
defaultValues: {
datasetSize: 0,
month: 1
}
});
const onclickBuyDatasetSize = useCallback(
async ({ datasetSize, month }: { datasetSize: number; month: number }) => {
try {
const datasetSizePayAmount = datasetSize * month * extraDatasetPrice;
if (datasetSizePayAmount === 0) {
return toast({
status: 'warning',
title: '购买数量不能为0'
});
}
setLoading(true);
const [datasetSize, setDatasetSize] = useState(0);
const [isRenew, setIsRenew] = useState('false');
const router = useRouter();
const { userInfo } = useUserStore();
const res = await getWxPayQRCode({
type: BillTypeEnum.extraDatasetSub,
month,
extraDatasetSize: datasetSize
});
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
});
} catch (err) {
toast({
title: getErrText(err),
status: 'error'
});
}
setLoading(false);
const [confirmPayExtraDatasetSizeData, setConfirmPayExtraDatasetSizeData] =
useState<SubDatasetSizePreviewCheckResponse>();
useEffect(() => {
setDatasetSize((extraDatasetSize?.nextExtraDatasetSize || 0) / 1000);
setIsRenew(extraDatasetSize?.status === SubStatusEnum.active ? 'true' : 'false');
}, [extraDatasetSize]);
const { mutate: onUpdateExtraDatasetSizeStatus } = useRequest({
mutationFn: (e: 'true' | 'false') => {
setIsRenew(e);
return putTeamDatasetSubStatus({
status: subSelectMap[e],
type: SubTypeEnum.extraDatasetSize
});
},
[extraDatasetPrice, toast]
);
// extra ai points
const extraPointsPrice = subPlans?.extraPoints?.price || 0;
const { register: registerExtraPoints, handleSubmit: handleSubmitExtraPoints } = useForm({
defaultValues: {
points: 0,
month: 1
}
successToast: t('common.Update success'),
errorToast: t('common.error.Update error')
});
const onclickBuyExtraPoints = useCallback(
async ({ points, month }: { points: number; month: number }) => {
try {
const payAmount = points * month * extraPointsPrice;
if (payAmount === 0) {
return toast({
status: 'warning',
title: '购买数量不能为0'
});
}
setLoading(true);
const res = await getWxPayQRCode({
type: BillTypeEnum.extraPoints,
month,
extraPoints: points
});
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
});
} catch (err) {
toast({
title: getErrText(err),
status: 'error'
});
}
setLoading(false);
},
[extraPointsPrice, toast]
const { mutate: onClickUpdateExtraDatasetPlan, isLoading: isPayingExtraDatasetSize } = 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) {
onClickUpdateExtraDatasetPlan('');
return;
} else {
setConfirmPayExtraDatasetSizeData(res);
}
},
errorToast: t('common.error.Update error')
});
return (
<Flex
@@ -120,13 +101,13 @@ const ExtraPlan = () => {
alignItems={'center'}
position={'relative'}
>
<Box id={'extra-plan'} fontWeight={'bold'} fontSize={['24px', '36px']}>
<Box fontWeight={'bold'} fontSize={['24px', '36px']}>
{t('support.wallet.subscription.Extra plan')}
</Box>
<Box mt={8} mb={10} color={'myGray.500'} fontSize={'lg'}>
{t('support.wallet.subscription.Extra plan tip')}
</Box>
<Grid mt={8} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5} w={['100%', 'auto']}>
<Grid mt={8} gridTemplateColumns={['1fr', '1fr']}>
<Box
bg={'rgba(255, 255, 255, 0.90)'}
px={'32px'}
@@ -135,60 +116,78 @@ const ExtraPlan = () => {
borderWidth={'1px'}
borderColor={'myGray.150'}
boxShadow={'1.5'}
w={['100%', '500px']}
>
<Flex borderBottomWidth={'1px'} borderBottomColor={'myGray.200'}>
<Flex w={['100%', '500px']} borderBottomWidth={'1px'} borderBottomColor={'myGray.200'}>
<Box flex={'1 0 0'}>
<Box fontSize={'xl'} color={'primary.600'}>
{t('support.wallet.subscription.Extra dataset size')}
</Box>
<Box mt={3} fontSize={['28px', '32px']} fontWeight={'bold'}>
{extraDatasetPrice}/1000{' '}
<Box mt={3} fontSize={['32px', '38px']} fontWeight={'bold'}>
{extraDatasetPrice}/1k{' '}
<Box ml={1} as={'span'} fontSize={'lg'} color={'myGray.600'} fontWeight={'normal'}>
/{t('common.month')}
</Box>
</Box>
</Box>
<MyIcon
display={['none', 'display']}
mt={'-30px'}
transform={'translateX(20px)'}
name={'support/bill/extraDatasetsize'}
transform={'translate(20px,-20px)'}
name={'support/pay/extraDatasetsize'}
fill={'none'}
/>
</Flex>
<Box>
<Flex mt={4}>
<MyIcon mr={2} name={'support/bill/shoppingCart'} w={'16px'} color={'primary.600'} />
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={['0 0 100px', '0 0 200px']}>
{t('support.wallet.subscription.Month amount')}
<Box flex={'0 0 200px'}>
{t('support.wallet.subscription.Current dataset store')}:{' '}
</Box>
<Flex alignItems={'center'} mt={1} w={'180px'} position={'relative'}>
<NumberInput size={'sm'} flex={1} step={1} min={1} max={12} position={'relative'}>
<NumberInputField
pr={'30px'}
{...registerDatasetSize('month', {
required: true,
min: 1,
max: 12,
valueAsNumber: true
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<Box position={'absolute'} right={'20px'} color={'myGray.500'} fontSize={'xs'}>
{t('common.month')}
<Box fontWeight={'bold'} flex={1}>
{extraDatasetSize?.currentExtraDatasetSize || 0}
{t('core.dataset.data.unit')}
</Box>
</Flex>
{extraDatasetSize?.nextExtraDatasetSize !== undefined && (
<Flex mt={4}>
<Box flex={'0 0 200px'}>
{t('support.wallet.subscription.Next sub dataset size')}:
</Box>
<Box fontWeight={'bold'} flex={1}>
{extraDatasetSize?.nextExtraDatasetSize || 0}
{t('core.dataset.data.unit')}
</Box>
</Flex>
)}
{!!extraDatasetSize?.startTime && (
<Flex mt={3}>
<Box flex={'0 0 200px'}>: </Box>
<Box>{formatTime2YMDHM(extraDatasetSize?.startTime)}</Box>
</Flex>
)}
{!!extraDatasetSize?.expiredTime && (
<Flex mt={3}>
<Box flex={'0 0 200px'}>: </Box>
<Box>{formatTime2YMDHM(extraDatasetSize?.expiredTime)}</Box>
</Flex>
)}
<Flex mt={3} alignItems={'center'}>
<Box flex={'0 0 200px'}>: </Box>
<MySelect
value={isRenew}
size={'sm'}
w={'180px'}
bg={'myGray.50'}
boxShadow={'none'}
list={[
{ label: '自动续费', value: 'true' },
{ label: '不自动续费', value: 'false' }
]}
onchange={(e) => {
if (!extraDatasetSize) return;
onUpdateExtraDatasetSizeStatus(e);
}}
/>
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={['0 0 100px', '0 0 200px']}>
<Box flex={'0 0 200px'}>
{t('support.wallet.subscription.Update extra dataset size')}
</Box>
<Flex alignItems={'center'} mt={1} w={'180px'} position={'relative'}>
@@ -198,18 +197,13 @@ const ExtraPlan = () => {
min={0}
max={10000}
step={1}
value={datasetSize}
position={'relative'}
onChange={(e) => {
setDatasetSize(Number(e));
}}
>
<NumberInputField
pr={'30px'}
{...registerDatasetSize('datasetSize', {
required: true,
min: 0,
max: 10000,
valueAsNumber: true
})}
step={1}
/>
<NumberInputField pr={'30px'} value={datasetSize} step={1} min={0} max={10000} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
@@ -221,124 +215,88 @@ const ExtraPlan = () => {
</Flex>
</Flex>
<Button
isDisabled={datasetSize * 1000 === extraDatasetSize?.nextExtraDatasetSize}
mt={6}
w={'100%'}
variant={'primaryGhost'}
isLoading={loading}
onClick={handleSubmitDatasetSize(onclickBuyDatasetSize)}
isLoading={isPayingExtraDatasetSize || isFetchingPreviewCheck}
onClick={onClickPreviewCheck}
>
{t('support.wallet.Buy')}
</Button>
</Box>
</Box>
{/* points */}
<Box
bg={'rgba(255, 255, 255, 0.90)'}
w={['100%', '500px']}
px={'32px'}
py={'24px'}
borderRadius={'2xl'}
borderWidth={'1px'}
borderColor={'myGray.150'}
boxShadow={'1.5'}
>
<Flex borderBottomWidth={'1px'} borderBottomColor={'myGray.200'}>
<Box flex={'1 0 0'}>
<Box fontSize={'xl'} color={'primary.600'}>
{t('support.wallet.subscription.Extra ai points')}
</Box>
<Box mt={3} fontSize={['28px', '32px']} fontWeight={'bold'}>
{extraPointsPrice}/1000{' '}
<Box ml={1} as={'span'} fontSize={'lg'} color={'myGray.600'} fontWeight={'normal'}>
/{t('common.month')}
</Box>
</Box>
</Box>
<MyIcon
display={['none', 'display']}
mt={'-30px'}
transform={'translateX(20px)'}
name={'support/bill/extraPoints'}
fill={'none'}
/>
</Flex>
<Box>
<Flex mt={4}>
<MyIcon mr={2} name={'support/bill/shoppingCart'} w={'16px'} color={'primary.600'} />
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={['0 0 100px', '0 0 200px']}>
{t('support.wallet.subscription.Month amount')}
</Box>
<Flex alignItems={'center'} mt={1} w={'180px'} position={'relative'}>
<NumberInput size={'sm'} flex={1} step={1} min={1} max={12} position={'relative'}>
<NumberInputField
pr={'30px'}
{...registerExtraPoints('month', {
required: true,
min: 1,
max: 12,
valueAsNumber: true
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<Box position={'absolute'} right={'20px'} color={'myGray.500'} fontSize={'xs'}>
{t('common.month')}
</Box>
</Flex>
</Flex>
<Flex mt={4} alignItems={'center'}>
<Box flex={['0 0 100px', '0 0 200px']}>
{t('support.wallet.subscription.Update extra ai points')}
</Box>
<Flex alignItems={'center'} mt={1} w={'180px'} position={'relative'}>
<NumberInput
size={'sm'}
flex={1}
min={0}
max={10000}
step={1}
position={'relative'}
>
<NumberInputField
pr={'30px'}
step={1}
{...registerExtraPoints('points', {
required: true,
min: 0,
max: 10000,
valueAsNumber: true
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<Box position={'absolute'} right={'20px'} color={'myGray.500'} fontSize={'xs'}>
000
</Box>
</Flex>
</Flex>
<Button
mt={6}
w={'100%'}
variant={'primaryGhost'}
isLoading={loading}
onClick={handleSubmitExtraPoints(onclickBuyExtraPoints)}
>
{t('support.wallet.Buy')}
{t('common.change')}
</Button>
</Box>
</Box>
</Grid>
{!!qrPayData && <QRCodePayModal {...qrPayData} />}
{/* extra dataset size modal */}
{!!confirmPayExtraDatasetSizeData && (
<MyModal
isOpen
onClose={() => setConfirmPayExtraDatasetSizeData(undefined)}
title={t('support.wallet.Confirm pay')}
iconSrc="common/confirm/rightTip"
>
<ModalBody px={8} py={5}>
<Flex>
<Box flex={'0 0 120px'} color={'myGray.600'}>
</Box>
<Box>{extraDatasetSize?.currentExtraDatasetSize || 0}</Box>
</Flex>
<Flex mt={4}>
<Box flex={'0 0 120px'} color={'myGray.600'}>
</Box>
<Box>{confirmPayExtraDatasetSizeData.newSubSize}</Box>
</Flex>
<Flex mt={4}>
<Box flex={'0 0 120px'} color={'myGray.600'}>
</Box>
<Box>{formatStorePrice2Read(confirmPayExtraDatasetSizeData.newPlanPrice)}</Box>
</Flex>
<Flex mt={4}>
<Box flex={'0 0 120px'} color={'myGray.600'}>
</Box>
<Box>30</Box>
</Flex>
{/* <Flex>
<Box flex={'0 0 120px'}>账号余额:</Box>
<Box>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}元</Box>
</Flex> */}
</ModalBody>
<ModalFooter mx={8} px={0} borderTopWidth={'1px'} borderTopColor={'myGray.200'}>
<Box color={'myGray.600'}></Box>
{confirmPayExtraDatasetSizeData.balanceEnough ? (
<>
<Box flex={'1 0 0'}>
{formatStorePrice2Read(userInfo?.team?.balance).toFixed(2)}
</Box>
<Button
isLoading={isPayingExtraDatasetSize}
onClick={() => onClickUpdateExtraDatasetPlan('')}
>
{formatStorePrice2Read(confirmPayExtraDatasetSizeData.payPrice).toFixed(2)}
</Button>
</>
) : (
<>
<Box color={'red.600'} flex={'1 0 0'}>
</Box>
<Button
isLoading={isPayingExtraDatasetSize}
onClick={() => router.push('/account')}
>
</Button>
</>
)}
</ModalFooter>
</MyModal>
)}
</Flex>
);
};

View File

@@ -4,40 +4,7 @@ import { useTranslation } from 'next-i18next';
const FAQ = () => {
const { t } = useTranslation();
const faqs = [
{
title: '订阅套餐会自动续费么?',
desc: '当前套餐过期后,系统会自动根据“未来套餐”进行续费,系统会尝试从账户余额进行扣费,如果您需要自动续费,请在账户余额中预留额度。'
},
{
title: '能否切换订阅套餐?',
desc: '当前套餐价格大于新套餐时,无法立即切换,将会在当前套餐过期后以“续费”形式进行切换。\n当前套餐价格小于新套餐时系统会自动计算当前套餐剩余余额您可支付差价进行套餐切换。'
},
{
title: '什么是AI积分',
desc: '每次调用AI模型时都会消耗一定的AI积分。具体的计算标准可参考上方的“AI 积分计算标准”。'
},
{
title: 'AI积分会过期么',
desc: '会过期。当前套餐过期后AI积分将会清空并更新为新套餐的AI积分。年度套餐的AI积分时长为1年而不是每个月。'
},
{
title: '知识库索引怎么计算?',
desc: '知识库索引是系统存储的最小单位。通常每条知识库数据对应一条索引,但也会有多条索引的情况。你可以在知识库数据的编辑面板,查看该数据的索引数量和具体内容。'
},
{
title: '额外资源包可以叠加么?',
desc: '可以的。每次购买的资源包都是独立的在其有效期内将会叠加使用。AI积分会优先扣除最先过期的资源包。'
},
{
title: '知识库索引超出会删除么?',
desc: '不会,知识库索引超出时,仅无法插入新的知识库索引。'
},
{
title: '免费版数据会清除么?',
desc: '免费版用户15天无使用记录后会自动清除所有知识库内容。'
}
];
const faqs = [{ title: '怎么付费', describe: '2222' }];
return (
<Flex
@@ -51,25 +18,22 @@ const FAQ = () => {
{t('support.wallet.subscription.FAQ')}
</Box>
<Grid mt={4} gridTemplateColumns={['1fr', '1fr 1fr']} gap={4} w={'100%'}>
{faqs.map((item, i) => (
<Box
key={i}
py={4}
px={5}
borderRadius={'lg'}
borderWidth={'1px'}
borderColor={'myGray.150'}
bg={'rgba(255,255,255,0.9)'}
_hover={{
borderColor: 'primary.300'
}}
>
<Box fontWeight={'bold'}>{item.title}</Box>
<Box fontSize={'sm'} color={'myGray.600'} whiteSpace={'pre-wrap'}>
{item.desc}
</Box>
<Box
py={2}
px={4}
borderRadius={'lg'}
borderWidth={'1px'}
borderColor={'myGray.150'}
bg={'rgba(255,255,255,0.9)'}
_hover={{
borderColor: 'primary.300'
}}
>
<Box fontSize={'lg'} fontWeight={'500'}>
</Box>
))}
<Box color={'myGray.500'}>2222</Box>
</Box>
</Grid>
</Flex>
);

View File

@@ -15,7 +15,7 @@ const Points = () => {
alignItems={'center'}
position={'relative'}
>
<Box id="point-card" fontWeight={'bold'} fontSize={['24px', '36px']}>
<Box fontWeight={'bold'} fontSize={['24px', '36px']}>
{t('support.wallet.subscription.Ai points')}
</Box>
<Grid gap={6} mt={['30px', '48px']} w={'100%'}>
@@ -42,7 +42,7 @@ const Points = () => {
{llmModelList?.map((item, i) => (
<Flex key={item.model} py={4} bg={i % 2 !== 0 ? 'myGray.50' : ''}>
<Box flex={'1 0 0'}>{item.name}</Box>
<Box flex={'1 0 0'}>{item.charsPointsPrice} / 1000</Box>
<Box flex={'1 0 0'}>5 / 1000</Box>
</Flex>
))}
</Box>
@@ -67,7 +67,7 @@ const Points = () => {
{vectorModelList?.map((item, i) => (
<Flex key={item.model} py={4} bg={i % 2 !== 0 ? 'myGray.50' : ''}>
<Box flex={'1 0 0'}>{item.name}</Box>
<Box flex={'1 0 0'}>{item.charsPointsPrice} / 1000</Box>
<Box flex={'1 0 0'}>5 / 1000</Box>
</Flex>
))}
</Box>
@@ -89,7 +89,7 @@ const Points = () => {
{audioSpeechModelList?.map((item, i) => (
<Flex key={item.model} py={4} bg={i % 2 !== 0 ? 'myGray.50' : ''}>
<Box flex={'1 0 0'}>{item.name}</Box>
<Box flex={'1 0 0'}>{item.charsPointsPrice} / 1000</Box>
<Box flex={'1 0 0'}>5 / 1000</Box>
</Flex>
))}
</Box>
@@ -110,7 +110,7 @@ const Points = () => {
<Box flex={4} textAlign={'center'} h={'100%'}>
<Flex py={4}>
<Box flex={'1 0 0'}>{whisperModel?.name}</Box>
<Box flex={'1 0 0'}>{whisperModel?.charsPointsPrice} / </Box>
<Box flex={'1 0 0'}>{whisperModel?.inputPrice} / </Box>
</Flex>
</Box>
</Box>

View File

@@ -1,8 +1,9 @@
import React, { useMemo, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Button, Flex, Grid, ModalBody, ModalFooter } from '@chakra-ui/react';
import { Box, Button, Flex, Grid } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/wallet/sub/constants';
import { useUserStore } from '@/web/support/user/useUserStore';
import { postCheckStandardSub, postUpdateStandardSub } from '@/web/support/wallet/sub/api';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
@@ -10,21 +11,9 @@ import { StandardSubPlanParams } from '@fastgpt/global/support/wallet/sub/api';
import { useRequest } from '@/web/common/hooks/useRequest';
import { StandardSubPlanUpdateResponse } from '@fastgpt/global/support/wallet/sub/api.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import MyModal from '@/components/MyModal';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
import { getWxPayQRCode } from '@/web/support/wallet/bill/api';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
type ConfirmPayModalProps = {
teamBalance: number;
totalPrice: number;
payPrice: number;
planProps: StandardSubPlanParams;
};
const Standard = ({
standardPlan,
@@ -36,7 +25,7 @@ const Standard = ({
const { t } = useTranslation();
const { subPlans, feConfigs } = useSystemStore();
const { toast } = useToast();
const [confirmPayData, setConfirmPayData] = useState<ConfirmPayModalProps>();
const { ConfirmModal, openConfirm } = useConfirm({});
const [selectSubMode, setSelectSubMode] = useState<`${SubModeEnum}`>(SubModeEnum.month);
@@ -52,12 +41,12 @@ const Standard = ({
maxDatasetAmount: value.maxDatasetAmount,
chatHistoryStoreDuration: value.chatHistoryStoreDuration,
maxDatasetSize: value.maxDatasetSize,
permissionCustomApiKey: value.permissionCustomApiKey,
permissionCustomCopyright: value.permissionCustomCopyright,
customApiKey: value.customApiKey,
customCopyright: value.customCopyright,
trainingWeight: value.trainingWeight,
permissionReRank: value.permissionReRank,
reRankWeight: value.reRankWeight,
totalPoints: value.totalPoints * (selectSubMode === SubModeEnum.month ? 1 : 12),
permissionWebsiteSync: value.permissionWebsiteSync
websiteSyncInterval: value.websiteSyncInterval
};
})
: [];
@@ -75,21 +64,41 @@ const Standard = ({
const { mutate: onclickPreCheckStandPlan, isLoading: isCheckingStandardPlan } = useRequest({
mutationFn: (data: StandardSubPlanParams) => postCheckStandardSub(data),
onSuccess(res: StandardSubPlanUpdateResponse) {
if (!res.balanceEnough) {
return toast({
status: 'warning',
title: t('support.wallet.Balance not enough tip')
});
}
if (res.payPrice === undefined) {
onclickUpdateStandardPlan({
level: res.nextSubLevel,
mode: res.nextMode
});
} else if (res.payPrice > 0) {
openConfirm(
() =>
onclickUpdateStandardPlan({
level: res.nextSubLevel,
mode: res.nextMode
}),
undefined,
t('support.wallet.subscription.Standard plan pay confirm', {
payPrice: formatStorePrice2Read(res.payPrice).toFixed(2)
})
)();
} else {
setConfirmPayData({
teamBalance: res.teamBalance,
totalPrice: res.planPrice,
payPrice: res.payPrice,
planProps: {
level: res.nextSubLevel,
mode: res.nextMode
}
});
openConfirm(
() =>
onclickUpdateStandardPlan({
level: res.nextSubLevel,
mode: res.nextMode
}),
undefined,
t('support.wallet.subscription.Refund plan and pay confirm', {
amount: formatStorePrice2Read(Math.abs(res.payPrice)).toFixed(2)
})
)();
}
}
});
@@ -129,114 +138,148 @@ const Standard = ({
gap={[4, 6, 8]}
w={'100%'}
>
{standardSubList.map((item) => {
const isCurrentPlan =
item.level === standardPlan?.currentSubLevel &&
selectSubMode === standardPlan?.currentMode;
return (
<Box
key={item.level}
flex={'1 0 0'}
bg={'rgba(255, 255, 255, 0.90)'}
p={'28px'}
borderRadius={'2xl'}
borderWidth={'1.5px'}
boxShadow={'1.5'}
{...(isCurrentPlan
? {
borderColor: 'primary.600'
}
: {
borderColor: 'myGray.150'
})}
>
<Box fontSize={'lg'} fontWeight={'500'}>
{t(item.label)}
</Box>
<Box fontSize={['32px', '42px']} fontWeight={'bold'}>
{item.price}
</Box>
<Box color={'myGray.500'} h={'40px'} fontSize={'xs'}>
{t(item.desc, { title: feConfigs?.systemTitle })}
</Box>
{(() => {
if (
item.level === StandardSubLevelEnum.free &&
selectSubMode === SubModeEnum.year
) {
return (
<Button isDisabled mt={4} mb={6} w={'100%'} variant={'solid'}>
{t('support.wallet.subscription.Nonsupport')}
</Button>
);
}
if (
item.level === standardPlan?.nextSubLevel &&
selectSubMode === standardPlan?.nextMode
) {
return (
<Button mt={4} mb={6} w={'100%'} variant={'whiteBase'} isDisabled>
{t('support.wallet.subscription.Next plan')}
</Button>
);
}
if (isCurrentPlan) {
return (
<Button
mt={4}
mb={6}
w={'100%'}
variant={'whiteBase'}
isDisabled={
item.level === standardPlan?.nextSubLevel &&
selectSubMode === standardPlan?.nextMode
}
onClick={() =>
onclickPreCheckStandPlan({
level: item.level,
mode: selectSubMode
})
}
>
{t('support.wallet.subscription.Current plan')}
</Button>
);
}
{standardSubList.map((item) => (
<Box
key={item.level}
bg={'rgba(255, 255, 255, 0.90)'}
p={'28px'}
borderRadius={'2xl'}
borderWidth={'1px'}
borderColor={'myGray.150'}
boxShadow={'1.5'}
>
<Box fontSize={'lg'} fontWeight={'500'}>
{t(item.label)}
</Box>
<Box fontSize={['32px', '42px']} fontWeight={'bold'}>
{item.price}
</Box>
<Box color={'myGray.500'} h={'40px'}>
{t(item.desc, { title: feConfigs?.systemTitle })}
</Box>
{(() => {
if (item.level === StandardSubLevelEnum.free && selectSubMode === SubModeEnum.year) {
return (
<Button
mt={4}
mb={6}
w={'100%'}
variant={'primaryGhost'}
isLoading={isUpdatingStandardPlan || isCheckingStandardPlan}
onClick={() =>
onclickPreCheckStandPlan({
level: item.level,
mode: selectSubMode
})
}
>
{t('support.wallet.subscription.Buy now')}
<Button isDisabled mt={4} mb={6} w={'100%'} variant={'solid'}>
{t('support.wallet.subscription.Nonsupport')}
</Button>
);
})()}
}
if (
item.level === standardPlan?.currentSubLevel &&
selectSubMode === standardPlan?.currentMode
) {
return (
<Button mt={4} mb={6} w={'100%'} variant={'whiteBase'} isDisabled>
{t('support.wallet.subscription.Current plan')}
</Button>
);
}
if (
item.level === standardPlan?.nextSubLevel &&
selectSubMode === standardPlan?.nextMode
) {
return (
<Button mt={4} mb={6} w={'100%'} variant={'whiteBase'} isDisabled>
{t('support.wallet.subscription.Next plan')}
</Button>
);
}
return (
<Button
mt={4}
mb={6}
w={'100%'}
variant={'primaryGhost'}
isLoading={isUpdatingStandardPlan || isCheckingStandardPlan}
onClick={() =>
onclickPreCheckStandPlan({
level: item.level,
mode: selectSubMode
})
}
>
{t('support.wallet.subscription.Buy now')}
</Button>
);
})()}
{/* function list */}
<StandardPlanContentList level={item.level} mode={selectSubMode} />
</Box>
);
})}
{/* function list */}
<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: item.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: item.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: item.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: item.chatHistoryStoreDuration
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max dataset size', {
amount: item.maxDatasetSize
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Points', {
amount: item.totalPoints
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.Training weight', {
weight: item.trainingWeight
})}
</Box>
</Flex>
{!!item.customApiKey && (
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>API Key</Box>
</Flex>
)}
{!!item.websiteSyncInterval && (
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>{item.websiteSyncInterval} h/ web站点同步</Box>
</Flex>
)}
</Grid>
</Box>
))}
</Grid>
{!!confirmPayData && (
<ConfirmPayModal
{...confirmPayData}
onClose={() => setConfirmPayData(undefined)}
onConfirmPay={() => onclickUpdateStandardPlan(confirmPayData.planProps)}
/>
)}
<ConfirmModal />
</Flex>
);
};
@@ -295,87 +338,3 @@ const RowTabs = ({
</Box>
);
};
const ConfirmPayModal = ({
teamBalance,
totalPrice,
payPrice,
onClose,
onConfirmPay
}: ConfirmPayModalProps & { onClose: () => void; onConfirmPay: () => void }) => {
const { t } = useTranslation();
const [qrPayData, setQRPayData] = useState<QRPayProps>();
const formatPayPrice = Math.ceil(formatStorePrice2Read(payPrice));
const formatTeamBalance = Math.floor(formatStorePrice2Read(teamBalance));
const { mutate: handleClickPay, isLoading } = useRequest({
mutationFn: async (amount: number) => {
// 获取支付二维码
return getWxPayQRCode({
type: BillTypeEnum.balance,
balance: amount
});
},
onSuccess(res) {
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
});
}
});
return (
<MyModal
isOpen
iconSrc="modal/confirmPay"
title={t('support.wallet.Confirm pay')}
onClose={onClose}
>
<ModalBody py={5} px={9}>
<Flex>
<Box flex={'0 0 100px'}></Box>
<Box>{formatStorePrice2Read(totalPrice)}</Box>
</Flex>
<Flex mt={6}>
<Box flex={'0 0 100px'}></Box>
<Box>{Math.floor(formatStorePrice2Read(totalPrice - payPrice))}</Box>
</Flex>
<Flex mt={6}>
<Box flex={'0 0 100px'}></Box>
<Box>{formatPayPrice}</Box>
</Flex>
</ModalBody>
<ModalFooter
borderTopWidth={'1px'}
borderTopColor={'borderColor.base'}
mx={9}
justifyContent={'flex-start'}
px={0}
>
<Box>: </Box>
<Box ml={2} flex={1}>
{formatTeamBalance}
</Box>
{teamBalance >= payPrice ? (
<Button size={'sm'} onClick={onConfirmPay}>
</Button>
) : (
<Button
size={'sm'}
isLoading={isLoading}
onClick={() => {
handleClickPay(Math.ceil(formatStorePrice2Read(payPrice - teamBalance)));
}}
>
</Button>
)}
</ModalFooter>
{!!qrPayData && <QRCodePayModal {...qrPayData} onSuccess={onConfirmPay} />}
</MyModal>
);
};

View File

@@ -3,56 +3,48 @@ import { serviceSideProps } from '@/web/common/utils/i18n';
import { Box, Image } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamPlanStatus } from '@/web/support/wallet/sub/api';
import { getTeamDatasetValidSub } from '@/web/support/wallet/sub/api';
import { useQuery } from '@tanstack/react-query';
import StandardPlan from './components/Standard';
import ExtraPlan from './components/ExtraPlan';
import PointsCard from './components/Points';
import FAQ from './components/FAQ';
import { getToken } from '@/web/support/user/auth';
import Script from 'next/script';
const PriceBox = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { data: teamSubPlan, refetch: refetchTeamSubPlan } = useQuery(
['getTeamPlanStatus'],
getTeamPlanStatus,
['getTeamDatasetValidSub'],
getTeamDatasetValidSub,
{
enabled: !!getToken() || !!userInfo
enabled: !!userInfo
}
);
return (
<>
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
<Box
h={'100%'}
overflow={'overlay'}
w={'100%'}
px={['20px', '5vw']}
py={['30px', '80px']}
backgroundImage={'url(/imgs/priceBg.svg)'}
backgroundSize={'cover'}
backgroundRepeat={'no-repeat'}
>
{/* standard sub */}
<StandardPlan
standardPlan={teamSubPlan?.standard}
refetchTeamSubPlan={refetchTeamSubPlan}
/>
<Box
h={'100%'}
overflow={'overlay'}
w={'100%'}
px={['20px', '5vw']}
py={['30px', '80px']}
backgroundImage={'url(/imgs/priceBg.svg)'}
backgroundSize={'cover'}
backgroundRepeat={'no-repeat'}
>
{/* standard sub */}
<StandardPlan standardPlan={teamSubPlan?.standard} refetchTeamSubPlan={refetchTeamSubPlan} />
<ExtraPlan />
<ExtraPlan extraDatasetSize={teamSubPlan?.extraDatasetSize} />
{/* points */}
<PointsCard />
{/* points */}
<PointsCard />
{/* question */}
<FAQ />
</Box>
</>
{/* question */}
<FAQ />
</Box>
);
};

View File

@@ -40,15 +40,6 @@ const Tools = () => {
link: getDocPath('/docs/intro')
}
]
: []),
...(feConfigs?.show_pay
? [
{
icon: 'support/bill/priceLight',
label: '计费说明',
link: '/price'
}
]
: [])
];

View File

@@ -0,0 +1,17 @@
import React from 'react';
import Price from '@/components/support/wallet/Price';
import { useRouter } from 'next/router';
import { serviceSideProps } from '@/web/common/utils/i18n';
const PriceBox = () => {
const router = useRouter();
return <Price onClose={router.back} />;
};
export default PriceBox;
export async function getServerSideProps(context: any) {
return {
props: { ...(await serviceSideProps(context)) }
};
}