V4.8.15 feature (#3331)
* feat: add customize toolkit (#3205) * chaoyang * fix-auth * add toolkit * add order * plugin usage * fix * delete console: * Fix: Fix fullscreen preview top positioning and improve Markdown rendering logic (#3247) * 完成任务:修复全屏预览顶部固定问题,优化 Markdown 渲染逻辑 * 有问题修改 * 问题再修改 * 修正问题 * fix: plugin standalone display issue (#3254) * 4.8.15 test (#3246) * o1 config * perf: system plugin code * 调整系统插件代码。增加html 渲染安全配置。 (#3258) * perf: base64 picker * perf: list app or dataset * perf: plugin config code * 小窗适配等问题 (#3257) * 小窗适配等问题 * git问题 * 小窗剩余问题 * feat: system plugin auth and lock version (#3265) * feat: system plugin auth and lock version * update comment * 4.8.15 test (#3267) * tmp log * perf: login direct * perf: iframe html code * remove log * fix: plugin standalone display (#3277) * refactor: 页面拆分&i18n拆分 (#3281) * refactor: account组件拆成独立页面 * script: 新增i18n json文件创建脚本 * refactor: 页面i18n拆分 * i18n: add en&hant * 4.8.15 test (#3285) * tmp log * remove log * fix: watch avatar refresh * perf: i18n code * fix(plugin): use intro instead of userguide (#3290) * Universal SSO (#3292) * tmp log * remove log * feat: common oauth * readme * perf: sso provider * remove sso code * perf: refresh plugins * feat: add api dataset (#3272) * add api-dataset * fix api-dataset * fix api dataset * fix ts * perf: create collection code (#3301) * tmp log * remove log * perf: i18n change * update version doc * feat: question guide from chatId * perf: create collection code * fix: request api * fix: request api * fix: tts auth and response type (#3303) * perf: md splitter * fix: tts auth and response type * fix: api file dataset (#3307) * perf: api dataset init (#3310) * perf: collection schema * perf: api dataset init * refactor: 团队管理独立页面 (#3302) * ui: 团队管理独立页面 * 代码优化 * fix * perf: sync collection and ui check (#3314) * perf: sync collection * remove script * perf: update api server * perf: api dataset parent * perf: team ui * perf: team 18n * update team ui * perf: ui check * perf: i18n * fix: debug variables & cronjob & system plugin callback load (#3315) * fix: debug variables & cronjob & system plugin callback load * fix type * fix * fix * fix: plugin dataset quote;perf: system variables init (#3316) * fix: plugin dataset quote * perf: system variables init * perf: node templates ui;fix: dataset import ui (#3318) * fix: dataset import ui * perf: node templates ui * perf: ui refresh * feat:套餐改名和套餐跳转配置 (#3309) * fixing:except Sidebar * 去除了多余的代码 * 修正了套餐说明的代码 * 修正了误删除的show_git代码 * 修正了名字部分等代码 * 修正了问题,遗留了其他和ui讨论不一致的部分 * 4.8.15 test (#3319) * remove log * pref: bill ui * pref: bill ui * perf: log * html渲染文档 (#3270) * html渲染文档 * 文档有点小问题 * feat: doc (#3322) * 集合重训练 (#3282) * rebaser * 一点补充 * 小问题 * 其他问题修正,删除集合保留文件的参数还没找到... * reTraining * delete uesless * 删除了一行错误代码 * 集合重训练部分 * fixing * 删除console代码 * feat: navbar item config (#3326) * perf: custom navbar code;perf: retraining code;feat: api dataset and dataset api doc (#3329) * feat: api dataset and dataset api doc * perf: retraining code * perf: custom navbar code * fix: ts (#3330) * fix: ts * fix: ts * retraining ui * perf: api collection filter * perf: retrining button --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com> Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
This commit is contained in:
27
projects/app/src/pages/account/apikey.tsx
Normal file
27
projects/app/src/pages/account/apikey.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import ApiKeyTable from '@/components/support/apikey/Table';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import AccountContainer, { TabEnum } from './components/AccountContainer';
|
||||
import { serviceSideProps } from '../../web/common/utils/i18n';
|
||||
|
||||
const ApiKey = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<AccountContainer>
|
||||
<Box px={[4, 8]} py={[4, 6]}>
|
||||
<ApiKeyTable tips={t('account_apikey:key_tips')}></ApiKeyTable>
|
||||
</Box>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account_apikey', 'account', 'publish']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default ApiKey;
|
||||
@@ -79,8 +79,8 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
successToast: t('common:common.submit_success'),
|
||||
errorToast: t('common:common.Submit failed'),
|
||||
successToast: t('account_bill:submit_success'),
|
||||
errorToast: t('account_bill:submit_failed'),
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
router.reload();
|
||||
@@ -100,6 +100,7 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
emailAddress: ''
|
||||
}
|
||||
});
|
||||
|
||||
const { loading: isLoadingHeader } = useRequest2(() => getTeamInvoiceHeader(), {
|
||||
manual: false,
|
||||
onSuccess: (res) => inputForm.reset(res)
|
||||
@@ -120,12 +121,12 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
w={'43rem'}
|
||||
onClose={onClose}
|
||||
isLoading={isLoading}
|
||||
title={t('common:support.wallet.apply_invoice')}
|
||||
title={t('account_bill:support_wallet_apply_invoice')}
|
||||
>
|
||||
{!isOpenSettleModal ? (
|
||||
<Box px={['1.6rem', '3.25rem']} py={['1rem', '2rem']}>
|
||||
<Box fontWeight={500} fontSize={'1rem'} pb={'0.75rem'}>
|
||||
{t('common:support.wallet.billable_invoice')}
|
||||
{t('account_bill:support_wallet_apply_invoice')}
|
||||
</Box>
|
||||
<Box h={'27.9rem'} overflow={'auto'}>
|
||||
<TableContainer>
|
||||
@@ -149,9 +150,9 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
}}
|
||||
/>
|
||||
</Th>
|
||||
<Th>{t('common:user.type')}</Th>
|
||||
<Th>{t('common:user.Time')}</Th>
|
||||
<Th>{t('common:support.wallet.Amount')}</Th>
|
||||
<Th>{t('account_bill:type')}</Th>
|
||||
<Th>{t('account_bill:time')}</Th>
|
||||
<Th>{t('account_bill:support_wallet_amount')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'0.875rem'}>
|
||||
@@ -179,7 +180,9 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss')
|
||||
: '-'}
|
||||
</Td>
|
||||
<Td>{t('common:pay.yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
|
||||
<Td>
|
||||
{t('account_bill:yuan', { amount: formatStorePrice2Read(item.price) })}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
@@ -193,7 +196,7 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t('common:support.wallet.noBill')}
|
||||
{t('account_bill:no_invoice_record')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -213,7 +216,7 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box px={'1.25rem'} py={'0.5rem'}>
|
||||
{t('common:common.Confirm')}
|
||||
{t('account_bill:confirm')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
@@ -223,8 +226,8 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
<Box px={['1.6rem', '3.25rem']} py={['1rem', '2rem']}>
|
||||
<Box w={'100%'} fontSize={'0.875rem'}>
|
||||
<Flex w={'100%'} justifyContent={'space-between'}>
|
||||
<Box>{t('common:support.wallet.invoice_amount')}</Box>
|
||||
<Box>{t('common:pay.yuan', { amount: formatStorePrice2Read(totalPrice) })}</Box>
|
||||
<Box>{t('account_bill:support_wallet_amount')}</Box>
|
||||
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(totalPrice) })}</Box>
|
||||
</Flex>
|
||||
<Box w={'100%'} py={4}>
|
||||
<Divider showBorderBottom={false} />
|
||||
@@ -248,21 +251,21 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
>
|
||||
<MyIcon name="infoRounded" w={'14px'} h={'14px'} />
|
||||
<Box ml={2} fontSize={'0.6875rem'}>
|
||||
{t('common:support.wallet.invoice_info')}
|
||||
{t('account_bill:invoice_sending_info')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex justify={'flex-end'} w={'100%'} pt={[3, 7]}>
|
||||
<Button variant={'outline'} mr={'0.75rem'} px="0" onClick={handleBack}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box px={'1.25rem'} py={'0.5rem'}>
|
||||
{t('common:back')}
|
||||
{t('account_bill:back')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
<Button isLoading={isSubmitting} px="0" onClick={inputForm.handleSubmit(onSubmitApply)}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box px={'1.25rem'} py={'0.5rem'}>
|
||||
{t('common:common.Confirm')}
|
||||
{t('account_bill:confirm')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
@@ -44,7 +44,7 @@ const BillTable = () => {
|
||||
const billTypeList = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ label: t('common:common.All'), value: '' },
|
||||
{ label: t('account_bill:all'), value: '' },
|
||||
...Object.entries(billTypeMap).map(([key, value]) => ({
|
||||
label: t(value.label as any),
|
||||
value: key
|
||||
@@ -120,9 +120,9 @@ const BillTable = () => {
|
||||
w={'130px'}
|
||||
></MySelect>
|
||||
</Th>
|
||||
<Th>{t('common:user.Time')}</Th>
|
||||
<Th>{t('common:support.wallet.Amount')}</Th>
|
||||
<Th>{t('common:support.wallet.bill.Status')}</Th>
|
||||
<Th>{t('account_bill:time')}</Th>
|
||||
<Th>{t('account_bill:support_wallet_amount')}</Th>
|
||||
<Th>{t('account_bill:status')}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -134,16 +134,16 @@ const BillTable = () => {
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{commonT('common:pay.yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
|
||||
<Td>{t('account_bill:yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
|
||||
<Td>{t(billStatusMap[item.status]?.label as any)}</Td>
|
||||
<Td>
|
||||
{item.status === 'NOTPAY' && (
|
||||
<Button mr={4} onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
|
||||
{t('common:common.Update')}
|
||||
{t('account_bill:update')}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant={'whiteBase'} size={'sm'} onClick={() => setBillDetail(item)}>
|
||||
{t('common:common.Detail')}
|
||||
{t('account_bill:detail')}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
@@ -164,7 +164,7 @@ const BillTable = () => {
|
||||
>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t('common:support.wallet.noBill')}
|
||||
{t('account_bill:no_invoice_record')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -187,85 +187,79 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/bill.svg"
|
||||
title={t('common:support.wallet.bill_detail')}
|
||||
title={t('account_bill:bill_detail')}
|
||||
maxW={['90vw', '700px']}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Number')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:order_number')}:</FormLabel>
|
||||
<Box>{bill.orderId}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.usage.Time')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:generation_time')}:</FormLabel>
|
||||
<Box>{dayjs(bill.createTime).format('YYYY/MM/DD HH:mm:ss')}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Type')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:order_type')}:</FormLabel>
|
||||
<Box>{t(billTypeMap[bill.type]?.label as any)}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Status')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:status')}:</FormLabel>
|
||||
<Box>{t(billStatusMap[bill.status]?.label as any)}</Box>
|
||||
</Flex>
|
||||
{!!bill.metadata?.payWay && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.payWay.Way')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:payment_method')}:</FormLabel>
|
||||
<Box>{t(billPayWayMap[bill.metadata.payWay]?.label as any)}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.Amount')}:</FormLabel>
|
||||
<Box>{commonT('common:pay.yuan', { amount: formatStorePrice2Read(bill.price) })}</Box>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:support_wallet_amount')}:</FormLabel>
|
||||
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(bill.price) })}</Box>
|
||||
</Flex>
|
||||
{bill.metadata && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.has_invoice')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:has_invoice')}:</FormLabel>
|
||||
{bill.metadata.payWay === 'balance' ? (
|
||||
t('user:bill.not_need_invoice')
|
||||
) : (
|
||||
<Box>
|
||||
{(bill.metadata.payWay = bill.hasInvoice ? t('common:yes') : t('common:no'))}
|
||||
{
|
||||
(bill.metadata.payWay = bill.hasInvoice
|
||||
? t('account_bill:yes')
|
||||
: t('account_bill:no'))
|
||||
}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{!!bill.metadata?.subMode && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>
|
||||
{t('common:support.wallet.subscription.mode.Period')}:
|
||||
</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_period')}:</FormLabel>
|
||||
<Box>{t(subModeMap[bill.metadata.subMode]?.label as any)}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{!!bill.metadata?.standSubLevel && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>
|
||||
{t('common:support.wallet.subscription.Stand plan level')}:
|
||||
</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_package')}:</FormLabel>
|
||||
<Box>{t(standardSubLevelMap[bill.metadata.standSubLevel]?.label as any)}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{bill.metadata?.month !== undefined && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>
|
||||
{t('common:support.wallet.subscription.Month amount')}:
|
||||
</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_mode_month')}:</FormLabel>
|
||||
<Box>{bill.metadata?.month}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{bill.metadata?.datasetSize !== undefined && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>
|
||||
{t('common:support.wallet.subscription.Extra dataset size')}:
|
||||
</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:extra_dataset_size')}:</FormLabel>
|
||||
<Box>{bill.metadata?.datasetSize}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{bill.metadata?.extraPoints !== undefined && (
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 120px'}>
|
||||
{t('common:support.wallet.subscription.Extra ai points')}:
|
||||
</FormLabel>
|
||||
<FormLabel flex={'0 0 120px'}>{t('account_bill:extra_ai_points')}:</FormLabel>
|
||||
<Box>{bill.metadata.extraPoints}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -38,12 +38,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required>
|
||||
{t('common:support.wallet.invoice_data.organization_name')}
|
||||
</FormLabel>
|
||||
<FormLabel required>{t('account_bill:organization_name')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.organization_name')}
|
||||
placeholder={t('account_bill:organization_name')}
|
||||
{...register('teamName', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -52,10 +50,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required>{t('common:support.wallet.invoice_data.unit_code')}</FormLabel>
|
||||
<FormLabel required>{t('account_bill:unit_code')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.unit_code')}
|
||||
placeholder={t('account_bill:unit_code')}
|
||||
{...register('unifiedCreditCode', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -64,12 +62,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required={!!needSpecialInvoice}>
|
||||
{t('common:support.wallet.invoice_data.company_address')}
|
||||
</FormLabel>
|
||||
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:company_address')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.company_address')}
|
||||
placeholder={t('account_bill:company_address')}
|
||||
{...register('companyAddress', { required: !!needSpecialInvoice })}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -78,12 +74,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required={!!needSpecialInvoice}>
|
||||
{t('common:support.wallet.invoice_data.company_phone')}
|
||||
</FormLabel>
|
||||
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:company_phone')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.company_phone')}
|
||||
placeholder={t('account_bill:company_phone')}
|
||||
{...register('companyPhone', { required: !!needSpecialInvoice })}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -92,12 +86,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required={!!needSpecialInvoice}>
|
||||
{t('common:support.wallet.invoice_data.bank')}
|
||||
</FormLabel>
|
||||
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:bank_name')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.bank')}
|
||||
placeholder={t('account_bill:bank_name')}
|
||||
{...register('bankName', { required: !!needSpecialInvoice })}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -106,12 +98,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required={!!needSpecialInvoice}>
|
||||
{t('common:support.wallet.invoice_data.bank_account')}
|
||||
</FormLabel>
|
||||
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:bank_account')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.bank_account')}
|
||||
placeholder={t('account_bill:bank_account')}
|
||||
{...register('bankAccount', { required: !!needSpecialInvoice })}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -120,9 +110,7 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required>
|
||||
{t('common:support.wallet.invoice_data.need_special_invoice')}
|
||||
</FormLabel>
|
||||
<FormLabel required>{t('account_bill:need_special_invoice')}</FormLabel>
|
||||
{/* @ts-ignore */}
|
||||
<RadioGroup
|
||||
value={`${needSpecialInvoice}`}
|
||||
@@ -133,10 +121,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
>
|
||||
<HStack h={'2rem'}>
|
||||
<Radio value="true" pr={'1rem'}>
|
||||
<Box fontSize={'14px'}>{t('common:yes')}</Box>
|
||||
<Box fontSize={'14px'}>{t('account_bill:yes')}</Box>
|
||||
</Radio>
|
||||
<Radio value="false">
|
||||
<Box fontSize={'14px'}>{t('common:no')}</Box>
|
||||
<Box fontSize={'14px'}>{t('account_bill:no')}</Box>
|
||||
</Radio>
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
@@ -149,10 +137,10 @@ export const InvoiceHeaderSingleForm = ({
|
||||
alignItems={['flex-start', 'center']}
|
||||
flexDir={['column', 'row']}
|
||||
>
|
||||
<FormLabel required>{t('common:support.wallet.invoice_data.email')}</FormLabel>
|
||||
<FormLabel required>{t('account_bill:email_address')}</FormLabel>
|
||||
<Input
|
||||
{...styles}
|
||||
placeholder={t('common:support.wallet.invoice_data.email')}
|
||||
placeholder={t('account_bill:email_address')}
|
||||
{...register('emailAddress', {
|
||||
required: true,
|
||||
pattern: {
|
||||
@@ -195,8 +183,8 @@ const InvoiceHeaderForm = () => {
|
||||
(data: TeamInvoiceHeaderType) => updateTeamInvoiceHeader(data),
|
||||
{
|
||||
manual: true,
|
||||
successToast: t('common:common.Save Success'),
|
||||
errorToast: t('common:common.Save Failed')
|
||||
successToast: t('account_bill:save_success'),
|
||||
errorToast: t('account_bill:save_failed')
|
||||
}
|
||||
);
|
||||
|
||||
@@ -214,7 +202,7 @@ const InvoiceHeaderForm = () => {
|
||||
>
|
||||
<Flex alignItems={'center'} px={'20px'}>
|
||||
<Box px={'1.25rem'} py={'0.5rem'}>
|
||||
{t('common:common.Save')}
|
||||
{t('account_bill:save')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
@@ -42,9 +42,9 @@ const InvoiceTable = () => {
|
||||
<Thead h="3rem">
|
||||
<Tr>
|
||||
<Th w={'20%'}>#</Th>
|
||||
<Th w={'20%'}>{t('common:user.Time')}</Th>
|
||||
<Th w={'20%'}>{t('common:support.wallet.Amount')}</Th>
|
||||
<Th w={'20%'}>{t('common:support.wallet.bill.Status')}</Th>
|
||||
<Th w={'20%'}>{t('account_bill:time')}</Th>
|
||||
<Th w={'20%'}>{t('account_bill:support_wallet_amount')}</Th>
|
||||
<Th w={'20%'}>{t('account_bill:status')}</Th>
|
||||
<Th w={'20%'}></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -55,7 +55,7 @@ const InvoiceTable = () => {
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{t('common:pay.yuan', { amount: formatStorePrice2Read(item.amount) })}</Td>
|
||||
<Td>{t('account_bill:yuan', { amount: formatStorePrice2Read(item.amount) })}</Td>
|
||||
<Td>
|
||||
<Flex
|
||||
px={'0.75rem'}
|
||||
@@ -71,8 +71,8 @@ const InvoiceTable = () => {
|
||||
<MyIcon name="point" w={'6px'} h={'6px'} />
|
||||
<Box ml={'0.25rem'}>
|
||||
{item.status === 1
|
||||
? t('common:common.submitted')
|
||||
: t('common:common.have_done')}
|
||||
? t('account_bill:submitted')
|
||||
: t('account_bill:completed')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Td>
|
||||
@@ -91,7 +91,7 @@ const InvoiceTable = () => {
|
||||
>
|
||||
<Flex>
|
||||
<MyIcon name="paragraph" w={'16px'} h={'16px'} />
|
||||
<Box ml={'0.38rem'}>{t('common:common.Detail')}</Box>
|
||||
<Box ml={'0.38rem'}>{t('account_bill:detail')}</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
</Td>
|
||||
@@ -113,7 +113,7 @@ const InvoiceTable = () => {
|
||||
>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t('common:support.wallet.no_invoice')}
|
||||
{t('account_bill:no_invoice_record_tip')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -143,48 +143,27 @@ function InvoiceDetailModal({
|
||||
title={
|
||||
<Flex align={'center'}>
|
||||
<MyIcon name="paragraph" w={'20px'} h={'20px'} color={'blue.600'} />
|
||||
<Box ml={'0.62rem'}>{t('common:support.wallet.invoice_detail')}</Box>
|
||||
<Box ml={'0.62rem'}>{t('account_bill:invoice_detail')}</Box>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<ModalBody px={'3.25rem'} py={'2rem'}>
|
||||
<Flex w={'100%'} h={'100%'} flexDir={'column'} gap={'1rem'}>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_amount')}
|
||||
value={t('common:pay.yuan', { amount: formatStorePrice2Read(invoice.amount) })}
|
||||
label={t('account_bill:invoice_amount')}
|
||||
value={t('account_bill:yuan', { amount: formatStorePrice2Read(invoice.amount) })}
|
||||
/>
|
||||
<LabelItem label={t('account_bill:organization_name')} value={invoice.teamName} />
|
||||
<LabelItem label={t('account_bill:unit_code')} value={invoice.unifiedCreditCode} />
|
||||
<LabelItem label={t('account_bill:company_address')} value={invoice.companyAddress} />
|
||||
<LabelItem label={t('account_bill:company_phone')} value={invoice.companyPhone} />
|
||||
<LabelItem label={t('account_bill:bank_name')} value={invoice.bankName} />
|
||||
<LabelItem label={t('account_bill:bank_account')} value={invoice.bankAccount} />
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.organization_name')}
|
||||
value={invoice.teamName}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.unit_code')}
|
||||
value={invoice.unifiedCreditCode}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.company_address')}
|
||||
value={invoice.companyAddress}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.company_phone')}
|
||||
value={invoice.companyPhone}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.bank')}
|
||||
value={invoice.bankName}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.bank_account')}
|
||||
value={invoice.bankAccount}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.need_special_invoice')}
|
||||
value={invoice.needSpecialInvoice ? t('common:yes') : t('common:no')}
|
||||
/>
|
||||
<LabelItem
|
||||
label={t('common:support.wallet.invoice_data.email')}
|
||||
value={invoice.emailAddress}
|
||||
label={t('account_bill:need_special_invoice')}
|
||||
value={invoice.needSpecialInvoice ? t('account_bill:yes') : t('account_bill:no')}
|
||||
/>
|
||||
<LabelItem label={t('account_bill:email_address')} value={invoice.emailAddress} />
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
@@ -3,8 +3,10 @@ import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import ApplyInvoiceModal from './ApplyInvoiceModal';
|
||||
import ApplyInvoiceModal from './components/ApplyInvoiceModal';
|
||||
import { useRouter } from 'next/router';
|
||||
import AccountContainer, { TabEnum } from '../components/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
|
||||
export enum InvoiceTabEnum {
|
||||
bill = 'bill',
|
||||
@@ -12,9 +14,9 @@ export enum InvoiceTabEnum {
|
||||
invoiceHeader = 'invoiceHeader'
|
||||
}
|
||||
|
||||
const BillTable = dynamic(() => import('./BillTable'));
|
||||
const InvoiceHeaderForm = dynamic(() => import('./InvoiceHeaderForm'));
|
||||
const InvoiceTable = dynamic(() => import('./InvoiceTable'));
|
||||
const BillTable = dynamic(() => import('./components/BillTable'));
|
||||
const InvoiceHeaderForm = dynamic(() => import('./components/InvoiceHeaderForm'));
|
||||
const InvoiceTable = dynamic(() => import('./components/InvoiceTable'));
|
||||
const BillAndInvoice = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -23,15 +25,18 @@ const BillAndInvoice = () => {
|
||||
const [isOpenInvoiceModal, setIsOpenInvoiceModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountContainer>
|
||||
<Box p={['1rem', '2rem']}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'} pb={'0.75rem'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{ label: t('common:support.wallet.bill_tag.bill'), value: InvoiceTabEnum.bill },
|
||||
{ label: t('common:support.wallet.bill_tag.invoice'), value: InvoiceTabEnum.invoice },
|
||||
{ label: t('account_bill:bill_record'), value: InvoiceTabEnum.bill },
|
||||
{
|
||||
label: t('common:support.wallet.bill_tag.default_header'),
|
||||
label: t('account_bill:support_wallet_bill_tag_invoice'),
|
||||
value: InvoiceTabEnum.invoice
|
||||
},
|
||||
{
|
||||
label: t('account_bill:default_header'),
|
||||
value: InvoiceTabEnum.invoiceHeader
|
||||
}
|
||||
]}
|
||||
@@ -49,7 +54,7 @@ const BillAndInvoice = () => {
|
||||
<Button variant={'primary'} px="0" onClick={() => setIsOpenInvoiceModal(true)}>
|
||||
<Flex alignItems={'center'} px={'20px'}>
|
||||
<Box px={'1.25rem'} py={'0.5rem'}>
|
||||
{t('common:support.wallet.invoicing')}
|
||||
{t('account_bill:support_wallet_invoicing')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Button>
|
||||
@@ -68,8 +73,16 @@ const BillAndInvoice = () => {
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account_bill', 'account']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default BillAndInvoice;
|
||||
@@ -1,27 +1,18 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRouter } from 'next/router';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import SideTabs from '@/components/SideTabs';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import UserInfo from './components/Info/index';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Script from 'next/script';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
|
||||
const Promotion = dynamic(() => import('./components/Promotion'));
|
||||
const UsageTable = dynamic(() => import('./components/UsageTable'));
|
||||
const BillAndInvoice = dynamic(() => import('./components/bill/BillAndInvoice'));
|
||||
const InformTable = dynamic(() => import('./components/InformTable'));
|
||||
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
|
||||
const Individuation = dynamic(() => import('./components/Individuation'));
|
||||
enum TabEnum {
|
||||
export enum TabEnum {
|
||||
'info' = 'info',
|
||||
'promotion' = 'promotion',
|
||||
'usage' = 'usage',
|
||||
@@ -29,26 +20,44 @@ enum TabEnum {
|
||||
'inform' = 'inform',
|
||||
'individuation' = 'individuation',
|
||||
'apikey' = 'apikey',
|
||||
'loginout' = 'loginout'
|
||||
'loginout' = 'loginout',
|
||||
'team' = 'team'
|
||||
}
|
||||
|
||||
const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
const AccountContainer = ({
|
||||
children,
|
||||
isLoading
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { userInfo, setUserInfo } = useUserStore();
|
||||
const { feConfigs, systemVersion } = useSystemStore();
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const currentTab = useMemo(() => {
|
||||
return router.pathname.split('/').pop() as TabEnum;
|
||||
}, [router.pathname]);
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
icon: 'support/user/userLight',
|
||||
label: t('user:personal_information'),
|
||||
label: t('account:personal_information'),
|
||||
value: TabEnum.info
|
||||
},
|
||||
...(feConfigs?.isPlus
|
||||
? [
|
||||
{
|
||||
icon: 'support/user/usersLight',
|
||||
label: t('account:team'),
|
||||
value: TabEnum.team
|
||||
},
|
||||
{
|
||||
icon: 'support/usage/usageRecordLight',
|
||||
label: t('user:usage_record'),
|
||||
label: t('account:usage_records'),
|
||||
value: TabEnum.usage
|
||||
}
|
||||
]
|
||||
@@ -57,7 +66,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
? [
|
||||
{
|
||||
icon: 'support/bill/payRecordLight',
|
||||
label: t('user:bill_and_invoices'),
|
||||
label: t('account:bills_and_invoices'),
|
||||
value: TabEnum.bill
|
||||
}
|
||||
]
|
||||
@@ -66,7 +75,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
? [
|
||||
{
|
||||
icon: 'support/account/promotionLight',
|
||||
label: t('user:promotion_records'),
|
||||
label: t('account:promotion_records'),
|
||||
value: TabEnum.promotion
|
||||
}
|
||||
]
|
||||
@@ -75,40 +84,36 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
? [
|
||||
{
|
||||
icon: 'support/outlink/apikeyLight',
|
||||
label: t('common:user.apikey.key'),
|
||||
label: t('account:api_key'),
|
||||
value: TabEnum.apikey
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: 'support/user/individuation',
|
||||
label: t('user:personalization'),
|
||||
label: t('account:personalization'),
|
||||
value: TabEnum.individuation
|
||||
},
|
||||
...(feConfigs.isPlus
|
||||
? [
|
||||
{
|
||||
icon: 'support/user/informLight',
|
||||
label: t('user:notice'),
|
||||
label: t('account:notifications'),
|
||||
value: TabEnum.inform
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
icon: 'support/account/loginoutLight',
|
||||
label: t('user:sign_out'),
|
||||
label: t('account:logout'),
|
||||
value: TabEnum.loginout
|
||||
}
|
||||
];
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: t('common:support.user.logout.confirm')
|
||||
content: t('account:confirm_logout')
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
|
||||
const setCurrentTab = useCallback(
|
||||
(tab: string) => {
|
||||
if (tab === TabEnum.loginout) {
|
||||
@@ -117,11 +122,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
router.replace('/login');
|
||||
})();
|
||||
} else {
|
||||
router.replace({
|
||||
query: {
|
||||
currentTab: tab
|
||||
}
|
||||
});
|
||||
router.replace('/account/' + tab);
|
||||
}
|
||||
},
|
||||
[openConfirm, router, setUserInfo]
|
||||
@@ -130,7 +131,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
return (
|
||||
<>
|
||||
<Script src={getWebReqUrl('/js/qrcode.min.js')} strategy="lazyOnload"></Script>
|
||||
<PageContainer>
|
||||
<PageContainer isLoading={isLoading}>
|
||||
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
|
||||
{isPc ? (
|
||||
<Flex
|
||||
@@ -172,13 +173,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
)}
|
||||
|
||||
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]} overflow={'auto'}>
|
||||
{currentTab === TabEnum.info && <UserInfo />}
|
||||
{currentTab === TabEnum.promotion && <Promotion />}
|
||||
{currentTab === TabEnum.usage && <UsageTable />}
|
||||
{currentTab === TabEnum.bill && <BillAndInvoice />}
|
||||
{currentTab === TabEnum.individuation && <Individuation />}
|
||||
{currentTab === TabEnum.inform && <InformTable />}
|
||||
{currentTab === TabEnum.apikey && <ApiKeyTable />}
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
<ConfirmModal />
|
||||
@@ -187,13 +182,4 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
currentTab: content?.query?.currentTab || TabEnum.info,
|
||||
...(await serviceSideProps(content, ['publish', 'user']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Account;
|
||||
export default AccountContainer;
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import ApiKeyTable from '@/components/support/apikey/Table';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
const ApiKey = () => {
|
||||
const { publishT } = useI18n();
|
||||
return (
|
||||
<Box px={[4, 8]} py={[4, 6]}>
|
||||
<ApiKeyTable tips={publishT('key_tips')}></ApiKeyTable>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKey;
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Box, Card, Flex } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { UserType } from '@fastgpt/global/support/user/type';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
|
||||
import I18nLngSelector from '@/components/Select/I18nLngSelector';
|
||||
|
||||
const Individuation = () => {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserType) => {
|
||||
await updateUserInfo({
|
||||
timezone: data.timezone
|
||||
});
|
||||
reset(data);
|
||||
toast({
|
||||
title: t('common:dataset.data.Update Success Tip'),
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
[reset, t, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box py={[3, '28px']} px={['5vw', '64px']}>
|
||||
<Flex alignItems={'center'} fontSize={'lg'} h={'30px'}>
|
||||
<MyIcon mr={2} name={'support/user/individuation'} w={'20px'} />
|
||||
{t('common:support.account.Individuation')}
|
||||
</Flex>
|
||||
|
||||
<Card mt={6} px={[3, 10]} py={[3, 7]} fontSize={'sm'}>
|
||||
<Flex alignItems={'center'} w={['85%', '350px']}>
|
||||
<Box flex={'0 0 80px'}>{t('common:user.Language')}: </Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<I18nLngSelector />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '350px']}>
|
||||
<Box flex={'0 0 80px'}>{t('common:user.Timezone')}: </Box>
|
||||
<TimezoneSelect
|
||||
value={userInfo?.timezone}
|
||||
onChange={(e) => {
|
||||
if (!userInfo) return;
|
||||
onclickSave({ ...userInfo, timezone: e });
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Individuation;
|
||||
@@ -1,96 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { getInforms, readInform } from '@/web/support/user/inform/api';
|
||||
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
|
||||
const InformTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: informs,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination<UserInformSchema>({
|
||||
api: getInforms,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
|
||||
<Box px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
{informs.map((item) => (
|
||||
<Box
|
||||
key={item._id}
|
||||
border={theme.borders.md}
|
||||
py={2}
|
||||
px={4}
|
||||
borderRadius={'md'}
|
||||
position={'relative'}
|
||||
_notLast={{ mb: 3 }}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontWeight={'bold'}>{item.title}</Box>
|
||||
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
|
||||
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
|
||||
</Box>
|
||||
{!item.read && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={'xs'}
|
||||
onClick={async () => {
|
||||
if (!item.read) {
|
||||
await readInform(item._id);
|
||||
getData(pageNum);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:support.inform.Read')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Box mt={2} fontSize={'sm'} color={'myGray.600'} whiteSpace={'pre-wrap'}>
|
||||
{item.content}
|
||||
</Box>
|
||||
{!item.read && (
|
||||
<>
|
||||
<Box
|
||||
w={'5px'}
|
||||
h={'5px'}
|
||||
borderRadius={'10px'}
|
||||
bg={'red.600'}
|
||||
position={'absolute'}
|
||||
top={'8px'}
|
||||
left={'8px'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!isLoading && informs.length === 0 && (
|
||||
<EmptyTip text={t('common:user.no_notice')}></EmptyTip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{total > pageSize && (
|
||||
<Flex w={'100%'} mt={4} px={[3, 8]} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading && informs.length === 0} fixed={false} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InformTable;
|
||||
@@ -1,138 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
Flex,
|
||||
BoxProps,
|
||||
useTheme,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPromotionInitData, getPromotionRecords } from '@/web/support/activity/promotion/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import dayjs from 'dayjs';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
|
||||
const Promotion = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const { userInfo } = useUserStore();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: promotionRecords,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination
|
||||
} = usePagination({
|
||||
api: getPromotionRecords,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
|
||||
['getPromotionInitData'],
|
||||
getPromotionInitData
|
||||
);
|
||||
|
||||
const statisticsStyles: BoxProps = {
|
||||
p: [4, 5],
|
||||
border: theme.borders.base,
|
||||
textAlign: 'center',
|
||||
fontSize: ['md', 'lg'],
|
||||
borderRadius: 'md'
|
||||
};
|
||||
const titleStyles: BoxProps = {
|
||||
mt: 2,
|
||||
fontSize: ['lg', '28px'],
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
|
||||
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('common:user.Amount of inviter')}</Box>
|
||||
<Box {...titleStyles}>{invitedAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('common:user.Amount of earnings')}</Box>
|
||||
<Box {...titleStyles}>{earningsAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('common:user.Promotion Rate')}</Box>
|
||||
<QuestionTip ml={1} label={t('common:user.Promotion rate tip')}></QuestionTip>
|
||||
</Flex>
|
||||
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('common:user.Invite Url')}</Box>
|
||||
<QuestionTip ml={1} label={t('common:user.Invite url tip')}></QuestionTip>
|
||||
</Flex>
|
||||
<Button
|
||||
mt={4}
|
||||
variant={'whitePrimary'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
|
||||
}}
|
||||
>
|
||||
{t('common:user.Copy invite url')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box mt={5}>
|
||||
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:user.Time')}</Th>
|
||||
<Th>{t('common:user.type')}</Th>
|
||||
<Th>{t('common:pay.amount')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{promotionRecords.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{t(`user:promotion.${item.type}` as any)}</Td>
|
||||
<Td>{item.amount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{!isLoading && promotionRecords.length === 0 && (
|
||||
<EmptyTip text={t('common:user.no_invite_records')}></EmptyTip>
|
||||
)}
|
||||
{total > pageSize && (
|
||||
<Flex mt={4} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Promotion;
|
||||
104
projects/app/src/pages/account/components/TeamSelector.tsx
Normal file
104
projects/app/src/pages/account/components/TeamSelector.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, ButtonProps, Flex } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const TeamSelector = ({
|
||||
showManage,
|
||||
...props
|
||||
}: ButtonProps & {
|
||||
showManage?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
const { setLoading } = useSystemStore();
|
||||
|
||||
const { data: myTeams = [] } = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo]
|
||||
});
|
||||
|
||||
const { runAsync: onSwitchTeam } = useRequest2(
|
||||
async (teamId: string) => {
|
||||
setLoading(true);
|
||||
await putSwitchTeam(teamId);
|
||||
return initUserInfo();
|
||||
},
|
||||
{
|
||||
onFinally: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
errorToast: t('common:user.team.Switch Team Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const teamList = useMemo(() => {
|
||||
return myTeams.map((team) => ({
|
||||
label: (
|
||||
<Flex
|
||||
key={team.teamId}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
cursor={'default'}
|
||||
gap={3}
|
||||
onClick={() => onSwitchTeam(team.teamId)}
|
||||
_hover={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Avatar src={team.avatar} w={['1.25rem', '1.375rem']} />
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" fontSize={'sm'}>
|
||||
{team.teamName}
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
value: team.teamId
|
||||
}));
|
||||
}, [myTeams, onSwitchTeam]);
|
||||
|
||||
const formatTeamList = useMemo(() => {
|
||||
return [
|
||||
...(showManage
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<Flex
|
||||
key={'manage'}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
cursor={'default'}
|
||||
gap={3}
|
||||
onClick={() => router.push('/account/team')}
|
||||
>
|
||||
<MyIcon name="common/setting" w={['1.25rem', '1.375rem']} />
|
||||
<Box flex={'1 0 0'} w={0} className="textEllipsis" fontSize={'sm'}>
|
||||
{t('user:manage_team')}
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
value: 'manage',
|
||||
showBorder: true
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...teamList
|
||||
];
|
||||
}, [showManage, teamList, router]);
|
||||
|
||||
return (
|
||||
<Box w={'100%'}>
|
||||
<MySelect {...props} value={userInfo?.team?.teamId} list={formatTeamList} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSelector;
|
||||
@@ -1,196 +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 '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import DateRangePicker, {
|
||||
type DateRangeType
|
||||
} from '@fastgpt/web/components/common/DateRangePicker';
|
||||
import { addDays } from 'date-fns';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
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 } = useSystem();
|
||||
const { userInfo, loadAndGetTeamMembers } = useUserStore();
|
||||
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
|
||||
|
||||
const sourceList = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ label: t('common:common.All'), value: '' },
|
||||
...Object.entries(UsageSourceMap).map(([key, value]) => ({
|
||||
label: t(value.label as any),
|
||||
value: key
|
||||
}))
|
||||
] as {
|
||||
label: never;
|
||||
value: UsageSourceEnum | '';
|
||||
}[],
|
||||
[t]
|
||||
);
|
||||
|
||||
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
|
||||
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
|
||||
if (!userInfo?.team?.teamId) return [];
|
||||
return loadAndGetTeamMembers();
|
||||
});
|
||||
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?.permission.hasManagePer && (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box mr={2} flexShrink={0}>
|
||||
{t('common: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
|
||||
mt={2}
|
||||
px={[3, 8]}
|
||||
position={'relative'}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
{/* <Th>{t('common:user.team.Member Name')}</Th> */}
|
||||
<Th>{t('common:user.Time')}</Th>
|
||||
<Th>
|
||||
<MySelect<UsageSourceEnum | ''>
|
||||
list={sourceList}
|
||||
value={usageSource}
|
||||
size={'sm'}
|
||||
onchange={(e) => {
|
||||
setUsageSource(e);
|
||||
}}
|
||||
w={'130px'}
|
||||
></MySelect>
|
||||
</Th>
|
||||
<Th>{t('common:user.Application Name')}</Th>
|
||||
<Th>{t('common: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 as any) || '-'}</Td>
|
||||
<Td>{t(item.appName as any) || '-'}</Td>
|
||||
<Td>{formatNumber(item.totalPoints) || 0}</Td>
|
||||
<Td>
|
||||
<Button size={'sm'} variant={'whitePrimary'} onClick={() => setUsageDetail(item)}>
|
||||
{t('common:common.Detail')}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{!isLoading && usages.length === 0 && (
|
||||
<EmptyTip text={t('common:user.no_usage_records')}></EmptyTip>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
{!!usageDetail && (
|
||||
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UsageTable);
|
||||
77
projects/app/src/pages/account/individuation.tsx
Normal file
77
projects/app/src/pages/account/individuation.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Box, Card, Flex } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { UserType } from '@fastgpt/global/support/user/type';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
|
||||
import I18nLngSelector from '@/components/Select/I18nLngSelector';
|
||||
import AccountContainer from './components/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
|
||||
const Individuation = () => {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserType) => {
|
||||
await updateUserInfo({
|
||||
timezone: data.timezone
|
||||
});
|
||||
reset(data);
|
||||
toast({
|
||||
title: t('account_individuation:update_data_success'),
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
[reset, t, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccountContainer>
|
||||
<Box py={[3, '28px']} px={['5vw', '64px']}>
|
||||
<Flex alignItems={'center'} fontSize={'lg'} h={'30px'}>
|
||||
<MyIcon mr={2} name={'support/user/individuation'} w={'20px'} />
|
||||
{t('account_individuation:personalization')}
|
||||
</Flex>
|
||||
|
||||
<Card mt={6} px={[3, 10]} py={[3, 7]} fontSize={'sm'}>
|
||||
<Flex alignItems={'center'} w={['85%', '350px']}>
|
||||
<Box flex={'0 0 80px'}>{t('account_individuation:language')}: </Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<I18nLngSelector />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '350px']}>
|
||||
<Box flex={'0 0 80px'}>{t('account_individuation:timezone')}: </Box>
|
||||
<TimezoneSelect
|
||||
value={userInfo?.timezone}
|
||||
onChange={(e) => {
|
||||
if (!userInfo) return;
|
||||
onclickSave({ ...userInfo, timezone: e });
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account', 'account_individuation']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Individuation;
|
||||
@@ -33,8 +33,8 @@ const ConversionModal = ({
|
||||
onSuccess() {
|
||||
router.reload();
|
||||
},
|
||||
successToast: t('user:bill.convert_success'),
|
||||
errorToast: t('user:bill.convert_error')
|
||||
successToast: t('account_info:exchange_success'),
|
||||
errorToast: t('account_info:exchange_failure')
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -43,27 +43,27 @@ const ConversionModal = ({
|
||||
onClose={onClose}
|
||||
iconSrc="support/bill/wallet"
|
||||
iconColor="primary.600"
|
||||
title={t('user:bill.use_balance')}
|
||||
title={t('account_info:usage_balance')}
|
||||
>
|
||||
<ModalBody maxW={'450px'}>
|
||||
<VStack px="2.25" gap={2} pb="6">
|
||||
<HStack px="4" py="2" color="primary.600" bgColor="primary.50" borderRadius="md">
|
||||
<Icon name="common/info" w="1rem" mr="1" />
|
||||
<Box fontSize={'mini'} fontWeight={'500'}>
|
||||
{t('user:bill.use_balance_hint')}
|
||||
{t('account_info:usage_balance_notice')}
|
||||
</Box>
|
||||
</HStack>
|
||||
<VStack mt={6}>
|
||||
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
|
||||
{t('user:bill.current_token_price')}
|
||||
{t('account_info:current_token_price')}
|
||||
</Box>
|
||||
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
|
||||
¥15/1000 {t('user:bill.tokens')}/{t('common:common.month')}
|
||||
¥15/1000 {t('account_info:tokens')}/{t('account_info:month')}
|
||||
</Box>
|
||||
</VStack>
|
||||
<VStack mt={6}>
|
||||
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
|
||||
{t('user:bill.balance')}
|
||||
{t('account_info:balance')}
|
||||
</Box>
|
||||
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
|
||||
¥{formatStorePrice2Read(userInfo?.team?.balance)?.toFixed(2)}
|
||||
@@ -71,13 +71,13 @@ const ConversionModal = ({
|
||||
</VStack>
|
||||
<VStack mt={6}>
|
||||
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
|
||||
{t('user:bill.you_can_convert')}
|
||||
{t('account_info:you_can_convert')}
|
||||
</Box>
|
||||
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
|
||||
{points} {t('user:bill.tokens')}
|
||||
{points} {t('account_info:tokens')}
|
||||
</Box>
|
||||
<Tag fontSize={'xs'} fontWeight={'500'}>
|
||||
{t('user:bill.token_expire_1year')}
|
||||
{t('account_info:token_validity_period')}
|
||||
</Tag>
|
||||
</VStack>
|
||||
|
||||
@@ -90,10 +90,10 @@ const ConversionModal = ({
|
||||
onClick={onConvert}
|
||||
isLoading={loading}
|
||||
>
|
||||
{t('user:bill.conversion')}
|
||||
{t('account_info:exchange')}
|
||||
</Button>
|
||||
<Link fontSize={'sm'} color="primary" mt="2" onClick={onOpenContact}>
|
||||
{t('user:bill.contact_customer_service')}
|
||||
{t('account_info:contact_customer_service')}
|
||||
</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
@@ -25,7 +25,7 @@ const OpenAIAccountModal = ({
|
||||
onSuccess(res) {
|
||||
onClose();
|
||||
},
|
||||
errorToast: t('common:user.Set OpenAI Account Failed')
|
||||
errorToast: t('account_info:openai_account_setting_exception')
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -33,11 +33,11 @@ const OpenAIAccountModal = ({
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="common/openai"
|
||||
title={t('common:user.OpenAI Account Setting')}
|
||||
title={t('account_info:openai_account_configuration')}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
{t('common:info.open_api_notice')}
|
||||
{t('account_info:open_api_notice')}
|
||||
</Box>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 65px'}>API Key:</Box>
|
||||
@@ -48,16 +48,16 @@ const OpenAIAccountModal = ({
|
||||
<Input
|
||||
flex={1}
|
||||
{...register('baseUrl')}
|
||||
placeholder={t('common:info.open_api_placeholder')}
|
||||
placeholder={t('account_info:request_address_notice')}
|
||||
></Input>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
{t('account_info:cancel')}
|
||||
</Button>
|
||||
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
|
||||
{t('common:common.Confirm')}
|
||||
{t('account_info:confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
@@ -47,8 +47,8 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
|
||||
initUserInfo();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('user:bind_inform_account_success'),
|
||||
errorToast: t('user:bind_inform_account_error')
|
||||
successToast: t('account_info:bind_notification_success'),
|
||||
errorToast: t('account_info:bind_notification_error')
|
||||
}
|
||||
);
|
||||
|
||||
@@ -58,9 +58,9 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
|
||||
?.map((item) => {
|
||||
switch (item) {
|
||||
case 'email':
|
||||
return t('common:support.user.login.Email');
|
||||
return t('account_info:email_label');
|
||||
case 'phone':
|
||||
return t('common:support.user.login.Phone number');
|
||||
return t('account_info:phone_label');
|
||||
}
|
||||
})
|
||||
.join('/');
|
||||
@@ -71,16 +71,16 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
|
||||
isOpen
|
||||
iconSrc="common/settingLight"
|
||||
w={'32rem'}
|
||||
title={t('common:user.Notification Receive')}
|
||||
title={t('account_info:notification_receiving_hint')}
|
||||
>
|
||||
<ModalBody px={10}>
|
||||
<Flex flexDirection="column">
|
||||
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
|
||||
<Icon name="common/info" w="1rem" />
|
||||
<Box fontSize={'sm'}>{t('user:notification.Bind Notification Pipe Hint')}</Box>
|
||||
<Box fontSize={'sm'}>{t('account_info:bind_notification_hint')}</Box>
|
||||
</HStack>
|
||||
<Flex mt="4" alignItems="center">
|
||||
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
|
||||
<Box flex={'0 0 70px'}>{t('account_info:user_account')}</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
bg={'myGray.50'}
|
||||
@@ -89,12 +89,12 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
|
||||
></Input>
|
||||
</Flex>
|
||||
<Flex mt="6" alignItems="center" position={'relative'}>
|
||||
<Box flex={'0 0 70px'}>{t('user:password.verification_code')}</Box>
|
||||
<Box flex={'0 0 70px'}>{t('account_info:verification_code_required')}</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
bg={'myGray.50'}
|
||||
{...register('verifyCode', { required: true })}
|
||||
placeholder={t('user:password.code_required')}
|
||||
placeholder={t('account_info:code_required')}
|
||||
></Input>
|
||||
<SendCodeBox username={account} />
|
||||
</Flex>
|
||||
@@ -102,14 +102,14 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
{t('account_info:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isDisabled={!account || !verifyCode}
|
||||
onClick={handleSubmit((data) => onSubmit(data))}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
{t('account_info:confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
@@ -25,15 +25,15 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { mutate: onSubmit, isLoading } = useRequest({
|
||||
mutationFn: (data: FormType) => {
|
||||
if (data.newPsw !== data.confirmPsw) {
|
||||
return Promise.reject(t('common:common.Password inconsistency'));
|
||||
return Promise.reject(t('account_info:password_mismatch'));
|
||||
}
|
||||
return updatePasswordByOld(data);
|
||||
},
|
||||
onSuccess() {
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:user.Update password successful'),
|
||||
errorToast: t('common:user.Update password failed')
|
||||
successToast: t('account_info:password_update_success'),
|
||||
errorToast: t('account_info:password_update_error')
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -41,15 +41,15 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/password.svg"
|
||||
title={t('common:user.Update Password')}
|
||||
title={t('account_info:update_password')}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>{t('common:user.old_password') + ':'}</Box>
|
||||
<Box flex={'0 0 70px'}>{t('account_info:old_password') + ':'}</Box>
|
||||
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'}>{t('common:user.new_password') + ':'}</Box>
|
||||
<Box flex={'0 0 70px'}>{t('account_info:new_password') + ':'}</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'password'}
|
||||
@@ -57,13 +57,13 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 60,
|
||||
message: t('common:user.password_message')
|
||||
message: t('account_info:password_length_error')
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'}>{t('common:user.confirm_password') + ':'}</Box>
|
||||
<Box flex={'0 0 70px'}>{t('account_info:confirm_password') + ':'}</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'password'}
|
||||
@@ -71,7 +71,7 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 60,
|
||||
message: t('common:user.password_message')
|
||||
message: t('account_info:password_length_error')
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
@@ -79,10 +79,10 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
{t('account_info:cancel')}
|
||||
</Button>
|
||||
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
|
||||
{t('common:common.Confirm')}
|
||||
{t('account_info:confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
@@ -65,7 +65,7 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
|
||||
isOpen
|
||||
maxW={['90vw', '1200px']}
|
||||
iconSrc="modal/teamPlans"
|
||||
title={t('common:support.wallet.Standard Plan Detail')}
|
||||
title={t('account_info:package_details')}
|
||||
isCentered
|
||||
>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
@@ -74,11 +74,11 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:support.standard.type')}</Th>
|
||||
<Th>{t('common:support.standard.storage')}</Th>
|
||||
<Th>{t('common:support.standard.AI Bonus Points')}</Th>
|
||||
<Th>{t('user:bill.valid_time')}</Th>
|
||||
<Th>{t('common:support.standard.due_date')}</Th>
|
||||
<Th>{t('account_info:type')}</Th>
|
||||
<Th>{t('account_info:storage_capacity')}</Th>
|
||||
<Th>{t('account_info:ai_points')}</Th>
|
||||
<Th>{t('account_info:effective_time')}</Th>
|
||||
<Th>{t('account_info:expiration_time')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
@@ -121,12 +121,10 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
|
||||
<StatusTag status={status as packageStatus} />
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>
|
||||
{datasetSize ? `${datasetSize + t('common:core.dataset.data.group')}` : '-'}
|
||||
</Td>
|
||||
<Td>{datasetSize ? `${datasetSize + t('account_info:group')}` : '-'}</Td>
|
||||
<Td>
|
||||
{totalPoints
|
||||
? `${Math.round(totalPoints - surplusPoints)} / ${totalPoints} ${t('common:support.wallet.subscription.point')}`
|
||||
? `${Math.round(totalPoints - surplusPoints)} / ${totalPoints} ${t('account_info:ai_points_calculation_standard')}`
|
||||
: '-'}
|
||||
</Td>
|
||||
<Td color={'myGray.600'}>{formatTime2YMDHM(startTime)}</Td>
|
||||
@@ -143,7 +141,7 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
|
||||
<HStack mt={4} color={'primary.700'}>
|
||||
<MyIcon name={'infoRounded'} w={'1rem'} />
|
||||
<Box fontSize={'mini'} fontWeight={'500'}>
|
||||
{t('user:bill.standard_valid_tip')}
|
||||
{t('account_info:package_usage_rules')}
|
||||
</Box>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
@@ -155,9 +153,9 @@ function StatusTag({ status }: { status: packageStatus }) {
|
||||
const { t } = useTranslation();
|
||||
const statusText = useMemo(() => {
|
||||
return {
|
||||
inactive: t('common:support.wallet.subscription.status.inactive'),
|
||||
active: t('common:support.wallet.subscription.status.active'),
|
||||
expired: t('common:support.wallet.subscription.status.expired')
|
||||
inactive: t('account_info:pending_usage'),
|
||||
active: t('account_info:active'),
|
||||
expired: t('account_info:expired')
|
||||
};
|
||||
}, [t]);
|
||||
const styleMap = useMemo(() => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
Link,
|
||||
Progress,
|
||||
Grid,
|
||||
BoxProps
|
||||
BoxProps,
|
||||
FlexProps
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
@@ -25,7 +26,6 @@ import { useTranslation } from 'next-i18next';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useRouter } from 'next/router';
|
||||
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
|
||||
import { putUpdateMemberName } from '@/web/support/user/team/api';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
@@ -35,10 +35,7 @@ import {
|
||||
standardSubLevelMap
|
||||
} from '@fastgpt/global/support/wallet/sub/constants';
|
||||
import { formatTime2YMD } from '@fastgpt/global/common/string/time';
|
||||
import {
|
||||
AI_POINT_USAGE_CARD_ROUTE,
|
||||
EXTRA_PLAN_CARD_ROUTE
|
||||
} from '@/web/support/wallet/sub/constants';
|
||||
import { getExtraPlanCardRoute } from '@/web/support/wallet/sub/constants';
|
||||
|
||||
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
@@ -46,28 +43,33 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
import AccountContainer from '../components/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import { useRouter } from 'next/router';
|
||||
import TeamSelector from '../components/TeamSelector';
|
||||
|
||||
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
|
||||
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
|
||||
const ConversionModal = dynamic(() => import('./ConversionModal'));
|
||||
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'));
|
||||
const UpdateNotification = dynamic(() => import('./UpdateNotificationModal'));
|
||||
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'));
|
||||
const StandDetailModal = dynamic(() => import('./components/standardDetailModal'));
|
||||
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
|
||||
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
|
||||
const UpdateNotification = dynamic(() => import('./components/UpdateNotificationModal'));
|
||||
const OpenAIAccountModal = dynamic(() => import('./components/OpenAIAccountModal'));
|
||||
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
|
||||
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
|
||||
const AiPointsModal = dynamic(() =>
|
||||
import('@/pages/price/components/Points').then((mod) => mod.AiPointsModal)
|
||||
);
|
||||
|
||||
const Account = () => {
|
||||
const Info = () => {
|
||||
const { isPc } = useSystem();
|
||||
const { teamPlanStatus } = useUserStore();
|
||||
const standardPlan = teamPlanStatus?.standardConstants;
|
||||
const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure();
|
||||
|
||||
const { initUserInfo } = useUserStore();
|
||||
|
||||
useQuery(['init'], initUserInfo);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountContainer>
|
||||
<Box py={[3, '28px']} px={[5, 10]} mx={'auto'}>
|
||||
{isPc ? (
|
||||
<Flex justifyContent={'center'} maxW={'1080px'}>
|
||||
@@ -92,11 +94,19 @@ const Account = () => {
|
||||
)}
|
||||
</Box>
|
||||
{isOpenContact && <CommunityModal onClose={onCloseContact} />}
|
||||
</>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Account);
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account', 'account_info', 'user']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default React.memo(Info);
|
||||
|
||||
const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
const theme = useTheme();
|
||||
@@ -110,6 +120,8 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
const standardPlan = teamPlanStatus?.standardConstants;
|
||||
const { isPc } = useSystem();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
isOpen: isOpenConversionModal,
|
||||
onClose: onCloseConversionModal,
|
||||
@@ -139,7 +151,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
});
|
||||
reset(data);
|
||||
toast({
|
||||
title: t('common:dataset.data.Update Success Tip'),
|
||||
title: t('account_info:update_success_tip'),
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
@@ -164,7 +176,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
});
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : t('common:common.error.Select avatar failed'),
|
||||
title: typeof err === 'string' ? err : t('account_info:avatar_selection_exception'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
@@ -184,16 +196,16 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
{isPc && (
|
||||
<Flex alignItems={'center'} fontSize={'md'} h={'30px'}>
|
||||
<MyIcon mr={2} name={'support/user/userLight'} w={'1.25rem'} />
|
||||
{t('common:support.user.User self info')}
|
||||
{t('account_info:personal_information')}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Box mt={[0, 6]} fontSize={'sm'}>
|
||||
{isPc ? (
|
||||
<Flex alignItems={'center'} cursor={'pointer'}>
|
||||
<Box {...labelStyles}>{t('common:support.user.Avatar')}: </Box>
|
||||
<Box {...labelStyles}>{t('account_info:avatar')}: </Box>
|
||||
|
||||
<MyTooltip label={t('common:common.avatar.Select Avatar')}>
|
||||
<MyTooltip label={t('account_info:select_avatar')}>
|
||||
<Box
|
||||
w={['44px', '56px']}
|
||||
h={['44px', '56px']}
|
||||
@@ -216,7 +228,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenSelectFile}
|
||||
>
|
||||
<MyTooltip label={t('common:common.avatar.Select Avatar')}>
|
||||
<MyTooltip label={t('account_info:choose_avatar')}>
|
||||
<Box
|
||||
w={['44px', '54px']}
|
||||
h={['44px', '54px']}
|
||||
@@ -233,17 +245,17 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
|
||||
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
|
||||
<MyIcon mr={1} name={'edit'} w={'14px'} />
|
||||
{t('common:user.Replace')}
|
||||
{t('account_info:change')}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{feConfigs?.isPlus && (
|
||||
<Flex mt={[0, 4]} alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('common:user.Member Name')}: </Box>
|
||||
<Box {...labelStyles}>{t('account_info:member_name')}: </Box>
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
defaultValue={userInfo?.team?.memberName || 'Member'}
|
||||
title={t('common:user.Edit name')}
|
||||
title={t('account_info:click_modify_nickname')}
|
||||
borderColor={'transparent'}
|
||||
transform={'translateX(-11px)'}
|
||||
maxLength={20}
|
||||
@@ -258,21 +270,21 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
</Flex>
|
||||
)}
|
||||
<Flex alignItems={'center'} mt={6}>
|
||||
<Box {...labelStyles}>{t('common:user.Account')}: </Box>
|
||||
<Box {...labelStyles}>{t('account_info:user_account')}: </Box>
|
||||
<Box flex={1}>{userInfo?.username}</Box>
|
||||
</Flex>
|
||||
{feConfigs?.isPlus && (
|
||||
<Flex mt={6} alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('common:user.Password')}: </Box>
|
||||
<Box {...labelStyles}>{t('account_info:password')}: </Box>
|
||||
<Box flex={1}>*****</Box>
|
||||
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdatePsw}>
|
||||
{t('common:user.Change')}
|
||||
{t('account_info:change')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
{feConfigs?.isPlus && (
|
||||
<Flex mt={6} alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('common:user.Notification Receive')}: </Box>
|
||||
<Box {...labelStyles}>{t('account_info:notification_receiving')}: </Box>
|
||||
<Box
|
||||
flex={1}
|
||||
{...(!userInfo?.team.notificationAccount && userInfo?.permission.isOwner
|
||||
@@ -282,35 +294,35 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
{userInfo?.team.notificationAccount
|
||||
? userInfo?.team.notificationAccount
|
||||
: userInfo?.permission.isOwner
|
||||
? t('common:user.Notification Receive Bind')
|
||||
: t('user:notification.remind_owner_bind')}
|
||||
? t('account_info:please_bind_notification_receiving_path')
|
||||
: t('account_info:reminder_create_bound_notification_account')}
|
||||
</Box>
|
||||
|
||||
{userInfo?.permission.isOwner && (
|
||||
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdateNotification}>
|
||||
{t('common:user.Change')}
|
||||
{t('account_info:change')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex mt={6} alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('common:user.Team')}: </Box>
|
||||
<Box flex={1}>
|
||||
<TeamMenu />
|
||||
</Box>
|
||||
<Box {...labelStyles}>{t('account_info:user_team_team_name')}: </Box>
|
||||
<Flex flex={'1 0 0'} w={0} align={'center'}>
|
||||
<TeamSelector height={'28px'} w={'100%'} showManage />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{feConfigs?.isPlus && (userInfo?.team?.balance ?? 0) > 0 && (
|
||||
<Box mt={6} whiteSpace={'nowrap'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('common:user.team.Balance')}: </Box>
|
||||
<Box {...labelStyles}>{t('account_info:team_balance')}: </Box>
|
||||
<Box flex={1}>
|
||||
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>{' '}
|
||||
{t('user:bill.yuan')}
|
||||
{t('account_info:yuan')}
|
||||
</Box>
|
||||
|
||||
{userInfo?.permission.hasManagePer && !!standardPlan && (
|
||||
<Button variant={'primary'} size={'sm'} ml={5} onClick={onOpenConversionModal}>
|
||||
{t('user:bill.conversion')}
|
||||
{t('account_info:exchange')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -331,6 +343,7 @@ const PlanUsage = () => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, initUserInfo, teamPlanStatus } = useUserStore();
|
||||
const { subPlans } = useSystemStore();
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
@@ -340,6 +353,11 @@ const PlanUsage = () => {
|
||||
onClose: onCloseStandardModal,
|
||||
onOpen: onOpenStandardModal
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenAiPointsModal,
|
||||
onClose: onCloseAiPointsModal,
|
||||
onOpen: onOpenAiPointsModal
|
||||
} = useDisclosure();
|
||||
|
||||
const planName = useMemo(() => {
|
||||
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
|
||||
@@ -373,7 +391,7 @@ const PlanUsage = () => {
|
||||
return {
|
||||
colorScheme: 'green',
|
||||
value: 0,
|
||||
maxSize: t('common:common.Unlimited'),
|
||||
maxSize: t('account_info:unlimited'),
|
||||
usedSize: 0
|
||||
};
|
||||
}
|
||||
@@ -388,7 +406,7 @@ const PlanUsage = () => {
|
||||
return {
|
||||
colorScheme,
|
||||
value: rate * 100,
|
||||
maxSize: teamPlanStatus.datasetMaxSize || t('common:common.Unlimited'),
|
||||
maxSize: teamPlanStatus.datasetMaxSize || t('account_info:unlimited'),
|
||||
usedSize: teamPlanStatus.usedDatasetSize
|
||||
};
|
||||
}, [teamPlanStatus, t]);
|
||||
@@ -397,7 +415,7 @@ const PlanUsage = () => {
|
||||
return {
|
||||
colorScheme: 'green',
|
||||
value: 0,
|
||||
maxSize: t('common:common.Unlimited'),
|
||||
maxSize: t('account_info:unlimited'),
|
||||
usedSize: 0
|
||||
};
|
||||
}
|
||||
@@ -413,7 +431,7 @@ const PlanUsage = () => {
|
||||
return {
|
||||
colorScheme,
|
||||
value: rate * 100,
|
||||
max: teamPlanStatus.totalPoints ? teamPlanStatus.totalPoints : t('common:common.Unlimited'),
|
||||
max: teamPlanStatus.totalPoints ? teamPlanStatus.totalPoints : t('account_info:unlimited'),
|
||||
used: teamPlanStatus.usedPoints ? Math.round(teamPlanStatus.usedPoints) : 0
|
||||
};
|
||||
}, [teamPlanStatus, t]);
|
||||
@@ -423,13 +441,13 @@ const PlanUsage = () => {
|
||||
<Flex fontSize={['md', 'lg']} h={'30px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon mr={2} name={'support/account/plans'} w={'20px'} />
|
||||
{t('common:support.wallet.subscription.Team plan and usage')}
|
||||
{t('account_info:package_and_usage')}
|
||||
</Flex>
|
||||
<Button ml={4} size={'sm'} onClick={() => router.push(AI_POINT_USAGE_CARD_ROUTE)}>
|
||||
{t('common:support.user.Price')}
|
||||
<Button ml={4} size={'sm'} onClick={onOpenAiPointsModal}>
|
||||
{t('account_info:billing_standard')}
|
||||
</Button>
|
||||
<Button ml={4} variant={'whitePrimary'} size={'sm'} onClick={onOpenStandardModal}>
|
||||
{t('common:support.wallet.Standard Plan Detail')}
|
||||
{t('account_info:package_details')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box
|
||||
@@ -442,25 +460,33 @@ const PlanUsage = () => {
|
||||
<Flex px={[5, 7]} pt={[3, 6]}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<Box color={'myGray.600'} fontSize="sm">
|
||||
{t('common:support.wallet.subscription.Current plan')}
|
||||
{t('account_info:current_package')}
|
||||
</Box>
|
||||
<Box fontWeight={'bold'} fontSize="lg">
|
||||
{t(planName as any)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Button onClick={() => router.push('/price')} w={'8rem'} size="sm">
|
||||
{t('common:support.wallet.subscription.Upgrade plan')}
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push(
|
||||
subPlans?.planDescriptionUrl ? getDocPath(subPlans.planDescriptionUrl) : '/price'
|
||||
);
|
||||
}}
|
||||
w={'8rem'}
|
||||
size="sm"
|
||||
>
|
||||
{t('account_info:upgrade_package')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box px={[5, 7]} pb={[3, 6]}>
|
||||
{isFreeTeam && (
|
||||
<Box mt="2" color={'#485264'} fontSize="sm">
|
||||
{t('common:info.free_plan')}
|
||||
{t('account_info:account_knowledge_base_cleanup_warning')}
|
||||
</Box>
|
||||
)}
|
||||
{standardPlan.currentSubLevel !== StandardSubLevelEnum.free && (
|
||||
<Flex mt="2" color={'#485264'} fontSize="xs">
|
||||
<Box>{t('common:support.wallet.Plan expired time')}:</Box>
|
||||
<Box>{t('account_info:package_expiry_time')}:</Box>
|
||||
<Box ml={2}>{formatTime2YMD(standardPlan?.expiredTime)}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -488,14 +514,14 @@ const PlanUsage = () => {
|
||||
<Flex>
|
||||
<Flex flex={'1 0 0'} alignItems={'flex-end'}>
|
||||
<Box fontSize={'md'} fontWeight={'bold'} color={'myGray.900'}>
|
||||
{t('common:info.resource')}
|
||||
{t('account_info:resource_usage')}
|
||||
</Box>
|
||||
<Box ml={1} display={['none', 'block']} fontSize={'xs'} color={'myGray.500'}>
|
||||
{t('common:info.include')}
|
||||
{t('account_info:standard_package_and_extra_resource_package')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Link
|
||||
href={getWebReqUrl(EXTRA_PLAN_CARD_ROUTE)}
|
||||
href={getWebReqUrl(getExtraPlanCardRoute())}
|
||||
transform={'translateX(15px)'}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
@@ -503,7 +529,7 @@ const PlanUsage = () => {
|
||||
cursor={'pointer'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{t('common:info.buy_extra')}
|
||||
{t('account_info:purchase_extra_package')}
|
||||
<MyIcon ml={1} name={'common/rightArrowLight'} w={'12px'} />
|
||||
</Link>
|
||||
</Flex>
|
||||
@@ -511,7 +537,7 @@ const PlanUsage = () => {
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontWeight={'bold'} color={'myGray.900'}>
|
||||
{t('common:support.user.team.Dataset usage')}
|
||||
{t('account_info:knowledge_base_capacity')}
|
||||
</Box>
|
||||
<Box color={'myGray.600'} ml={2}>
|
||||
{datasetUsageMap.usedSize}/{datasetUsageMap.maxSize}
|
||||
@@ -535,12 +561,9 @@ const PlanUsage = () => {
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontWeight={'bold'} color={'myGray.900'}>
|
||||
{t('common:support.wallet.subscription.AI points usage')}
|
||||
{t('account_info:ai_points_usage')}
|
||||
</Box>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.wallet.subscription.AI points usage tip')}
|
||||
></QuestionTip>
|
||||
<QuestionTip ml={1} label={t('account_info:ai_points_usage_tip')}></QuestionTip>
|
||||
<Box color={'myGray.600'} ml={2}>
|
||||
{aiPointsUsageMap.used}/{aiPointsUsageMap.max}
|
||||
</Box>
|
||||
@@ -561,6 +584,7 @@ const PlanUsage = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
{isOpenStandardModal && <StandDetailModal onClose={onCloseStandardModal} />}
|
||||
{isOpenAiPointsModal && <AiPointsModal onClose={onCloseAiPointsModal} />}
|
||||
</Box>
|
||||
) : null;
|
||||
};
|
||||
@@ -570,7 +594,9 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
const { toast } = useToast();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
@@ -586,12 +612,26 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
});
|
||||
reset(data);
|
||||
toast({
|
||||
title: t('common:dataset.data.Update Success Tip'),
|
||||
title: t('account_info:update_success_tip'),
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
[reset, toast, updateUserInfo]
|
||||
[reset, t, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
const buttonStyles = useRef<FlexProps>({
|
||||
bg: 'white',
|
||||
py: 3,
|
||||
px: 6,
|
||||
border: theme.borders.sm,
|
||||
borderWidth: '1.5px',
|
||||
borderRadius: 'md',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
fontSize: 'sm'
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Grid gridGap={4} mt={3}>
|
||||
@@ -613,50 +653,32 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
>
|
||||
<MyIcon name={'common/courseLight'} w={'18px'} color={'myGray.600'} />
|
||||
<Box ml={2} flex={1}>
|
||||
{t('common:system.Help Document')}
|
||||
</Box>
|
||||
</Link>
|
||||
)}
|
||||
{feConfigs?.chatbotUrl && (
|
||||
<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'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<MyIcon name={'core/app/aiLight'} w={'18px'} />
|
||||
<Box ml={2} flex={1}>
|
||||
{t('common:common.system.Help Chatbot')}
|
||||
{t('account_info:help_document')}
|
||||
</Box>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{!isPc &&
|
||||
feConfigs?.navbarItems
|
||||
?.filter((item) => item.isActive)
|
||||
.map((item) => (
|
||||
<Flex
|
||||
key={item.id}
|
||||
{...buttonStyles.current}
|
||||
onClick={() => window.open(item.url, '_blank')}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'18px'} />
|
||||
<Box ml={2} flex={1}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
{feConfigs?.lafEnv && userInfo?.team.role === TeamMemberRoleEnum.owner && (
|
||||
<Flex
|
||||
bg={'white'}
|
||||
py={3}
|
||||
px={6}
|
||||
border={theme.borders.sm}
|
||||
borderWidth={'1.5px'}
|
||||
borderRadius={'md'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
onClick={onOpenLaf}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex {...buttonStyles.current} onClick={onOpenLaf}>
|
||||
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
|
||||
<Box ml={2} flex={1}>
|
||||
{'laf' + t('common:navbar.Account')}
|
||||
{'laf' + t('account_info:account_duplicate')}
|
||||
</Box>
|
||||
<Box
|
||||
w={'9px'}
|
||||
@@ -668,22 +690,10 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
)}
|
||||
|
||||
{feConfigs?.show_openai_account && (
|
||||
<Flex
|
||||
bg={'white'}
|
||||
py={3}
|
||||
px={6}
|
||||
border={theme.borders.sm}
|
||||
borderWidth={'1.5px'}
|
||||
borderRadius={'md'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
onClick={onOpenOpenai}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex {...buttonStyles.current} onClick={onOpenOpenai}>
|
||||
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
|
||||
<Box ml={2} flex={1}>
|
||||
{'OpenAI / OneAPI' + t('common:navbar.Account')}
|
||||
{'OpenAI / OneAPI' + t('account_info:account_duplicate')}
|
||||
</Box>
|
||||
<Box
|
||||
w={'9px'}
|
||||
@@ -702,7 +712,7 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
h={'48px'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{t('common:system.Concat us')}
|
||||
{t('account_info:contact_us')}
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
107
projects/app/src/pages/account/inform.tsx
Normal file
107
projects/app/src/pages/account/inform.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { getInforms, readInform } from '@/web/support/user/inform/api';
|
||||
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import AccountContainer, { TabEnum } from './components/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
|
||||
const InformTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: informs,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination<UserInformSchema>({
|
||||
api: getInforms,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
return (
|
||||
<AccountContainer>
|
||||
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
|
||||
<Box px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
{informs.map((item) => (
|
||||
<Box
|
||||
key={item._id}
|
||||
border={theme.borders.md}
|
||||
py={2}
|
||||
px={4}
|
||||
borderRadius={'md'}
|
||||
position={'relative'}
|
||||
_notLast={{ mb: 3 }}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontWeight={'bold'}>{item.title}</Box>
|
||||
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
|
||||
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
|
||||
</Box>
|
||||
{!item.read && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={'xs'}
|
||||
onClick={async () => {
|
||||
if (!item.read) {
|
||||
await readInform(item._id);
|
||||
getData(pageNum);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('account_inform:read')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Box mt={2} fontSize={'sm'} color={'myGray.600'} whiteSpace={'pre-wrap'}>
|
||||
{item.content}
|
||||
</Box>
|
||||
{!item.read && (
|
||||
<>
|
||||
<Box
|
||||
w={'5px'}
|
||||
h={'5px'}
|
||||
borderRadius={'10px'}
|
||||
bg={'red.600'}
|
||||
position={'absolute'}
|
||||
top={'8px'}
|
||||
left={'8px'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!isLoading && informs.length === 0 && (
|
||||
<EmptyTip text={t('account_inform:no_notifications')}></EmptyTip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{total > pageSize && (
|
||||
<Flex w={'100%'} mt={4} px={[3, 8]} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading && informs.length === 0} fixed={false} />
|
||||
</Flex>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account_inform', 'account']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default InformTable;
|
||||
153
projects/app/src/pages/account/promotion.tsx
Normal file
153
projects/app/src/pages/account/promotion.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
Flex,
|
||||
BoxProps,
|
||||
useTheme,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPromotionInitData, getPromotionRecords } from '@/web/support/activity/promotion/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import dayjs from 'dayjs';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import AccountContainer, { TabEnum } from './components/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
|
||||
const Promotion = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const { userInfo } = useUserStore();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: promotionRecords,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination
|
||||
} = usePagination({
|
||||
api: getPromotionRecords,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
|
||||
['getPromotionInitData'],
|
||||
getPromotionInitData
|
||||
);
|
||||
|
||||
const statisticsStyles: BoxProps = {
|
||||
p: [4, 5],
|
||||
border: theme.borders.base,
|
||||
textAlign: 'center',
|
||||
fontSize: ['md', 'lg'],
|
||||
borderRadius: 'md'
|
||||
};
|
||||
const titleStyles: BoxProps = {
|
||||
mt: 2,
|
||||
fontSize: ['lg', '28px'],
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
return (
|
||||
<AccountContainer>
|
||||
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
|
||||
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('account_promotion:total_invited')}</Box>
|
||||
<Box {...titleStyles}>{invitedAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('account_promotion:earnings')}</Box>
|
||||
<Box {...titleStyles}>{earningsAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('account_promotion:cashback_ratio')}</Box>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('account_promotion:cashback_ratio_description')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('account_promotion:invite_url')}</Box>
|
||||
<QuestionTip ml={1} label={t('account_promotion:invite_url_tip')}></QuestionTip>
|
||||
</Flex>
|
||||
<Button
|
||||
mt={4}
|
||||
variant={'whitePrimary'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
|
||||
}}
|
||||
>
|
||||
{t('account_promotion:copy_invite_link')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box mt={5}>
|
||||
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('account_promotion:time')}</Th>
|
||||
<Th>{t('account_promotion:type')}</Th>
|
||||
<Th>{t('account_promotion:amount')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{promotionRecords.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{t(`user:promotion.${item.type}` as any)}</Td>
|
||||
<Td>{item.amount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{!isLoading && promotionRecords.length === 0 && (
|
||||
<EmptyTip text={t('account_promotion:no_invite_records')}></EmptyTip>
|
||||
)}
|
||||
{total > pageSize && (
|
||||
<Flex mt={4} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account', 'account_promotion']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Promotion;
|
||||
162
projects/app/src/pages/account/team/components/EditInfoModal.tsx
Normal file
162
projects/app/src/pages/account/team/components/EditInfoModal.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { compressImgFileAndUpload } from '@/web/common/file/controller';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api';
|
||||
import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
|
||||
export type EditTeamFormDataType = CreateTeamProps & {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const defaultForm = {
|
||||
name: '',
|
||||
avatar: DEFAULT_TEAM_AVATAR
|
||||
};
|
||||
|
||||
function EditModal({
|
||||
defaultData = defaultForm,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
defaultData?: EditTeamFormDataType;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { register, setValue, handleSubmit, watch } = useForm<CreateTeamProps>({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png,.svg',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const src = await compressImgFileAndUpload({
|
||||
type: MongoImageTypeEnum.teamAvatar,
|
||||
file,
|
||||
maxW: 300,
|
||||
maxH: 300
|
||||
});
|
||||
setValue('avatar', src);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, t('common:common.Select File Failed')),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[setValue, t, toast]
|
||||
);
|
||||
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (data: CreateTeamProps) => {
|
||||
return postCreateTeam(data);
|
||||
},
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
});
|
||||
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
|
||||
mutationFn: async (data: EditTeamFormDataType) => {
|
||||
if (!data.id) return Promise.resolve('');
|
||||
return putUpdateTeam({
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:common.Update Success'),
|
||||
errorToast: t('common:common.Update Failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc="support/team/group"
|
||||
iconColor="primary.600"
|
||||
title={defaultData.id ? t('user:team.Update Team') : t('user:team.Create Team')}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('user:team.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={3} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
autoFocus
|
||||
bg={'myWhite.600'}
|
||||
maxLength={20}
|
||||
placeholder={t('user:team.Team Name')}
|
||||
{...register('name', {
|
||||
required: t('common:common.Please Input Name')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{!!defaultData.id ? (
|
||||
<>
|
||||
<Box flex={1} />
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button isLoading={updating} onClick={handleSubmit((data) => onclickUpdate(data))}>
|
||||
{t('common:common.Confirm Update')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
w={'100%'}
|
||||
isLoading={creating}
|
||||
onClick={handleSubmit((data) => onclickCreate(data))}
|
||||
>
|
||||
{t('common:common.Confirm Create')}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
<File onSelect={onSelectFile} />
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(EditModal);
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Input, HStack, ModalBody, Button, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { compressImgFileAndUpload } from '@/web/common/file/controller';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
|
||||
export type GroupFormType = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
|
||||
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v);
|
||||
const { t } = useTranslation();
|
||||
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
|
||||
fileType: '.jpg, .jpeg, .png',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === editGroupId);
|
||||
}, [editGroupId, groups]);
|
||||
|
||||
const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({
|
||||
defaultValues: {
|
||||
name: group?.name || '',
|
||||
avatar: group?.avatar || DEFAULT_TEAM_AVATAR
|
||||
}
|
||||
});
|
||||
|
||||
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
|
||||
async (file: File[]) => {
|
||||
const src = await compressImgFileAndUpload({
|
||||
type: MongoImageTypeEnum.groupAvatar,
|
||||
file: file[0],
|
||||
maxW: 300,
|
||||
maxH: 300
|
||||
});
|
||||
return src;
|
||||
},
|
||||
{
|
||||
onSuccess: (src: string) => {
|
||||
setValue('avatar', src);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
|
||||
(data: GroupFormType) => {
|
||||
return postCreateGroup({
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
}
|
||||
);
|
||||
|
||||
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async (data: GroupFormType) => {
|
||||
if (!editGroupId) return;
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
name: data.name,
|
||||
avatar: data.avatar
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = isLoadingUpdate || isLoadingCreate || uploadingAvatar;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
>
|
||||
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
|
||||
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={getValues('avatar')}
|
||||
onClick={onOpenSelectAvatar}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Input
|
||||
bgColor="myGray.50"
|
||||
{...register('name', { required: true })}
|
||||
placeholder={t('user:team.group.name')}
|
||||
/>
|
||||
</HStack>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (editGroupId) {
|
||||
onUpdate(data);
|
||||
} else {
|
||||
onCreate(data);
|
||||
}
|
||||
})}
|
||||
>
|
||||
{editGroupId ? t('common:common.Save') : t('common:new_create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<AvatarSelect onSelect={onSelectAvatar} />
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupInfoModal;
|
||||
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
Box,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Checkbox,
|
||||
Grid,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
|
||||
export type GroupFormType = {
|
||||
members: {
|
||||
tmbId: string;
|
||||
role: `${GroupMemberRole}`;
|
||||
}[];
|
||||
};
|
||||
|
||||
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
|
||||
// 1. Owner can not be deleted, toast
|
||||
// 2. Owner/Admin can manage members
|
||||
// 3. Owner can add/remove admins
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { toast } = useToast();
|
||||
const [hoveredMemberId, setHoveredMemberId] = useState<string | undefined>(undefined);
|
||||
const {
|
||||
members: allMembers,
|
||||
refetchGroups,
|
||||
groups,
|
||||
refetchMembers
|
||||
} = useContextSelector(TeamContext, (v) => v);
|
||||
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === editGroupId);
|
||||
}, [editGroupId, groups]);
|
||||
|
||||
const [members, setMembers] = useState(group?.members || []);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
|
||||
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async () => {
|
||||
if (!editGroupId || !members.length) return;
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
memberList: members
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
|
||||
}
|
||||
);
|
||||
|
||||
const isSelected = (memberId: string) => {
|
||||
return members.find((item) => item.tmbId === memberId);
|
||||
};
|
||||
|
||||
const myRole = useMemo(() => {
|
||||
if (userInfo?.team.permission.hasManagePer) {
|
||||
return 'owner';
|
||||
}
|
||||
return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member';
|
||||
}, [members, userInfo]);
|
||||
|
||||
const handleToggleSelect = (memberId: string) => {
|
||||
if (
|
||||
myRole === 'owner' &&
|
||||
memberId === group?.members.find((item) => item.role === 'owner')?.tmbId
|
||||
) {
|
||||
toast({
|
||||
title: t('user:team.group.toast.can_not_delete_owner'),
|
||||
status: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
myRole === 'admin' &&
|
||||
group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected(memberId)) {
|
||||
setMembers(members.filter((item) => item.tmbId !== memberId));
|
||||
} else {
|
||||
setMembers([...members, { tmbId: memberId, role: 'member' }]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleAdmin = (memberId: string) => {
|
||||
if (myRole === 'owner' && isSelected(memberId)) {
|
||||
const oldRole = members.find((item) => item.tmbId === memberId)?.role;
|
||||
if (oldRole === 'admin') {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
|
||||
);
|
||||
} else {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isLoadingUpdate;
|
||||
return (
|
||||
<MyModal
|
||||
onClose={onClose}
|
||||
title={t('user:team.group.manage_member')}
|
||||
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
|
||||
iconColor="primary.600"
|
||||
minW="800px"
|
||||
h={'100%'}
|
||||
isCentered
|
||||
>
|
||||
<ModalBody flex={1}>
|
||||
<Grid
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
borderRadius="0.5rem"
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex flexDirection="column" p="4">
|
||||
<SearchInput
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
bg={'myGray.50'}
|
||||
onChange={(e) => {
|
||||
setSearchKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
|
||||
{filtered.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
|
||||
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
|
||||
{members.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
onMouseEnter={() => setHoveredMemberId(member.tmbId)}
|
||||
onMouseLeave={() => setHoveredMemberId(undefined)}
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={member.tmbId + member.role}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar
|
||||
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar}
|
||||
w="1.5rem"
|
||||
borderRadius={'md'}
|
||||
/>
|
||||
<Box>
|
||||
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box mr="auto">
|
||||
{(() => {
|
||||
if (member.role === 'owner') {
|
||||
return (
|
||||
<Tag ml={2} colorSchema="gray">
|
||||
{t('user:team.group.role.owner')}
|
||||
</Tag>
|
||||
);
|
||||
} else if (member.role === 'admin') {
|
||||
return (
|
||||
<Tag ml={2} mr="auto">
|
||||
{t('user:team.group.role.admin')}
|
||||
{myRole === 'owner' && (
|
||||
<MyIcon
|
||||
ml={1}
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleAdmin(member.tmbId)}
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
} else if (member.role === 'member') {
|
||||
return (
|
||||
myRole === 'owner' &&
|
||||
hoveredMemberId === member.tmbId && (
|
||||
<Tag
|
||||
ml={2}
|
||||
colorSchema="yellow"
|
||||
cursor={'pointer'}
|
||||
onClick={() => handleToggleAdmin(member.tmbId)}
|
||||
>
|
||||
{t('user:team.group.set_as_admin')}
|
||||
</Tag>
|
||||
)
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Box>
|
||||
{(myRole === 'owner' || (myRole === 'admin' && member.role === 'member')) && (
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(member.tmbId)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter alignItems="flex-end">
|
||||
<Button isLoading={isLoading} onClick={onUpdate}>
|
||||
{t('common:common.Save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default GroupEditModal;
|
||||
@@ -0,0 +1,196 @@
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Checkbox
|
||||
} from '@chakra-ui/react';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { TeamContext } from '../context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
|
||||
export type ChangeOwnerModalProps = {
|
||||
groupId: string;
|
||||
};
|
||||
|
||||
export function ChangeOwnerModal({
|
||||
onClose,
|
||||
groupId
|
||||
}: ChangeOwnerModalProps & { onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const { members: allMembers, groups, refetchGroups } = useContextSelector(TeamContext, (v) => v);
|
||||
const group = useMemo(() => {
|
||||
return groups.find((item) => item._id === groupId);
|
||||
}, [groupId, groups]);
|
||||
|
||||
const memberList = allMembers.filter((item) => {
|
||||
return item.memberName.toLowerCase().includes(inputValue.toLowerCase());
|
||||
});
|
||||
|
||||
const OldOwnerId = useMemo(() => {
|
||||
return group?.members.find((item) => item.role === 'owner')?.tmbId;
|
||||
}, [group]);
|
||||
|
||||
const [keepAdmin, setKeepAdmin] = useState(true);
|
||||
|
||||
const {
|
||||
isOpen: isOpenMemberListMenu,
|
||||
onClose: onCloseMemberListMenu,
|
||||
onOpen: onOpenMemberListMenu
|
||||
} = useDisclosure();
|
||||
|
||||
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
|
||||
|
||||
const onChangeOwner = async (tmbId: string) => {
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newMemberList = group.members
|
||||
.map((item) => {
|
||||
if (item.tmbId === OldOwnerId) {
|
||||
if (keepAdmin) {
|
||||
return { tmbId: OldOwnerId, role: 'admin' };
|
||||
}
|
||||
return { tmbId: OldOwnerId, role: 'member' };
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.tmbId !== tmbId) as any;
|
||||
|
||||
newMemberList.push({ tmbId, role: 'owner' });
|
||||
|
||||
return putUpdateGroup({
|
||||
groupId,
|
||||
memberList: newMemberList
|
||||
});
|
||||
};
|
||||
|
||||
const { runAsync, loading } = useRequest2(onChangeOwner, {
|
||||
onSuccess: () => Promise.all([onClose(), refetchGroups()]),
|
||||
successToast: t('common:permission.change_owner_success'),
|
||||
errorToast: t('common:permission.change_owner_failed')
|
||||
});
|
||||
|
||||
const onConfirm = async () => {
|
||||
if (!selectedMember) {
|
||||
return;
|
||||
}
|
||||
await runAsync(selectedMember.tmbId);
|
||||
};
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="modal/changePer"
|
||||
iconColor="primary.600"
|
||||
onClose={onClose}
|
||||
title={t('common:permission.change_owner')}
|
||||
isLoading={loading}
|
||||
>
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
<Avatar src={group?.avatar} w={'1.75rem'} borderRadius={'md'} />
|
||||
<Box>{group?.name}</Box>
|
||||
</HStack>
|
||||
<Flex mt={4} justify="start" flexDirection="column">
|
||||
<Box fontSize="14px" fontWeight="500" color="myGray.900">
|
||||
{t('common:permission.change_owner_to')}
|
||||
</Box>
|
||||
<Flex mt="4" alignItems="center" position={'relative'}>
|
||||
{selectedMember && (
|
||||
<Avatar
|
||||
src={selectedMember.avatar}
|
||||
w={'20px'}
|
||||
borderRadius={'md'}
|
||||
position="absolute"
|
||||
left={3}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
placeholder={t('common:permission.change_owner_placeholder')}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
onFocus={() => {
|
||||
onOpenMemberListMenu();
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
{...(selectedMember && { pl: '10' })}
|
||||
/>
|
||||
</Flex>
|
||||
{isOpenMemberListMenu && memberList.length > 0 && (
|
||||
<Flex
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
flexDirection={'column'}
|
||||
gap={2}
|
||||
p={1}
|
||||
boxShadow="lg"
|
||||
bg="white"
|
||||
borderRadius="md"
|
||||
zIndex={10}
|
||||
maxH={'300px'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
{memberList.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setInputValue(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Box mt="4">
|
||||
<Checkbox
|
||||
isChecked={keepAdmin}
|
||||
onChange={(e) => {
|
||||
setKeepAdmin(e.target.checked);
|
||||
}}
|
||||
>
|
||||
{t('account_team:retain_admin_permissions')}
|
||||
</Checkbox>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<HStack>
|
||||
<Button onClick={onClose} variant={'whiteBase'}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeOwnerModal;
|
||||
@@ -0,0 +1,217 @@
|
||||
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { deleteGroup } from '@/web/support/user/team/group/api';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useState } from 'react';
|
||||
|
||||
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
|
||||
|
||||
function MemberTable({
|
||||
onEditGroup,
|
||||
onManageMember
|
||||
}: {
|
||||
onEditGroup: (groupId: string) => void;
|
||||
onManageMember: (groupId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const [editGroupId, setEditGroupId] = useState<string>();
|
||||
|
||||
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
|
||||
type: 'delete',
|
||||
content: t('account_team:confirm_delete_group')
|
||||
});
|
||||
|
||||
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
|
||||
TeamContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
|
||||
onSuccess: () => {
|
||||
refetchGroups();
|
||||
refetchMembers();
|
||||
}
|
||||
});
|
||||
|
||||
const hasGroupManagePer = (group: (typeof groups)[0]) =>
|
||||
userInfo?.team.permission.hasManagePer ||
|
||||
['admin', 'owner'].includes(
|
||||
group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? ''
|
||||
);
|
||||
|
||||
const isGroupOwner = (group: (typeof groups)[0]) =>
|
||||
userInfo?.team.permission.hasManagePer ||
|
||||
group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId;
|
||||
|
||||
const {
|
||||
isOpen: isOpenChangeOwner,
|
||||
onOpen: onOpenChangeOwner,
|
||||
onClose: onCloseChangeOwner
|
||||
} = useDisclosure();
|
||||
|
||||
const onChangeOwner = (groupId: string) => {
|
||||
setEditGroupId(groupId);
|
||||
onOpenChangeOwner();
|
||||
};
|
||||
|
||||
return (
|
||||
<MyBox>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'}>
|
||||
<Table overflow={'unset'}>
|
||||
<Thead>
|
||||
<Tr bg={'white !important'}>
|
||||
<Th bg="myGray.100" borderLeftRadius="6px">
|
||||
{t('account_team:group_name')}
|
||||
</Th>
|
||||
<Th bg="myGray.100">{t('account_team:owner')}</Th>
|
||||
<Th bg="myGray.100">{t('account_team:member')}</Th>
|
||||
<Th bg="myGray.100" borderRightRadius="6px">
|
||||
{t('common:common.Action')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{groups?.map((group) => (
|
||||
<Tr key={group._id} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
avatar={group.avatar}
|
||||
/>
|
||||
<Box>
|
||||
({group.name === DefaultGroupName ? members.length : group.members.length})
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td>
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName
|
||||
? members.find((item) => item.role === 'owner')?.memberName ?? ''
|
||||
: members.find(
|
||||
(item) =>
|
||||
item.tmbId ===
|
||||
group.members.find((item) => item.role === 'owner')?.tmbId
|
||||
)?.memberName ?? ''
|
||||
}
|
||||
avatar={
|
||||
group.name === DefaultGroupName
|
||||
? members.find((item) => item.role === 'owner')?.avatar ?? ''
|
||||
: members.find(
|
||||
(i) =>
|
||||
i.tmbId === group.members.find((item) => item.role === 'owner')?.tmbId
|
||||
)?.avatar ?? ''
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{group.name === DefaultGroupName ? (
|
||||
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
|
||||
) : hasGroupManagePer(group) ? (
|
||||
<MyTooltip label={t('account_team:manage_member')}>
|
||||
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
|
||||
<AvatarGroup
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
groupId={group._id}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
) : (
|
||||
<AvatarGroup
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
groupId={group._id}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
|
||||
<MyMenu
|
||||
Button={<MyIcon name={'edit'} cursor={'pointer'} w="1rem" />}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: t('account_team:edit_info'),
|
||||
icon: 'edit',
|
||||
onClick: () => {
|
||||
onEditGroup(group._id);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('account_team:manage_member'),
|
||||
icon: 'support/team/group',
|
||||
onClick: () => {
|
||||
onManageMember(group._id);
|
||||
}
|
||||
},
|
||||
...(isGroupOwner(group)
|
||||
? [
|
||||
{
|
||||
label: t('account_team:transfer_ownership'),
|
||||
icon: 'modal/changePer',
|
||||
onClick: () => {
|
||||
onChangeOwner(group._id);
|
||||
},
|
||||
type: 'primary' as MenuItemType
|
||||
},
|
||||
{
|
||||
label: t('common:common.Delete'),
|
||||
icon: 'delete',
|
||||
onClick: () => {
|
||||
openDeleteGroupModal(() => delDeleteGroup(group._id))();
|
||||
},
|
||||
type: 'danger' as MenuItemType
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<ConfirmDeleteGroupModal />
|
||||
{isOpenChangeOwner && editGroupId && (
|
||||
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
|
||||
)}
|
||||
</MyBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberTable;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import TagTextarea from '@/components/common/Textarea/TagTextarea';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postInviteTeamMember } from '@/web/support/user/team/api';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d';
|
||||
|
||||
const InviteModal = ({
|
||||
teamId,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
teamId: string;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
title: t('user:team.Invite Member Result Tip'),
|
||||
showCancel: false
|
||||
});
|
||||
|
||||
const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
|
||||
|
||||
const { runAsync: onInvite, loading: isLoading } = useRequest2(
|
||||
() =>
|
||||
postInviteTeamMember({
|
||||
teamId,
|
||||
usernames: inviteUsernames
|
||||
}),
|
||||
{
|
||||
onSuccess(res: InviteMemberResponse) {
|
||||
onSuccess();
|
||||
openConfirm(
|
||||
() => onClose(),
|
||||
undefined,
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
{t('user:team.Invite Member Success Tip', {
|
||||
success: res.invite.length,
|
||||
inValid: res.inValid.map((item) => item.username).join(', '),
|
||||
inTeam: res.inTeam.map((item) => item.username).join(', ')
|
||||
})}
|
||||
</Box>
|
||||
)();
|
||||
},
|
||||
errorToast: t('user:team.Invite Member Failed Tip')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="common/inviteLight"
|
||||
iconColor="primary.600"
|
||||
title={
|
||||
<Box>
|
||||
<Box>{t('common:user.team.Invite Member')}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
|
||||
{t('common:user.team.Invite Member Tips')}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
maxW={['90vw', '400px']}
|
||||
overflow={'unset'}
|
||||
>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<ModalBody>
|
||||
<Box mb={2}>{t('common:user.Account')}</Box>
|
||||
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
w={'100%'}
|
||||
h={'34px'}
|
||||
isDisabled={inviteUsernames.length === 0}
|
||||
isLoading={isLoading}
|
||||
onClick={onInvite}
|
||||
>
|
||||
{t('user:team.Confirm Invite')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<ConfirmModal />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteModal;
|
||||
107
projects/app/src/pages/account/team/components/MemberTable.tsx
Normal file
107
projects/app/src/pages/account/team/components/MemberTable.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { Box, HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { delRemoveMember } from '@/web/support/user/team/api';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import GroupTags from '@/components/support/permission/Group/GroupTags';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from './context';
|
||||
|
||||
function MemberTable() {
|
||||
const { userInfo } = useUserStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
|
||||
TeamContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
return (
|
||||
<MyBox>
|
||||
<TableContainer overflow={'unset'} fontSize={'sm'}>
|
||||
<Table overflow={'unset'}>
|
||||
<Thead>
|
||||
<Tr bgColor={'white !important'}>
|
||||
<Th borderLeftRadius="6px" bgColor="myGray.100">
|
||||
{t('account_team:user_name')}
|
||||
</Th>
|
||||
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
|
||||
<Th borderRightRadius="6px" bgColor="myGray.100">
|
||||
{t('common:common.Action')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{members?.map((item) => (
|
||||
<Tr key={item.userId} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{item.memberName}
|
||||
{item.status === 'waiting' && (
|
||||
<Tag ml="2" colorSchema="yellow">
|
||||
{t('account_team:waiting')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td maxW={'300px'}>
|
||||
<GroupTags
|
||||
names={groups
|
||||
?.filter((group) => group.members.map((m) => m.tmbId).includes(item.tmbId))
|
||||
.map((g) => g.name)}
|
||||
max={3}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
item.role !== TeamMemberRoleEnum.owner &&
|
||||
item.tmbId !== userInfo?.team.tmbId && (
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRemoveMember(
|
||||
() =>
|
||||
delRemoveMember(item.tmbId).then(() =>
|
||||
Promise.all([refetchGroups(), refetchMembers()])
|
||||
),
|
||||
undefined,
|
||||
t('account_team:remove_tip', {
|
||||
username: item.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
<ConfirmRemoveMemberModal />
|
||||
</TableContainer>
|
||||
</MyBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemberTable;
|
||||
@@ -0,0 +1,287 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTeamClbs, updateMemberPermission } from '@/web/support/user/team/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
|
||||
import { TeamContext } from '../context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import {
|
||||
TeamManagePermissionVal,
|
||||
TeamWritePermissionVal
|
||||
} from '@fastgpt/global/support/permission/user/constant';
|
||||
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
|
||||
import { useCreation } from 'ahooks';
|
||||
|
||||
function PermissionManage() {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
|
||||
TeamContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const { runAsync: refetchClbs, data: clbs = [] } = useRequest2(getTeamClbs, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const filteredGroups = useCreation(
|
||||
() => groups?.filter((group) => group.name.toLowerCase().includes(searchKey.toLowerCase())),
|
||||
[groups, searchKey]
|
||||
);
|
||||
const filteredMembers = useCreation(
|
||||
() =>
|
||||
members
|
||||
?.filter((member) => member.memberName.toLowerCase().includes(searchKey.toLowerCase()))
|
||||
.map((member) => {
|
||||
const clb = clbs?.find((clb) => String(clb.tmbId) === String(member.tmbId));
|
||||
const permission =
|
||||
member.role === 'owner'
|
||||
? new TeamPermission({ isOwner: true })
|
||||
: new TeamPermission({ per: clb?.permission });
|
||||
|
||||
return { ...member, permission };
|
||||
}),
|
||||
[clbs, members, searchKey]
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateMemberPermission } = useRequest2(updateMemberPermission, {
|
||||
onSuccess: () => {
|
||||
refetchGroups();
|
||||
refetchMembers();
|
||||
refetchClbs();
|
||||
}
|
||||
});
|
||||
|
||||
const { runAsync: onAddPermission, loading: addLoading } = useRequest2(
|
||||
async ({
|
||||
groupId,
|
||||
memberId,
|
||||
per
|
||||
}: {
|
||||
groupId?: string;
|
||||
memberId?: string;
|
||||
per: 'write' | 'manage';
|
||||
}) => {
|
||||
if (groupId) {
|
||||
const group = groups?.find((group) => group._id === groupId);
|
||||
if (group) {
|
||||
const permission = new TeamPermission({ per: group.permission.value });
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.addPer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.addPer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (memberId) {
|
||||
const member = filteredMembers?.find((member) => String(member.tmbId) === memberId);
|
||||
if (member) {
|
||||
const permission = new TeamPermission({ per: member.permission.value });
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.addPer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.addPer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onRemovePermission, loading: removeLoading } = useRequest2(
|
||||
async ({
|
||||
groupId,
|
||||
memberId,
|
||||
per
|
||||
}: {
|
||||
groupId?: string;
|
||||
memberId?: string;
|
||||
per: 'write' | 'manage';
|
||||
}) => {
|
||||
if (groupId) {
|
||||
const group = groups?.find((group) => group._id === groupId);
|
||||
if (group) {
|
||||
const permission = new TeamPermission({ per: group.permission.value });
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.removePer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.removePer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
groupId: group._id,
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (memberId) {
|
||||
const member = members?.find((member) => String(member.tmbId) === memberId);
|
||||
if (member) {
|
||||
const permission = new TeamPermission({ per: member.permission.value }); // Hint: member.permission is read-only
|
||||
switch (per) {
|
||||
case 'write':
|
||||
permission.removePer(TeamWritePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
case 'manage':
|
||||
permission.removePer(TeamManagePermissionVal);
|
||||
return onUpdateMemberPermission({
|
||||
memberId: String(member.tmbId),
|
||||
permission: permission.value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const userManage = userInfo?.permission.hasManagePer;
|
||||
|
||||
return (
|
||||
<TableContainer fontSize={'sm'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr bg={'white !important'}>
|
||||
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
|
||||
{t('user:team.group.group')} / {t('user:team.group.members')}
|
||||
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
|
||||
</Th>
|
||||
<Th bg="myGray.100">
|
||||
<Box mx="auto" w="fit-content">
|
||||
{t('user:team.group.permission.write')}
|
||||
</Box>
|
||||
</Th>
|
||||
<Th bg="myGray.100" borderRightRadius="md">
|
||||
<Box mx="auto" w="fit-content">
|
||||
{t('user:team.group.permission.manage')}
|
||||
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
|
||||
</Box>
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filteredGroups?.map((group) => (
|
||||
<Tr key={group._id} overflow={'unset'} border="none">
|
||||
<Td border="none">
|
||||
<MemberTag
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
avatar={group.avatar}
|
||||
/>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={!userManage}
|
||||
isChecked={group.permission.hasWritePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ groupId: group._id, per: 'write' })
|
||||
: onRemovePermission({ groupId: group._id, per: 'write' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={!userInfo?.permission.isOwner}
|
||||
isChecked={group.permission.hasManagePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ groupId: group._id, per: 'manage' })
|
||||
: onRemovePermission({ groupId: group._id, per: 'manage' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{filteredGroups?.length > 0 && filteredMembers?.length > 0 && (
|
||||
<Tr borderBottom={'1px solid'} borderColor={'myGray.300'} />
|
||||
)}
|
||||
{filteredMembers?.map((member) => (
|
||||
<Tr key={member.tmbId} overflow={'unset'} border="none">
|
||||
<Td border="none">
|
||||
<HStack>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>{member.memberName}</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={member.permission.isOwner || !userManage}
|
||||
isChecked={member.permission.hasWritePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ memberId: String(member.tmbId), per: 'write' })
|
||||
: onRemovePermission({ memberId: String(member.tmbId), per: 'write' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<Box mx="auto" w="fit-content">
|
||||
<Checkbox
|
||||
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
|
||||
isChecked={member.permission.hasManagePer}
|
||||
onChange={(e) =>
|
||||
e.target.checked
|
||||
? onAddPermission({ memberId: String(member.tmbId), per: 'manage' })
|
||||
: onRemovePermission({ memberId: String(member.tmbId), per: 'manage' })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default PermissionManage;
|
||||
266
projects/app/src/pages/account/team/components/SelectMember.tsx
Normal file
266
projects/app/src/pages/account/team/components/SelectMember.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Checkbox, Flex, Grid, HStack } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
|
||||
type memberType = {
|
||||
type: 'member';
|
||||
tmbId: string;
|
||||
memberName: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type groupType = {
|
||||
type: 'group';
|
||||
_id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
type selectedType = {
|
||||
member: string[];
|
||||
group: string[];
|
||||
};
|
||||
|
||||
function SelectMember({
|
||||
allMembers,
|
||||
selected = { member: [], group: [] },
|
||||
setSelected
|
||||
// mode = 'both'
|
||||
}: {
|
||||
allMembers: {
|
||||
member: memberType[];
|
||||
group: groupType[];
|
||||
};
|
||||
selected?: selectedType;
|
||||
setSelected: React.Dispatch<React.SetStateAction<selectedType>>;
|
||||
mode?: 'member' | 'group' | 'both';
|
||||
}) {
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.member.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
}),
|
||||
...allMembers.group.filter((member) => {
|
||||
if (member.name.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
|
||||
const selectedFlated = useMemo(() => {
|
||||
return [
|
||||
...allMembers.member.filter((member) => {
|
||||
return selected.member?.includes(member.tmbId);
|
||||
}),
|
||||
...allMembers.group.filter((member) => {
|
||||
return selected.group?.includes(member._id);
|
||||
})
|
||||
];
|
||||
}, [selected, allMembers]);
|
||||
|
||||
const handleToggleSelect = (member: memberType | groupType) => {
|
||||
if (member.type == 'member') {
|
||||
if (selected.member?.indexOf(member.tmbId) == -1) {
|
||||
setSelected({
|
||||
member: [...selected.member, member.tmbId],
|
||||
group: [...selected.group]
|
||||
});
|
||||
} else {
|
||||
setSelected({
|
||||
member: [...selected.member.filter((item) => item != member.tmbId)],
|
||||
group: [...selected.group]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selected.group?.indexOf(member._id) == -1) {
|
||||
setSelected({ member: [...selected.member], group: [...selected.group, member._id] });
|
||||
} else {
|
||||
setSelected({
|
||||
member: [...selected.member],
|
||||
group: [...selected.group.filter((item) => item != member._id)]
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (member: memberType | groupType) => {
|
||||
if (member.type == 'member') {
|
||||
return selected.member?.includes(member.tmbId);
|
||||
} else {
|
||||
return selected.group?.includes(member._id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid
|
||||
templateColumns="1fr 1fr"
|
||||
borderRadius="8px"
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex flexDirection="column" p="4" h={'100%'} overflow={'auto'}>
|
||||
<SearchInput
|
||||
placeholder={t('user:search_user')}
|
||||
fontSize="sm"
|
||||
bg={'myGray.50'}
|
||||
onChange={(e) => {
|
||||
setSearchKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Flex flexDirection="column" mt={3}>
|
||||
{filtered.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
key={member.type == 'member' ? member.tmbId : member._id}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_notLast={{ mb: 2 }}
|
||||
onClick={() => handleToggleSelect(member)}
|
||||
>
|
||||
<Checkbox
|
||||
isChecked={!!isSelected(member)}
|
||||
icon={<MyIcon name={'common/check'} w={'12px'} />}
|
||||
/>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<Box>
|
||||
{member.type == 'member'
|
||||
? member.memberName
|
||||
: member.name === DefaultGroupName
|
||||
? userInfo?.team.teamName
|
||||
: member.name}
|
||||
</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex
|
||||
borderLeft="1px"
|
||||
borderColor="myGray.200"
|
||||
flexDirection="column"
|
||||
p="4"
|
||||
h={'100%'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
<Box mt={3}>
|
||||
{t('common:chosen') + ': ' + Number(selected.member.length + selected.group.length)}{' '}
|
||||
</Box>
|
||||
<Box mt={5}>
|
||||
{selectedFlated.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
justifyContent="space-between"
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
key={member.type == 'member' ? member.tmbId : member._id}
|
||||
_hover={{ bg: 'myGray.50' }}
|
||||
_notLast={{ mb: 2 }}
|
||||
>
|
||||
<Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
|
||||
<Box w="full">
|
||||
{member.type == 'member'
|
||||
? member.memberName
|
||||
: member.name === DefaultGroupName
|
||||
? userInfo?.team.teamName
|
||||
: member.name}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name={'common/closeLight'}
|
||||
w={'1rem'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => handleToggleSelect(member)}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
// This function is for using with react-hook-form
|
||||
function ControllerWrapper({
|
||||
control,
|
||||
allMembers,
|
||||
mode = 'both',
|
||||
name = 'members'
|
||||
}: {
|
||||
control: Control;
|
||||
allMembers: RequireAtLeastOne<{ member?: memberType[]; group?: groupType[] }>;
|
||||
mode?: 'member' | 'group' | 'both';
|
||||
name?: string;
|
||||
}) {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { value: selected, onChange } }) => (
|
||||
<SelectMember
|
||||
mode={mode}
|
||||
allMembers={
|
||||
(() => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
return { member: allMembers.member, group: [] };
|
||||
case 'group':
|
||||
return { member: [], group: allMembers.group };
|
||||
case 'both':
|
||||
return { member: allMembers.member, group: allMembers.group };
|
||||
}
|
||||
})() as Required<typeof allMembers>
|
||||
}
|
||||
selected={(() => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
return { member: selected, group: [] };
|
||||
case 'group':
|
||||
return { member: [], group: selected };
|
||||
case 'both':
|
||||
return { member: selected.member, group: selected.group };
|
||||
}
|
||||
})()}
|
||||
setSelected={
|
||||
(({ member, group }: selectedType, _prevState: selectedType) => {
|
||||
switch (mode) {
|
||||
case 'member':
|
||||
onChange(member);
|
||||
return;
|
||||
case 'group':
|
||||
onChange(group);
|
||||
return;
|
||||
case 'both':
|
||||
onChange({ member, group });
|
||||
return;
|
||||
}
|
||||
}) as any // hack: we do not need to handle prevState
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const UnControlledSelectMember = SelectMember;
|
||||
export default ControllerWrapper;
|
||||
150
projects/app/src/pages/account/team/components/context.tsx
Normal file
150
projects/app/src/pages/account/team/components/context.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import type { EditTeamFormDataType } from './EditInfoModal';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getGroupList } from '@/web/support/user/team/group/api';
|
||||
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
|
||||
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
|
||||
|
||||
type TeamModalContextType = {
|
||||
myTeams: TeamTmbItemType[];
|
||||
members: TeamMemberItemType[];
|
||||
groups: MemberGroupListType;
|
||||
isLoading: boolean;
|
||||
onSwitchTeam: (teamId: string) => void;
|
||||
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
|
||||
|
||||
refetchMembers: () => void;
|
||||
refetchTeams: () => void;
|
||||
refetchGroups: () => void;
|
||||
searchKey: string;
|
||||
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
|
||||
teamSize: number;
|
||||
};
|
||||
|
||||
export const TeamContext = createContext<TeamModalContextType>({
|
||||
myTeams: [],
|
||||
groups: [],
|
||||
members: [],
|
||||
isLoading: false,
|
||||
onSwitchTeam: function (_teamId: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setEditTeamData: function (_value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchTeams: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchMembers: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchGroups: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
|
||||
searchKey: '',
|
||||
setSearchKey: function (_value: React.SetStateAction<string>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
teamSize: 0
|
||||
});
|
||||
|
||||
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
|
||||
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const {
|
||||
data: myTeams = [],
|
||||
loading: isLoadingTeams,
|
||||
refresh: refetchTeams
|
||||
} = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?._id]
|
||||
});
|
||||
|
||||
// member action
|
||||
const {
|
||||
data: members = [],
|
||||
runAsync: refetchMembers,
|
||||
loading: loadingMembers
|
||||
} = useRequest2(
|
||||
() => {
|
||||
if (!userInfo?.team?.teamId) return Promise.resolve([]);
|
||||
return loadAndGetTeamMembers(true);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
|
||||
async (teamId: string) => {
|
||||
await putSwitchTeam(teamId);
|
||||
return initUserInfo();
|
||||
},
|
||||
{
|
||||
errorToast: t('common:user.team.Switch Team Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: groups = [],
|
||||
loading: isLoadingGroups,
|
||||
refresh: refetchGroups
|
||||
} = useRequest2(getGroupList, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
|
||||
|
||||
const contextValue = {
|
||||
myTeams,
|
||||
refetchTeams,
|
||||
isLoading,
|
||||
onSwitchTeam,
|
||||
searchKey,
|
||||
setSearchKey,
|
||||
|
||||
// create | update team
|
||||
setEditTeamData,
|
||||
members,
|
||||
refetchMembers,
|
||||
groups,
|
||||
refetchGroups,
|
||||
teamSize: members.length
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamContext.Provider value={contextValue}>
|
||||
{userInfo?.team?.permission && (
|
||||
<>
|
||||
{children}
|
||||
{!!editTeamData && (
|
||||
<EditInfoModal
|
||||
defaultData={editTeamData}
|
||||
onClose={() => setEditTeamData(undefined)}
|
||||
onSuccess={() => {
|
||||
refetchTeams();
|
||||
initUserInfo();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TeamContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamModalContextProvider;
|
||||
329
projects/app/src/pages/account/team/index.tsx
Normal file
329
projects/app/src/pages/account/team/index.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import AccountContainer from '../components/AccountContainer';
|
||||
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import TeamSelector from '../components/TeamSelector';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import React, { useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { useRouter } from 'next/router';
|
||||
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { delLeaveTeam } from '@/web/support/user/team/api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { TeamContext, TeamModalContextProvider } from './components/context';
|
||||
import dynamic from 'next/dynamic';
|
||||
import TeamTagModal from '@/components/support/user/team/TeamTagModal';
|
||||
import MemberTable from './components/MemberTable';
|
||||
|
||||
const InviteModal = dynamic(() => import('./components/InviteModal'));
|
||||
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
|
||||
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
|
||||
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
|
||||
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
|
||||
|
||||
export enum TeamTabEnum {
|
||||
member = 'member',
|
||||
group = 'group',
|
||||
permission = 'permission'
|
||||
}
|
||||
|
||||
const Team = () => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { userInfo, teamPlanStatus } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const {
|
||||
myTeams,
|
||||
refetchTeams,
|
||||
members,
|
||||
refetchMembers,
|
||||
setEditTeamData,
|
||||
onSwitchTeam,
|
||||
searchKey,
|
||||
setSearchKey,
|
||||
teamSize,
|
||||
isLoading
|
||||
} = useContextSelector(TeamContext, (v) => v);
|
||||
|
||||
const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
|
||||
|
||||
const {
|
||||
isOpen: isOpenTeamTagsAsync,
|
||||
onOpen: onOpenTeamTagsAsync,
|
||||
onClose: onCloseTeamTagsAsync
|
||||
} = useDisclosure();
|
||||
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenGroupInfo,
|
||||
onOpen: onOpenGroupInfo,
|
||||
onClose: onCloseGroupInfo
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenManageGroupMember,
|
||||
onOpen: onOpenManageGroupMember,
|
||||
onClose: onCloseManageGroupMember
|
||||
} = useDisclosure();
|
||||
|
||||
const { runAsync: onLeaveTeam, loading: isLoadingLeaveTeam } = useRequest2(
|
||||
async (teamId?: string) => {
|
||||
if (!teamId) return;
|
||||
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
|
||||
// change to personal team
|
||||
// get members
|
||||
onSwitchTeam(defaultTeam.teamId);
|
||||
return delLeaveTeam(teamId);
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
refetchTeams();
|
||||
},
|
||||
errorToast: t('account_team:user_team_leave_team_failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
|
||||
content: t('account_team:confirm_leave_team')
|
||||
});
|
||||
|
||||
const [editGroupId, setEditGroupId] = useState<string>();
|
||||
const onEditGroup = (groupId: string) => {
|
||||
setEditGroupId(groupId);
|
||||
onOpenGroupInfo();
|
||||
};
|
||||
|
||||
const onManageMember = (groupId: string) => {
|
||||
setEditGroupId(groupId);
|
||||
onOpenManageGroupMember();
|
||||
};
|
||||
|
||||
return (
|
||||
<AccountContainer isLoading={isLoading}>
|
||||
{/* header */}
|
||||
<Flex
|
||||
w={'100%'}
|
||||
h={'3.5rem'}
|
||||
px={'1.56rem'}
|
||||
py={'0.56rem'}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'myGray.25'}
|
||||
align={'center'}
|
||||
gap={6}
|
||||
justify={'space-between'}
|
||||
>
|
||||
<Flex align={'center'}>
|
||||
<Flex gap={2} color={'myGray.900'}>
|
||||
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
|
||||
<Box fontWeight={'500'} fontSize={'1rem'}>
|
||||
{t('account:team')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex align={'center'} ml={6}>
|
||||
<TeamSelector height={'28px'} />
|
||||
</Flex>
|
||||
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
|
||||
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
|
||||
<MyIcon
|
||||
name="edit"
|
||||
w="18px"
|
||||
cursor="pointer"
|
||||
_hover={{
|
||||
color: 'primary.500'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!userInfo?.team) return;
|
||||
setEditTeamData({
|
||||
id: userInfo.team.teamId,
|
||||
name: userInfo.team.teamName,
|
||||
avatar: userInfo.team.avatar
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
float={'right'}
|
||||
color={'myGray.900'}
|
||||
h={'1.25rem'}
|
||||
px={'0.5rem'}
|
||||
py={'0.125rem'}
|
||||
fontSize={'0.75rem'}
|
||||
borderRadius={'1.25rem'}
|
||||
bg={'myGray.150'}
|
||||
>
|
||||
{t('account_team:total_team_members', { amount: teamSize })}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* table */}
|
||||
<Box py={'1.5rem'} px={'2rem'}>
|
||||
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{ label: t('account_team:member'), value: TeamTabEnum.member },
|
||||
{ label: t('account_team:group'), value: TeamTabEnum.group },
|
||||
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
|
||||
]}
|
||||
px={'1rem'}
|
||||
value={teamTab}
|
||||
onChange={(e) => {
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
teamTab: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex alignItems={'center'}>
|
||||
{teamTab === TeamTabEnum.member &&
|
||||
userInfo?.team.permission.hasManagePer &&
|
||||
feConfigs?.show_team_chat && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
|
||||
onClick={() => {
|
||||
onOpenTeamTagsAsync();
|
||||
}}
|
||||
>
|
||||
{t('account_team:label_sync')}
|
||||
</Button>
|
||||
)}
|
||||
{teamTab === TeamTabEnum.member && userInfo?.team.permission.hasManagePer && (
|
||||
<Button
|
||||
variant={'primary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
|
||||
onClick={() => {
|
||||
if (
|
||||
teamPlanStatus?.standardConstants?.maxTeamMember &&
|
||||
teamPlanStatus.standardConstants.maxTeamMember <= members.length
|
||||
) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('user.team.Over Max Member Tip', {
|
||||
max: teamPlanStatus.standardConstants.maxTeamMember
|
||||
})
|
||||
});
|
||||
} else {
|
||||
onOpenInvite();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('account_team:user_team_invite_member')}
|
||||
</Button>
|
||||
)}
|
||||
{teamTab === TeamTabEnum.member && !userInfo?.team.permission.isOwner && (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
|
||||
isLoading={isLoadingLeaveTeam}
|
||||
onClick={() => {
|
||||
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
|
||||
}}
|
||||
>
|
||||
{t('account_team:user_team_leave_team')}
|
||||
</Button>
|
||||
)}
|
||||
{teamTab === TeamTabEnum.group && userInfo?.team.permission.hasManagePer && (
|
||||
<Button
|
||||
variant={'primary'}
|
||||
size="md"
|
||||
borderRadius={'md'}
|
||||
ml={3}
|
||||
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
|
||||
onClick={onOpenGroupInfo}
|
||||
>
|
||||
{t('user:team.group.create')}
|
||||
</Button>
|
||||
)}
|
||||
{teamTab === TeamTabEnum.permission && (
|
||||
<Box ml="auto">
|
||||
<SearchInput
|
||||
placeholder={t('user:team.group.search_placeholder')}
|
||||
w="200px"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
|
||||
{teamTab === TeamTabEnum.member && <MemberTable />}
|
||||
{teamTab === TeamTabEnum.group && (
|
||||
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
|
||||
)}
|
||||
{teamTab === TeamTabEnum.permission && <PermissionManage />}
|
||||
</Box>
|
||||
</Box>
|
||||
{isOpenInvite && userInfo?.team?.teamId && (
|
||||
<InviteModal
|
||||
teamId={userInfo.team.teamId}
|
||||
onClose={onCloseInvite}
|
||||
onSuccess={refetchMembers}
|
||||
/>
|
||||
)}
|
||||
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
|
||||
{isOpenGroupInfo && (
|
||||
<GroupInfoModal
|
||||
onClose={() => {
|
||||
onCloseGroupInfo();
|
||||
setEditGroupId(undefined);
|
||||
}}
|
||||
editGroupId={editGroupId}
|
||||
/>
|
||||
)}
|
||||
{isOpenManageGroupMember && (
|
||||
<ManageGroupMemberModal
|
||||
onClose={() => {
|
||||
onCloseManageGroupMember();
|
||||
setEditGroupId(undefined);
|
||||
}}
|
||||
editGroupId={editGroupId}
|
||||
/>
|
||||
)}
|
||||
<ConfirmLeaveTeamModal />
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account', 'account_team', 'user']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const Render = () => {
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
return !!userInfo?.team ? (
|
||||
<TeamModalContextProvider>
|
||||
<Team />
|
||||
</TeamModalContextProvider>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
@@ -63,44 +63,44 @@ const UsageDetail = ({ usage, onClose }: { usage: UsageItemType; onClose: () =>
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/bill.svg"
|
||||
title={t('common:support.wallet.usage.Usage Detail')}
|
||||
title={t('account_usage:usage_detail')}
|
||||
maxW={['90vw', '700px']}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.bill.Number')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 80px'}>{t('account_usage:order_number')}:</FormLabel>
|
||||
<Box>{usage.id}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.Time')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 80px'}>{t('account_usage:generation_time')}:</FormLabel>
|
||||
<Box>{dayjs(usage.time).format('YYYY/MM/DD HH:mm:ss')}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.App name')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 80px'}>{t('account_usage:app_name')}:</FormLabel>
|
||||
<Box>{t(usage.appName as any) || '-'}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.Source')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 80px'}>{t('account_usage:source')}:</FormLabel>
|
||||
<Box>{t(UsageSourceMap[usage.source]?.label as any)}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.Total points')}:</FormLabel>
|
||||
<FormLabel flex={'0 0 80px'}>{t('account_usage:total_points_consumed')}:</FormLabel>
|
||||
<Box fontWeight={'bold'}>{formatNumber(usage.totalPoints)}</Box>
|
||||
</Flex>
|
||||
<Box pb={4}>
|
||||
<FormLabel flex={'0 0 80px'} mb={1}>
|
||||
{t('common:support.wallet.usage.Bill Module')}
|
||||
{t('account_usage:billing_module')}
|
||||
</FormLabel>
|
||||
<TableContainer fontSize={'sm'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:support.wallet.usage.Module name')}</Th>
|
||||
{hasModel && <Th>{t('common:support.wallet.usage.Ai model')}</Th>}
|
||||
{hasToken && <Th>{t('common:support.wallet.usage.Token Length')}</Th>}
|
||||
{hasCharsLen && <Th>{t('common:support.wallet.usage.Text Length')}</Th>}
|
||||
{hasDuration && <Th>{t('common:support.wallet.usage.Duration')}</Th>}
|
||||
<Th>{t('common:support.wallet.usage.Total points')}</Th>
|
||||
<Th>{t('account_usage:module_name')}</Th>
|
||||
{hasModel && <Th>{t('account_usage:ai_model')}</Th>}
|
||||
{hasToken && <Th>{t('account_usage:token_length')}</Th>}
|
||||
{hasCharsLen && <Th>{t('account_usage:text_length')}</Th>}
|
||||
{hasDuration && <Th>{t('account_usage:duration_seconds')}</Th>}
|
||||
<Th>{t('account_usage:total_points_consumed')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
212
projects/app/src/pages/account/usage/index.tsx
Normal file
212
projects/app/src/pages/account/usage/index.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
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 '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import DateRangePicker, {
|
||||
type DateRangeType
|
||||
} from '@fastgpt/web/components/common/DateRangePicker';
|
||||
import { addDays } from 'date-fns';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import AccountContainer, { TabEnum } from '../components/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
|
||||
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 } = useSystem();
|
||||
const { userInfo, loadAndGetTeamMembers } = useUserStore();
|
||||
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
|
||||
|
||||
const sourceList = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ label: t('account_usage:all'), value: '' },
|
||||
...Object.entries(UsageSourceMap).map(([key, value]) => ({
|
||||
label: t(value.label as any),
|
||||
value: key
|
||||
}))
|
||||
] as {
|
||||
label: never;
|
||||
value: UsageSourceEnum | '';
|
||||
}[],
|
||||
[t]
|
||||
);
|
||||
|
||||
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
|
||||
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
|
||||
if (!userInfo?.team?.teamId) return [];
|
||||
return loadAndGetTeamMembers();
|
||||
});
|
||||
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 (
|
||||
<AccountContainer>
|
||||
<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?.permission.hasManagePer && (
|
||||
<Flex alignItems={'center'}>
|
||||
<Box mr={2} flexShrink={0}>
|
||||
{t('account_usage: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
|
||||
mt={2}
|
||||
px={[3, 8]}
|
||||
position={'relative'}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
{/* <Th>{t('account_usage:user.team.Member Name')}</Th> */}
|
||||
<Th>{t('account_usage:user_type')}</Th>
|
||||
<Th>
|
||||
<MySelect<UsageSourceEnum | ''>
|
||||
list={sourceList}
|
||||
value={usageSource}
|
||||
size={'sm'}
|
||||
onchange={(e) => {
|
||||
setUsageSource(e);
|
||||
}}
|
||||
w={'130px'}
|
||||
></MySelect>
|
||||
</Th>
|
||||
<Th>{t('account_usage:project_name')}</Th>
|
||||
<Th>{t('account_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 as any) || '-'}</Td>
|
||||
<Td>{t(item.appName as any) || '-'}</Td>
|
||||
<Td>{formatNumber(item.totalPoints) || 0}</Td>
|
||||
<Td>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
onClick={() => setUsageDetail(item)}
|
||||
>
|
||||
{t('account_usage:details')}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{!isLoading && usages.length === 0 && (
|
||||
<EmptyTip text={t('account_usage:no_usage_records')}></EmptyTip>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
{!!usageDetail && (
|
||||
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
|
||||
)}
|
||||
</Flex>
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account_usage', 'account']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default React.memo(UsageTable);
|
||||
@@ -7,42 +7,62 @@ import { NextAPI } from '@/service/middleware/entry';
|
||||
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import {
|
||||
OwnerPermissionVal,
|
||||
WritePermissionVal
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
|
||||
export type PreviewContextProps = {
|
||||
datasetId: string;
|
||||
type: DatasetSourceReadTypeEnum;
|
||||
sourceId: string;
|
||||
isQAImport?: boolean;
|
||||
selector?: string;
|
||||
externalFileId?: string;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<PreviewContextProps>, res: NextApiResponse<any>) {
|
||||
const { type, sourceId, isQAImport, selector } = req.body;
|
||||
const { type, sourceId, isQAImport, selector, datasetId, externalFileId } = req.body;
|
||||
|
||||
if (!sourceId) {
|
||||
throw new Error('fileId is empty');
|
||||
}
|
||||
|
||||
const { teamId } = await (async () => {
|
||||
const { teamId, apiServer } = await (async () => {
|
||||
if (type === DatasetSourceReadTypeEnum.fileLocal) {
|
||||
return authCollectionFile({
|
||||
const res = await authCollectionFile({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
fileId: sourceId,
|
||||
per: OwnerPermissionVal
|
||||
});
|
||||
return {
|
||||
teamId: res.teamId
|
||||
};
|
||||
}
|
||||
return authCert({ req, authApiKey: true, authToken: true });
|
||||
const { dataset } = await authDataset({
|
||||
req,
|
||||
authApiKey: true,
|
||||
authToken: true,
|
||||
datasetId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
return {
|
||||
teamId: dataset.teamId,
|
||||
apiServer: dataset.apiServer
|
||||
};
|
||||
})();
|
||||
|
||||
const rawText = await readDatasetSourceRawText({
|
||||
teamId,
|
||||
type,
|
||||
sourceId: sourceId,
|
||||
sourceId,
|
||||
isQAImport,
|
||||
selector
|
||||
selector,
|
||||
apiServer,
|
||||
externalFileId
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,11 +3,19 @@ import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
import { pushQuestionGuideUsage } from '@/service/support/wallet/usage/push';
|
||||
import { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
|
||||
import { authChatCert } from '@/service/support/permission/auth/chat';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { AuthModeType } from '@fastgpt/service/support/permission/type';
|
||||
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/authLink';
|
||||
import { authOutLinkInit } from '@/service/support/permission/auth/outLink';
|
||||
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<
|
||||
@@ -52,3 +60,62 @@ async function handler(
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
/*
|
||||
Abandoned
|
||||
Different chat source
|
||||
1. token (header)
|
||||
2. apikey (header)
|
||||
3. share page (body: shareId outLinkUid)
|
||||
4. team chat page (body: teamId teamToken)
|
||||
*/
|
||||
async function authChatCert(props: AuthModeType): Promise<{
|
||||
teamId: string;
|
||||
tmbId: string;
|
||||
authType: AuthUserTypeEnum;
|
||||
apikey: string;
|
||||
isOwner: boolean;
|
||||
canWrite: boolean;
|
||||
outLinkUid?: string;
|
||||
}> {
|
||||
const { teamId, teamToken, shareId, outLinkUid } = props.req.body as OutLinkChatAuthProps;
|
||||
|
||||
if (shareId && outLinkUid) {
|
||||
const { outLinkConfig } = await authOutLinkValid({ shareId });
|
||||
const { uid } = await authOutLinkInit({
|
||||
outLinkUid,
|
||||
tokenUrl: outLinkConfig.limit?.hookUrl
|
||||
});
|
||||
|
||||
return {
|
||||
teamId: String(outLinkConfig.teamId),
|
||||
tmbId: String(outLinkConfig.tmbId),
|
||||
authType: AuthUserTypeEnum.outLink,
|
||||
apikey: '',
|
||||
isOwner: false,
|
||||
canWrite: false,
|
||||
outLinkUid: uid
|
||||
};
|
||||
}
|
||||
if (teamId && teamToken) {
|
||||
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
|
||||
const tmb = await MongoTeamMember.findOne(
|
||||
{ teamId, role: TeamMemberRoleEnum.owner },
|
||||
'tmbId'
|
||||
).lean();
|
||||
|
||||
if (!tmb) return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
|
||||
return {
|
||||
teamId,
|
||||
tmbId: String(tmb._id),
|
||||
authType: AuthUserTypeEnum.teamDomain,
|
||||
apikey: '',
|
||||
isOwner: false,
|
||||
canWrite: false,
|
||||
outLinkUid: uid
|
||||
};
|
||||
}
|
||||
|
||||
return authCert(props);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,59 @@
|
||||
import type { 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 { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { getChatItems } from '@fastgpt/service/core/chat/controller';
|
||||
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
|
||||
export type CreateQuestionGuideParams = OutLinkChatAuthProps & {
|
||||
appId: string;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<CreateQuestionGuideParams>, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { messages } = req.body;
|
||||
const { appId, chatId } = req.body;
|
||||
|
||||
const { tmbId, teamId } = await authChatCrud({
|
||||
const [{ tmbId, teamId }] = await Promise.all([
|
||||
authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
...req.body
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
const qgModel = global.llmModels[0];
|
||||
// Auth app and get questionGuide config
|
||||
|
||||
const { result, tokens } = await createQuestionGuide({
|
||||
messages,
|
||||
model: qgModel.model
|
||||
});
|
||||
// Get histories
|
||||
const { histories } = await getChatItems({
|
||||
appId,
|
||||
chatId,
|
||||
offset: 0,
|
||||
limit: 6,
|
||||
field: 'obj value time'
|
||||
});
|
||||
const messages = chats2GPTMessages({ messages: histories, reserveId: false });
|
||||
|
||||
jsonRes(res, {
|
||||
data: result
|
||||
});
|
||||
const qgModel = global.llmModels[0];
|
||||
|
||||
pushQuestionGuideUsage({
|
||||
tokens,
|
||||
teamId,
|
||||
tmbId
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
const { result, tokens } = await createQuestionGuide({
|
||||
messages,
|
||||
model: qgModel.model
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: result
|
||||
});
|
||||
|
||||
pushQuestionGuideUsage({
|
||||
tokens,
|
||||
teamId,
|
||||
tmbId
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -28,33 +28,51 @@ export type ListAppBody = {
|
||||
async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemType[]> {
|
||||
const { parentId, type, getRecentlyChat, searchKey } = req.body;
|
||||
|
||||
// 凭证校验
|
||||
const {
|
||||
app: ParentApp,
|
||||
tmbId,
|
||||
teamId,
|
||||
permission: myPer
|
||||
} = await (async () => {
|
||||
if (parentId) {
|
||||
return await authApp({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
appId: parentId,
|
||||
per: ReadPermissionVal
|
||||
// Auth user permission
|
||||
const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([
|
||||
authUserPer({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
per: ReadPermissionVal
|
||||
}),
|
||||
...(parentId
|
||||
? [
|
||||
authApp({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
appId: parentId,
|
||||
per: ReadPermissionVal
|
||||
})
|
||||
]
|
||||
: [])
|
||||
]);
|
||||
|
||||
// Get team all app permissions
|
||||
const [perList, myGroupMap] = await Promise.all([
|
||||
MongoResourcePermission.find({
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
teamId,
|
||||
resourceId: {
|
||||
$exists: true
|
||||
}
|
||||
}).lean(),
|
||||
getGroupsByTmbId({
|
||||
tmbId,
|
||||
teamId
|
||||
}).then((item) => {
|
||||
const map = new Map<string, 1>();
|
||||
item.forEach((item) => {
|
||||
map.set(String(item._id), 1);
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
...(await authUserPer({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
per: ReadPermissionVal
|
||||
})),
|
||||
app: undefined
|
||||
};
|
||||
}
|
||||
})();
|
||||
return map;
|
||||
})
|
||||
]);
|
||||
// Get my permissions
|
||||
const myPerList = perList.filter(
|
||||
(item) => String(item.tmbId) === String(tmbId) || myGroupMap.has(String(item.groupId))
|
||||
);
|
||||
|
||||
const findAppsQuery = (() => {
|
||||
const searchMatch = searchKey
|
||||
@@ -65,10 +83,15 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
|
||||
]
|
||||
}
|
||||
: {};
|
||||
// Filter apps by permission, if not owner, only get apps that I have permission to access
|
||||
const appIdQuery = teamPer.isOwner
|
||||
? {}
|
||||
: { _id: { $in: myPerList.map((item) => item.resourceId) } };
|
||||
|
||||
if (getRecentlyChat) {
|
||||
return {
|
||||
// get all chat app
|
||||
...appIdQuery,
|
||||
teamId,
|
||||
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.plugin] },
|
||||
...searchMatch
|
||||
@@ -77,63 +100,46 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
|
||||
|
||||
if (searchKey) {
|
||||
return {
|
||||
...appIdQuery,
|
||||
teamId,
|
||||
...searchMatch
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...appIdQuery,
|
||||
teamId,
|
||||
...(type && (Array.isArray(type) ? { type: { $in: type } } : { type })),
|
||||
...parseParentIdInMongo(parentId)
|
||||
};
|
||||
})();
|
||||
const limit = (() => {
|
||||
if (getRecentlyChat) return 15;
|
||||
if (searchKey) return 20;
|
||||
return 1000;
|
||||
})();
|
||||
|
||||
/* temp: get all apps and per */
|
||||
const myGroupIds = (
|
||||
await getGroupsByTmbId({
|
||||
tmbId,
|
||||
teamId
|
||||
const myApps = await MongoApp.find(
|
||||
findAppsQuery,
|
||||
'_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission'
|
||||
)
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
).map((item) => String(item._id));
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const [myApps, perList] = await Promise.all([
|
||||
MongoApp.find(
|
||||
findAppsQuery,
|
||||
'_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission'
|
||||
)
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
.limit(searchKey ? 20 : 1000)
|
||||
.lean(),
|
||||
MongoResourcePermission.find({
|
||||
resourceType: PerResourceTypeEnum.app,
|
||||
teamId,
|
||||
resourceId: {
|
||||
$exists: true
|
||||
}
|
||||
}).lean()
|
||||
]);
|
||||
|
||||
// Filter apps by permission
|
||||
const filterApps = myApps
|
||||
// Add app permission and filter apps by read permission
|
||||
const formatApps = myApps
|
||||
.map((app) => {
|
||||
const { Per, privateApp } = (() => {
|
||||
const myPerList = perList.filter(
|
||||
(item) =>
|
||||
String(item.tmbId) === String(tmbId) || myGroupIds.includes(String(item.groupId))
|
||||
);
|
||||
const getPer = (appId: string) => {
|
||||
const tmbPer = myPerList.find(
|
||||
(item) => String(item.resourceId) === appId && !!item.tmbId
|
||||
)?.permission;
|
||||
const groupPer = getGroupPer(
|
||||
myPerList
|
||||
.filter(
|
||||
(item) =>
|
||||
String(item.resourceId) === appId && myGroupIds.includes(String(item.groupId))
|
||||
)
|
||||
.filter((item) => String(item.resourceId) === appId && !!item.groupId)
|
||||
.map((item) => item.permission)
|
||||
);
|
||||
|
||||
@@ -143,15 +149,15 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
|
||||
return {
|
||||
Per: new AppPermission({
|
||||
per: tmbPer ?? groupPer ?? AppDefaultPermissionVal,
|
||||
isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner
|
||||
isOwner: String(app.tmbId) === String(tmbId) || teamPer.isOwner
|
||||
}),
|
||||
privateApp: AppFolderTypeList.includes(app.type) ? clbCount <= 1 : clbCount === 0
|
||||
};
|
||||
};
|
||||
|
||||
// Inherit app
|
||||
if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) {
|
||||
return getPer(String(ParentApp._id));
|
||||
if (app.inheritPermission && parentId && !AppFolderTypeList.includes(app.type)) {
|
||||
return getPer(String(parentId));
|
||||
} else {
|
||||
return getPer(String(app._id));
|
||||
}
|
||||
@@ -165,9 +171,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
|
||||
})
|
||||
.filter((app) => app.permission.hasReadPer);
|
||||
|
||||
const sliceApps = getRecentlyChat ? filterApps.slice(0, 15) : filterApps;
|
||||
|
||||
return sliceApps.map((app) => ({
|
||||
return formatApps.map((app) => ({
|
||||
_id: app._id,
|
||||
tmbId: app.tmbId,
|
||||
avatar: app.avatar,
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { NextApiResponse } from 'next';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { getSystemPlugins } from '@/service/core/app/plugin';
|
||||
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { getSystemPluginCb, getSystemPlugins } from '@/service/core/app/plugin';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
|
||||
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
|
||||
export type GetSystemPluginTemplatesBody = {
|
||||
searchKey?: string;
|
||||
@@ -24,6 +24,10 @@ async function handler(
|
||||
|
||||
const formatParentId = parentId || null;
|
||||
|
||||
// Make sure system plugin callbacks are loaded
|
||||
if (!global.systemPluginCb || Object.keys(global.systemPluginCb).length === 0)
|
||||
await getSystemPluginCb();
|
||||
|
||||
return getSystemPlugins().then((res) =>
|
||||
res
|
||||
// Just show the active plugins
|
||||
@@ -39,7 +43,9 @@ async function handler(
|
||||
intro: plugin.intro,
|
||||
isTool: plugin.isTool,
|
||||
currentCost: plugin.currentCost,
|
||||
author: plugin.author
|
||||
hasTokenFee: plugin.hasTokenFee,
|
||||
author: plugin.author,
|
||||
instructions: plugin.userGuide
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (searchKey) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { NextAPI } from '@/service/middleware/entry';
|
||||
import {
|
||||
ManagePermissionVal,
|
||||
PerResourceTypeEnum,
|
||||
ReadPermissionVal,
|
||||
WritePermissionVal
|
||||
ReadPermissionVal
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
@@ -16,7 +15,7 @@ import {
|
||||
syncChildrenPermission,
|
||||
syncCollaborators
|
||||
} from '@fastgpt/service/support/permission/inheritPermission';
|
||||
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
|
||||
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { ClientSession } from 'mongoose';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
|
||||
@@ -91,7 +90,10 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
|
||||
const onUpdate = async (session?: ClientSession) => {
|
||||
// format nodes data
|
||||
// 1. dataset search limit, less than model quoteMaxToken
|
||||
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
|
||||
const { nodes: formatNodes } = beforeUpdateAppFormat({
|
||||
nodes,
|
||||
isPlugin: app.type === AppTypeEnum.plugin
|
||||
});
|
||||
|
||||
return MongoApp.findByIdAndUpdate(
|
||||
appId,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/strin
|
||||
import { PostPublishAppProps } from '@/global/core/app/api';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<PostPublishAppProps>,
|
||||
@@ -17,9 +18,12 @@ async function handler(
|
||||
const { appId } = req.query as { appId: string };
|
||||
const { nodes = [], edges = [], chatConfig, isPublish, versionName } = req.body;
|
||||
|
||||
const { tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
const { app, tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
|
||||
const { nodes: formatNodes } = beforeUpdateAppFormat({
|
||||
nodes,
|
||||
isPlugin: app.type === AppTypeEnum.plugin
|
||||
});
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
// create version histories
|
||||
|
||||
@@ -8,7 +8,6 @@ import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { authType2UsageSource } from '@/service/support/wallet/usage/utils';
|
||||
import { getAudioSpeechModel } from '@fastgpt/service/core/ai/model';
|
||||
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
|
||||
/*
|
||||
@@ -93,4 +92,5 @@ async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiRes
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
// 不能使用 NextApiResponse
|
||||
export default handler;
|
||||
|
||||
36
projects/app/src/pages/api/core/dataset/apiDataset/list.ts
Normal file
36
projects/app/src/pages/api/core/dataset/apiDataset/list.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { APIFileItem } from '@fastgpt/global/core/dataset/apiDataset';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { NextApiRequest } from 'next';
|
||||
|
||||
export type GetApiDatasetFileListProps = {
|
||||
searchKey?: string;
|
||||
parentId?: ParentIdType;
|
||||
datasetId: string;
|
||||
};
|
||||
|
||||
export type GetApiDatasetFileListResponse = APIFileItem[];
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
let { searchKey = '', parentId = null, datasetId } = req.body;
|
||||
|
||||
const { dataset } = await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
datasetId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const apiServer = dataset.apiServer;
|
||||
if (!apiServer) {
|
||||
return Promise.reject('apiServer is required');
|
||||
}
|
||||
|
||||
return useApiDatasetRequest({ apiServer }).listFiles({ searchKey, parentId });
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
|
||||
|
||||
export type listExistIdQuery = {
|
||||
datasetId: string;
|
||||
};
|
||||
|
||||
export type listExistIdBody = {};
|
||||
|
||||
export type listExistIdResponse = string[];
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<listExistIdBody, listExistIdQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<listExistIdResponse> {
|
||||
const { datasetId } = req.query;
|
||||
|
||||
const { dataset } = await authDataset({
|
||||
req,
|
||||
datasetId,
|
||||
per: ReadPermissionVal,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
const collections = await MongoDatasetCollection.find(
|
||||
{
|
||||
teamId: dataset.teamId,
|
||||
datasetId: dataset._id
|
||||
},
|
||||
'_id apiFileId'
|
||||
).lean();
|
||||
|
||||
return collections.map((col) => col.apiFileId).filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import type { ApiDatasetCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
TrainingModeEnum,
|
||||
DatasetCollectionTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CreateCollectionResponse } from '@/global/core/dataset/api';
|
||||
import { readApiServerFileContent } from '@fastgpt/service/core/dataset/read';
|
||||
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
|
||||
async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
const {
|
||||
name,
|
||||
apiFileId,
|
||||
trainingType = TrainingModeEnum.chunk,
|
||||
chunkSize = 512,
|
||||
chunkSplitter,
|
||||
qaPrompt,
|
||||
...body
|
||||
} = req.body as ApiDatasetCreateDatasetCollectionParams;
|
||||
|
||||
const { teamId, tmbId, dataset } = await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
datasetId: body.datasetId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
const apiServer = dataset.apiServer;
|
||||
if (!apiServer) {
|
||||
return Promise.reject('Api server not found');
|
||||
}
|
||||
if (!apiFileId) {
|
||||
return Promise.reject('ApiFileId not found');
|
||||
}
|
||||
|
||||
// Auth same apiFileId
|
||||
const storeCol = await MongoDatasetCollection.findOne(
|
||||
{
|
||||
teamId,
|
||||
datasetId: dataset._id,
|
||||
apiFileId
|
||||
},
|
||||
'_id'
|
||||
).lean();
|
||||
|
||||
if (storeCol) {
|
||||
return Promise.reject(DatasetErrEnum.sameApiCollection);
|
||||
}
|
||||
|
||||
const content = await readApiServerFileContent({
|
||||
apiServer,
|
||||
apiFileId,
|
||||
teamId
|
||||
});
|
||||
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText: content,
|
||||
relatedId: apiFileId,
|
||||
createCollectionParams: {
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: DatasetCollectionTypeEnum.apiFile,
|
||||
name: name,
|
||||
trainingType,
|
||||
chunkSize,
|
||||
chunkSplitter,
|
||||
qaPrompt,
|
||||
apiFileId,
|
||||
metadata: {
|
||||
relatedImgId: apiFileId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { collectionId, results: insertResults };
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -2,23 +2,16 @@ import type { NextApiRequest } from 'next';
|
||||
import { readFileContentFromMongo } from '@fastgpt/service/common/file/gridfs/controller';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { FileIdCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api';
|
||||
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
TrainingModeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
|
||||
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
|
||||
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
|
||||
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { rawText2Chunks } from '@fastgpt/service/core/dataset/read';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { CreateCollectionResponse } from '@/global/core/dataset/api';
|
||||
import { MongoRawTextBuffer } from '@fastgpt/service/common/buffer/rawText/schema';
|
||||
|
||||
async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
const { datasetId, parentId, fileId, ...body } = req.body as FileIdCreateDatasetCollectionParams;
|
||||
@@ -39,21 +32,11 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
isQAImport: true
|
||||
});
|
||||
|
||||
// 2. split chunks
|
||||
const chunks = rawText2Chunks({
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText,
|
||||
isQAImport: true
|
||||
});
|
||||
|
||||
// 3. auth limit
|
||||
await checkDatasetLimit({
|
||||
teamId,
|
||||
insertLen: predictDataLimitLength(trainingType, chunks)
|
||||
});
|
||||
|
||||
return mongoSessionRun(async (session) => {
|
||||
// 4. create collection
|
||||
const { _id: collectionId } = await createOneCollection({
|
||||
isQAImport: true,
|
||||
createCollectionParams: {
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
@@ -65,41 +48,13 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
|
||||
// special metadata
|
||||
trainingType,
|
||||
chunkSize: 0,
|
||||
|
||||
session
|
||||
});
|
||||
|
||||
// 5. create training bill
|
||||
const { billId } = await createTrainingUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
appName: filename,
|
||||
billSource: UsageSourceEnum.training,
|
||||
vectorModel: getVectorModel(dataset.vectorModel)?.name,
|
||||
agentModel: getLLMModel(dataset.agentModel)?.name,
|
||||
session
|
||||
});
|
||||
|
||||
// 6. insert to training queue
|
||||
const insertResult = await pushDataListToTrainingQueue({
|
||||
teamId,
|
||||
tmbId,
|
||||
datasetId: dataset._id,
|
||||
collectionId,
|
||||
agentModel: dataset.agentModel,
|
||||
vectorModel: dataset.vectorModel,
|
||||
trainingMode: trainingType,
|
||||
billId,
|
||||
data: chunks.map((chunk, index) => ({
|
||||
q: chunk.q,
|
||||
a: chunk.a,
|
||||
chunkIndex: index
|
||||
})),
|
||||
session
|
||||
});
|
||||
|
||||
return { collectionId, results: insertResult };
|
||||
chunkSize: 0
|
||||
}
|
||||
});
|
||||
|
||||
// remove buffer
|
||||
await MongoRawTextBuffer.deleteOne({ sourceId: fileId });
|
||||
|
||||
return { collectionId, results: insertResults };
|
||||
}
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
import { readFileContentFromMongo } from '@fastgpt/service/common/file/gridfs/controller';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { FileIdCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api';
|
||||
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
TrainingModeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { MongoImage } from '@fastgpt/service/common/file/image/schema';
|
||||
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
|
||||
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
|
||||
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
|
||||
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { hashStr } from '@fastgpt/global/common/string/tools';
|
||||
import { MongoRawTextBuffer } from '@fastgpt/service/common/buffer/rawText/schema';
|
||||
import { rawText2Chunks } from '@fastgpt/service/core/dataset/read';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CreateCollectionResponse } from '@/global/core/dataset/api';
|
||||
|
||||
async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>) {
|
||||
async function handler(
|
||||
req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
|
||||
): CreateCollectionResponse {
|
||||
const {
|
||||
fileId,
|
||||
trainingType = TrainingModeEnum.chunk,
|
||||
@@ -32,7 +26,6 @@ async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
|
||||
...body
|
||||
} = req.body;
|
||||
|
||||
const start = Date.now();
|
||||
const { teamId, tmbId, dataset } = await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
@@ -48,23 +41,10 @@ async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
|
||||
fileId
|
||||
});
|
||||
|
||||
// 2. split chunks
|
||||
const chunks = rawText2Chunks({
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText,
|
||||
chunkLen: chunkSize,
|
||||
overlapRatio: trainingType === TrainingModeEnum.chunk ? 0.2 : 0,
|
||||
customReg: chunkSplitter ? [chunkSplitter] : []
|
||||
});
|
||||
|
||||
// 3. auth limit
|
||||
await checkDatasetLimit({
|
||||
teamId,
|
||||
insertLen: predictDataLimitLength(trainingType, chunks)
|
||||
});
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
// 4. create collection
|
||||
const { _id: collectionId } = await createOneCollection({
|
||||
createCollectionParams: {
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
@@ -79,63 +59,19 @@ async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
|
||||
trainingType,
|
||||
chunkSize,
|
||||
chunkSplitter,
|
||||
qaPrompt,
|
||||
qaPrompt
|
||||
},
|
||||
|
||||
hashRawText: hashStr(rawText),
|
||||
rawTextLength: rawText.length,
|
||||
session
|
||||
});
|
||||
|
||||
// 5. create training bill
|
||||
const { billId } = await createTrainingUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
appName: filename,
|
||||
billSource: UsageSourceEnum.training,
|
||||
vectorModel: getVectorModel(dataset.vectorModel)?.name,
|
||||
agentModel: getLLMModel(dataset.agentModel)?.name,
|
||||
session
|
||||
});
|
||||
|
||||
// 6. insert to training queue
|
||||
await pushDataListToTrainingQueue({
|
||||
teamId,
|
||||
tmbId,
|
||||
datasetId: dataset._id,
|
||||
collectionId,
|
||||
agentModel: dataset.agentModel,
|
||||
vectorModel: dataset.vectorModel,
|
||||
trainingMode: trainingType,
|
||||
prompt: qaPrompt,
|
||||
billId,
|
||||
data: chunks.map((item, index) => ({
|
||||
...item,
|
||||
chunkIndex: index
|
||||
})),
|
||||
session
|
||||
});
|
||||
|
||||
// 7. remove related image ttl
|
||||
await MongoImage.updateMany(
|
||||
{
|
||||
teamId,
|
||||
'metadata.relatedId': fileId
|
||||
},
|
||||
{
|
||||
// Remove expiredTime to avoid ttl expiration
|
||||
$unset: {
|
||||
expiredTime: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
|
||||
// remove buffer
|
||||
await MongoRawTextBuffer.deleteOne({ sourceId: fileId });
|
||||
return collectionId;
|
||||
relatedId: fileId
|
||||
});
|
||||
|
||||
// remove buffer
|
||||
await MongoRawTextBuffer.deleteOne({ sourceId: fileId });
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
results: insertResults
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import type { LinkCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
TrainingModeEnum,
|
||||
DatasetCollectionTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
|
||||
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 { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { reloadCollectionChunks } from '@fastgpt/service/core/dataset/collection/utils';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CreateCollectionResponse } from '@/global/core/dataset/api';
|
||||
import { urlsFetch } from '@fastgpt/service/common/string/cheerio';
|
||||
import { hashStr } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
const {
|
||||
@@ -35,59 +30,45 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
// 1. check dataset limit
|
||||
await checkDatasetLimit({
|
||||
teamId,
|
||||
insertLen: predictDataLimitLength(trainingType, new Array(10))
|
||||
const result = await urlsFetch({
|
||||
urlList: [link],
|
||||
selector: body?.metadata?.webPageSelector
|
||||
});
|
||||
const { title = link, content = '' } = result[0];
|
||||
|
||||
return mongoSessionRun(async (session) => {
|
||||
// 2. create collection
|
||||
const collection = await createOneCollection({
|
||||
if (!content) {
|
||||
return Promise.reject('Can not fetch content from link');
|
||||
}
|
||||
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText: content,
|
||||
createCollectionParams: {
|
||||
...body,
|
||||
name: link,
|
||||
name: title,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: DatasetCollectionTypeEnum.link,
|
||||
metadata: {
|
||||
relatedImgId: link,
|
||||
webPageSelector: body?.metadata?.webPageSelector
|
||||
},
|
||||
|
||||
trainingType,
|
||||
chunkSize,
|
||||
chunkSplitter,
|
||||
qaPrompt,
|
||||
|
||||
rawLink: link,
|
||||
session
|
||||
});
|
||||
rawLink: link
|
||||
},
|
||||
|
||||
// 3. create bill and start sync
|
||||
const { billId } = await createTrainingUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
appName: 'core.dataset.collection.Sync Collection',
|
||||
billSource: UsageSourceEnum.training,
|
||||
vectorModel: getVectorModel(dataset.vectorModel).name,
|
||||
agentModel: getLLMModel(dataset.agentModel).name,
|
||||
session
|
||||
});
|
||||
|
||||
// load
|
||||
const result = await reloadCollectionChunks({
|
||||
collection: {
|
||||
...collection.toObject(),
|
||||
datasetId: dataset
|
||||
},
|
||||
tmbId,
|
||||
billId,
|
||||
session
|
||||
});
|
||||
|
||||
return {
|
||||
collectionId: collection._id,
|
||||
results: {
|
||||
insertLen: result.insertLen
|
||||
}
|
||||
};
|
||||
relatedId: link
|
||||
});
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
results: insertResults
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -4,22 +4,10 @@ import { getUploadModel } from '@fastgpt/service/common/file/multer';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { FileCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api';
|
||||
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
|
||||
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
TrainingModeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { getNanoid, hashStr } from '@fastgpt/global/common/string/tools';
|
||||
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
|
||||
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
|
||||
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
|
||||
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
|
||||
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
import { getDatasetModel, getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { MongoImage } from '@fastgpt/service/common/file/image/schema';
|
||||
import { readRawTextByLocalFile } from '@fastgpt/service/common/file/read/utils';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
@@ -52,12 +40,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): CreateCo
|
||||
datasetId: data.datasetId
|
||||
});
|
||||
|
||||
const {
|
||||
trainingType = TrainingModeEnum.chunk,
|
||||
chunkSize = 512,
|
||||
chunkSplitter,
|
||||
qaPrompt
|
||||
} = data;
|
||||
const { fileMetadata, collectionMetadata, ...collectionData } = data;
|
||||
const collectionName = file.originalname;
|
||||
|
||||
@@ -89,84 +71,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): CreateCo
|
||||
// 3. delete tmp file
|
||||
removeFilesByPaths(filePaths);
|
||||
|
||||
// 4. split raw text to chunks
|
||||
const { chunks } = splitText2Chunks({
|
||||
text: rawText,
|
||||
chunkLen: chunkSize,
|
||||
overlapRatio: trainingType === TrainingModeEnum.chunk ? 0.2 : 0,
|
||||
customReg: chunkSplitter ? [chunkSplitter] : []
|
||||
});
|
||||
|
||||
// 5. check dataset limit
|
||||
await checkDatasetLimit({
|
||||
teamId,
|
||||
insertLen: predictDataLimitLength(trainingType, chunks)
|
||||
});
|
||||
|
||||
// 6. create collection and training bill
|
||||
const { collectionId, insertResults } = await mongoSessionRun(async (session) => {
|
||||
const { _id: collectionId } = await createOneCollection({
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText,
|
||||
relatedId: fileId,
|
||||
createCollectionParams: {
|
||||
...collectionData,
|
||||
name: collectionName,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: DatasetCollectionTypeEnum.file,
|
||||
fileId,
|
||||
rawTextLength: rawText.length,
|
||||
hashRawText: hashStr(rawText),
|
||||
metadata: {
|
||||
...collectionMetadata,
|
||||
relatedImgId
|
||||
},
|
||||
session
|
||||
});
|
||||
const { billId } = await createTrainingUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
appName: collectionName,
|
||||
billSource: UsageSourceEnum.training,
|
||||
vectorModel: getVectorModel(dataset.vectorModel)?.name,
|
||||
agentModel: getDatasetModel(dataset.agentModel)?.name
|
||||
});
|
||||
|
||||
// 7. push chunks to training queue
|
||||
const insertResults = await pushDataListToTrainingQueue({
|
||||
teamId,
|
||||
tmbId,
|
||||
datasetId: dataset._id,
|
||||
collectionId,
|
||||
agentModel: dataset.agentModel,
|
||||
vectorModel: dataset.vectorModel,
|
||||
trainingMode: trainingType,
|
||||
prompt: qaPrompt,
|
||||
billId,
|
||||
data: chunks.map((text, index) => ({
|
||||
q: text,
|
||||
chunkIndex: index
|
||||
}))
|
||||
});
|
||||
|
||||
// 8. remove image expired time
|
||||
await MongoImage.updateMany(
|
||||
{
|
||||
teamId,
|
||||
'metadata.relatedId': relatedImgId
|
||||
},
|
||||
{
|
||||
// Remove expiredTime to avoid ttl expiration
|
||||
$unset: {
|
||||
expiredTime: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
session
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
insertResults
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return { collectionId, results: insertResults };
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { reTrainingDatasetFileCollectionParams } from '@fastgpt/global/core/dataset/api';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
DatasetSourceReadTypeEnum,
|
||||
TrainingModeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { hashStr } from '@fastgpt/global/common/string/tools';
|
||||
import { readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { delOnlyCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
|
||||
type RetrainingCollectionResponse = {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
// 获取集合并处理
|
||||
async function handler(
|
||||
req: ApiRequestProps<reTrainingDatasetFileCollectionParams>
|
||||
): Promise<RetrainingCollectionResponse> {
|
||||
const {
|
||||
collectionId,
|
||||
trainingType = TrainingModeEnum.chunk,
|
||||
chunkSize = 512,
|
||||
chunkSplitter,
|
||||
qaPrompt
|
||||
} = req.body;
|
||||
|
||||
if (!collectionId) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { collection } = await authDatasetCollection({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
collectionId: collectionId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const sourceReadType = await (async () => {
|
||||
if (collection.type === DatasetCollectionTypeEnum.link) {
|
||||
if (!collection.rawLink) return Promise.reject('rawLink is missing');
|
||||
return {
|
||||
type: DatasetSourceReadTypeEnum.link,
|
||||
sourceId: collection.rawLink,
|
||||
selector: collection.metadata?.webPageSelector
|
||||
};
|
||||
}
|
||||
if (collection.type === DatasetCollectionTypeEnum.file) {
|
||||
if (!collection.fileId) return Promise.reject('fileId is missing');
|
||||
return {
|
||||
type: DatasetSourceReadTypeEnum.fileLocal,
|
||||
sourceId: collection.fileId
|
||||
};
|
||||
}
|
||||
if (collection.type === DatasetCollectionTypeEnum.apiFile) {
|
||||
if (!collection.apiFileId) return Promise.reject('apiFileId is missing');
|
||||
return {
|
||||
type: DatasetSourceReadTypeEnum.apiFile,
|
||||
sourceId: collection.apiFileId,
|
||||
apiServer: collection.datasetId.apiServer
|
||||
};
|
||||
}
|
||||
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
|
||||
if (!collection.externalFileUrl) return Promise.reject('externalFileId is missing');
|
||||
return {
|
||||
type: DatasetSourceReadTypeEnum.externalFile,
|
||||
sourceId: collection.externalFileUrl,
|
||||
externalFileId: collection.externalFileId
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(i18nT('dataset:collection_not_support_retraining'));
|
||||
})();
|
||||
|
||||
const rawText = await readDatasetSourceRawText({
|
||||
teamId: collection.teamId,
|
||||
...sourceReadType
|
||||
});
|
||||
|
||||
return mongoSessionRun(async (session) => {
|
||||
const { collectionId } = await createCollectionAndInsertData({
|
||||
dataset: collection.datasetId,
|
||||
rawText,
|
||||
createCollectionParams: {
|
||||
teamId: collection.teamId,
|
||||
tmbId: collection.tmbId,
|
||||
datasetId: collection.datasetId._id,
|
||||
name: collection.name,
|
||||
type: collection.type,
|
||||
|
||||
fileId: collection.fileId,
|
||||
rawLink: collection.rawLink,
|
||||
externalFileId: collection.externalFileId,
|
||||
externalFileUrl: collection.externalFileUrl,
|
||||
apiFileId: collection.apiFileId,
|
||||
|
||||
hashRawText: hashStr(rawText),
|
||||
rawTextLength: rawText.length,
|
||||
|
||||
tags: collection.tags,
|
||||
createTime: collection.createTime,
|
||||
|
||||
parentId: collection.parentId,
|
||||
|
||||
// special metadata
|
||||
trainingType,
|
||||
chunkSize,
|
||||
chunkSplitter,
|
||||
qaPrompt,
|
||||
metadata: collection.metadata
|
||||
}
|
||||
});
|
||||
await delOnlyCollection({
|
||||
collections: [collection],
|
||||
session
|
||||
});
|
||||
|
||||
return { collectionId };
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -1,20 +1,12 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import type { TextCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
TrainingModeEnum,
|
||||
DatasetCollectionTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
|
||||
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
|
||||
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
|
||||
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/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 { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CreateCollectionResponse } from '@/global/core/dataset/api';
|
||||
@@ -38,23 +30,10 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
// 1. split text to chunks
|
||||
const { chunks } = splitText2Chunks({
|
||||
text,
|
||||
chunkLen: chunkSize,
|
||||
overlapRatio: trainingType === TrainingModeEnum.chunk ? 0.2 : 0,
|
||||
customReg: chunkSplitter ? [chunkSplitter] : []
|
||||
});
|
||||
|
||||
// 2. check dataset limit
|
||||
await checkDatasetLimit({
|
||||
teamId,
|
||||
insertLen: predictDataLimitLength(trainingType, chunks)
|
||||
});
|
||||
|
||||
const createResult = await mongoSessionRun(async (session) => {
|
||||
// 3. create collection
|
||||
const { _id: collectionId } = await createOneCollection({
|
||||
const { collectionId, insertResults } = await createCollectionAndInsertData({
|
||||
dataset,
|
||||
rawText: text,
|
||||
createCollectionParams: {
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
@@ -64,46 +43,14 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
|
||||
trainingType,
|
||||
chunkSize,
|
||||
chunkSplitter,
|
||||
qaPrompt,
|
||||
|
||||
hashRawText: hashStr(text),
|
||||
rawTextLength: text.length,
|
||||
session
|
||||
});
|
||||
|
||||
// 4. create training bill
|
||||
const { billId } = await createTrainingUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
appName: name,
|
||||
billSource: UsageSourceEnum.training,
|
||||
vectorModel: getVectorModel(dataset.vectorModel)?.name,
|
||||
agentModel: getLLMModel(dataset.agentModel)?.name,
|
||||
session
|
||||
});
|
||||
|
||||
// 5. push chunks to training queue
|
||||
const insertResults = await pushDataListToTrainingQueue({
|
||||
teamId,
|
||||
tmbId,
|
||||
datasetId: dataset._id,
|
||||
collectionId,
|
||||
agentModel: dataset.agentModel,
|
||||
vectorModel: dataset.vectorModel,
|
||||
trainingMode: trainingType,
|
||||
prompt: qaPrompt,
|
||||
billId,
|
||||
data: chunks.map((text, index) => ({
|
||||
q: text,
|
||||
chunkIndex: index
|
||||
})),
|
||||
session
|
||||
});
|
||||
|
||||
return { collectionId, results: insertResults };
|
||||
qaPrompt
|
||||
}
|
||||
});
|
||||
|
||||
return createResult;
|
||||
return {
|
||||
collectionId,
|
||||
results: insertResults
|
||||
};
|
||||
}
|
||||
|
||||
export const config = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { findCollectionAndChild } from '@fastgpt/service/core/dataset/collection/utils';
|
||||
import { delCollectionAndRelatedSources } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { delCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
@@ -32,8 +32,9 @@ async function handler(req: NextApiRequest) {
|
||||
|
||||
// delete
|
||||
await mongoSessionRun((session) =>
|
||||
delCollectionAndRelatedSources({
|
||||
delCollection({
|
||||
collections,
|
||||
delRelatedSource: true,
|
||||
session
|
||||
})
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { AIChatItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
|
||||
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
|
||||
|
||||
export type readCollectionSourceQuery = {};
|
||||
|
||||
@@ -145,6 +146,14 @@ async function handler(
|
||||
if (collection.type === DatasetCollectionTypeEnum.link && collection.rawLink) {
|
||||
return collection.rawLink;
|
||||
}
|
||||
if (collection.type === DatasetCollectionTypeEnum.apiFile && collection.apiFileId) {
|
||||
const apiServer = collection.datasetId.apiServer;
|
||||
if (!apiServer) return Promise.reject('apiServer not found');
|
||||
|
||||
return useApiDatasetRequest({ apiServer }).getFilePreviewUrl({
|
||||
apiFileId: collection.apiFileId
|
||||
});
|
||||
}
|
||||
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
|
||||
if (collection.externalFileId && collection.datasetId.externalReadUrl) {
|
||||
return collection.datasetId.externalReadUrl.replace(
|
||||
|
||||
37
projects/app/src/pages/api/core/dataset/collection/sync.ts
Normal file
37
projects/app/src/pages/api/core/dataset/collection/sync.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { syncCollection } from '@fastgpt/service/core/dataset/collection/utils';
|
||||
|
||||
/*
|
||||
Collection sync
|
||||
1. Check collection type: link, api dataset collection
|
||||
2. Get collection and raw text
|
||||
3. Check whether the original text is the same: skip if same
|
||||
4. Create new collection
|
||||
5. Delete old collection
|
||||
*/
|
||||
export type CollectionSyncBody = {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<CollectionSyncBody>) {
|
||||
const { collectionId } = req.body;
|
||||
|
||||
if (!collectionId) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
}
|
||||
|
||||
const { collection } = await authDatasetCollection({
|
||||
req,
|
||||
authToken: true,
|
||||
collectionId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
return syncCollection(collection);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import {
|
||||
getCollectionAndRawText,
|
||||
reloadCollectionChunks
|
||||
} from '@fastgpt/service/core/dataset/collection/utils';
|
||||
import { delCollectionAndRelatedSources } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import {
|
||||
DatasetCollectionSyncResultEnum,
|
||||
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 { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { collectionId } = req.body as { collectionId: string };
|
||||
|
||||
if (!collectionId) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
}
|
||||
|
||||
const { collection, tmbId } = await authDatasetCollection({
|
||||
req,
|
||||
authToken: true,
|
||||
collectionId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
if (collection.type !== DatasetCollectionTypeEnum.link || !collection.rawLink) {
|
||||
return Promise.reject(DatasetErrEnum.unLinkCollection);
|
||||
}
|
||||
|
||||
const { title, rawText, isSameRawText } = await getCollectionAndRawText({
|
||||
collection
|
||||
});
|
||||
|
||||
if (isSameRawText) {
|
||||
return DatasetCollectionSyncResultEnum.sameRaw;
|
||||
}
|
||||
|
||||
/* Not the same original text, create and reload */
|
||||
|
||||
const vectorModelData = getVectorModel(collection.datasetId.vectorModel);
|
||||
const agentModelData = getLLMModel(collection.datasetId.agentModel);
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
// create training bill
|
||||
const { billId } = await createTrainingUsage({
|
||||
teamId: collection.teamId,
|
||||
tmbId,
|
||||
appName: 'core.dataset.collection.Sync Collection',
|
||||
billSource: UsageSourceEnum.training,
|
||||
vectorModel: vectorModelData.name,
|
||||
agentModel: agentModelData.name,
|
||||
session
|
||||
});
|
||||
|
||||
// create a collection and delete old
|
||||
const newCol = await createOneCollection({
|
||||
teamId: collection.teamId,
|
||||
tmbId: collection.tmbId,
|
||||
parentId: collection.parentId,
|
||||
datasetId: collection.datasetId._id,
|
||||
name: title || collection.name,
|
||||
type: collection.type,
|
||||
trainingType: collection.trainingType,
|
||||
chunkSize: collection.chunkSize,
|
||||
chunkSplitter: collection.chunkSplitter,
|
||||
qaPrompt: collection.qaPrompt,
|
||||
fileId: collection.fileId,
|
||||
rawLink: collection.rawLink,
|
||||
metadata: collection.metadata,
|
||||
createTime: collection.createTime,
|
||||
session
|
||||
});
|
||||
|
||||
// start load
|
||||
await reloadCollectionChunks({
|
||||
collection: {
|
||||
...newCol.toObject(),
|
||||
datasetId: collection.datasetId
|
||||
},
|
||||
tmbId,
|
||||
billId,
|
||||
rawText,
|
||||
session
|
||||
});
|
||||
|
||||
// delete old collection
|
||||
await delCollectionAndRelatedSources({
|
||||
collections: [collection],
|
||||
session
|
||||
});
|
||||
});
|
||||
|
||||
return DatasetCollectionSyncResultEnum.success;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -24,7 +24,8 @@ async function handler(
|
||||
type = DatasetTypeEnum.dataset,
|
||||
avatar,
|
||||
vectorModel = global.vectorModels[0].model,
|
||||
agentModel = getDatasetModel().model
|
||||
agentModel = getDatasetModel().model,
|
||||
apiServer
|
||||
} = req.body;
|
||||
|
||||
// auth
|
||||
@@ -54,7 +55,8 @@ async function handler(
|
||||
vectorModel,
|
||||
agentModel,
|
||||
avatar,
|
||||
type
|
||||
type,
|
||||
apiServer
|
||||
});
|
||||
|
||||
return _id;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* push data to training queue */
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import type {
|
||||
PushDatasetDataProps,
|
||||
PushDatasetDataResponse
|
||||
@@ -39,15 +38,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
insertLen: predictDataLimitLength(collection.trainingType, data)
|
||||
});
|
||||
|
||||
jsonRes<PushDatasetDataResponse>(res, {
|
||||
data: await pushDataListToTrainingQueue({
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
datasetId: collection.datasetId._id,
|
||||
agentModel: collection.datasetId.agentModel,
|
||||
vectorModel: collection.datasetId.vectorModel
|
||||
})
|
||||
return pushDataListToTrainingQueue({
|
||||
...body,
|
||||
teamId,
|
||||
tmbId,
|
||||
datasetId: collection.datasetId._id,
|
||||
agentModel: collection.datasetId.agentModel,
|
||||
vectorModel: collection.datasetId.vectorModel
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ async function handler(req: ApiRequestProps<Query>): Promise<DatasetItemType> {
|
||||
|
||||
return {
|
||||
...dataset,
|
||||
apiServer: dataset.apiServer
|
||||
? {
|
||||
baseUrl: dataset.apiServer.baseUrl,
|
||||
authorization: ''
|
||||
}
|
||||
: undefined,
|
||||
permission,
|
||||
vectorModel: getVectorModel(dataset.vectorModel),
|
||||
agentModel: getLLMModel(dataset.agentModel)
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { rawText2Chunks, readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import {
|
||||
OwnerPermissionVal,
|
||||
WritePermissionVal
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { authCollectionFile } from '@fastgpt/service/support/permission/auth/file';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
|
||||
export type PostPreviewFilesChunksProps = {
|
||||
datasetId: string;
|
||||
type: DatasetSourceReadTypeEnum;
|
||||
sourceId: string;
|
||||
|
||||
chunkSize: number;
|
||||
overlapRatio: number;
|
||||
customSplitChar?: string;
|
||||
|
||||
// Read params
|
||||
selector?: string;
|
||||
isQAImport?: boolean;
|
||||
externalFileId?: string;
|
||||
};
|
||||
export type PreviewChunksResponse = {
|
||||
q: string;
|
||||
@@ -23,8 +31,17 @@ export type PreviewChunksResponse = {
|
||||
async function handler(
|
||||
req: ApiRequestProps<PostPreviewFilesChunksProps>
|
||||
): Promise<PreviewChunksResponse> {
|
||||
const { type, sourceId, chunkSize, customSplitChar, overlapRatio, selector, isQAImport } =
|
||||
req.body;
|
||||
const {
|
||||
type,
|
||||
sourceId,
|
||||
chunkSize,
|
||||
customSplitChar,
|
||||
overlapRatio,
|
||||
selector,
|
||||
isQAImport,
|
||||
datasetId,
|
||||
externalFileId
|
||||
} = req.body;
|
||||
|
||||
if (!sourceId) {
|
||||
throw new Error('sourceId is empty');
|
||||
@@ -33,25 +50,40 @@ async function handler(
|
||||
throw new Error('chunkSize is too large, should be less than 30000');
|
||||
}
|
||||
|
||||
const { teamId } = await (async () => {
|
||||
const { teamId, apiServer } = await (async () => {
|
||||
if (type === DatasetSourceReadTypeEnum.fileLocal) {
|
||||
return authCollectionFile({
|
||||
const res = await authCollectionFile({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
fileId: sourceId,
|
||||
per: OwnerPermissionVal
|
||||
});
|
||||
return {
|
||||
teamId: res.teamId
|
||||
};
|
||||
}
|
||||
return authCert({ req, authApiKey: true, authToken: true });
|
||||
const { dataset } = await authDataset({
|
||||
req,
|
||||
authApiKey: true,
|
||||
authToken: true,
|
||||
datasetId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
return {
|
||||
teamId: dataset.teamId,
|
||||
apiServer: dataset.apiServer
|
||||
};
|
||||
})();
|
||||
|
||||
const rawText = await readDatasetSourceRawText({
|
||||
teamId,
|
||||
type,
|
||||
sourceId: sourceId,
|
||||
sourceId,
|
||||
selector,
|
||||
isQAImport
|
||||
isQAImport,
|
||||
apiServer,
|
||||
externalFileId
|
||||
});
|
||||
|
||||
return rawText2Chunks({
|
||||
|
||||
@@ -27,32 +27,51 @@ export type GetDatasetListBody = {
|
||||
|
||||
async function handler(req: ApiRequestProps<GetDatasetListBody>) {
|
||||
const { parentId, type, searchKey } = req.body;
|
||||
// 凭证校验
|
||||
const {
|
||||
dataset: parentDataset,
|
||||
teamId,
|
||||
tmbId,
|
||||
permission: myPer
|
||||
} = await (async () => {
|
||||
if (parentId) {
|
||||
return await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
per: ReadPermissionVal,
|
||||
datasetId: parentId
|
||||
|
||||
// Auth user permission
|
||||
const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([
|
||||
authUserPer({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
per: ReadPermissionVal
|
||||
}),
|
||||
...(parentId
|
||||
? [
|
||||
authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
per: ReadPermissionVal,
|
||||
datasetId: parentId
|
||||
})
|
||||
]
|
||||
: [])
|
||||
]);
|
||||
|
||||
// Get team all app permissions
|
||||
const [perList, myGroupMap] = await Promise.all([
|
||||
MongoResourcePermission.find({
|
||||
resourceType: PerResourceTypeEnum.dataset,
|
||||
teamId,
|
||||
resourceId: {
|
||||
$exists: true
|
||||
}
|
||||
}).lean(),
|
||||
getGroupsByTmbId({
|
||||
tmbId,
|
||||
teamId
|
||||
}).then((item) => {
|
||||
const map = new Map<string, 1>();
|
||||
item.forEach((item) => {
|
||||
map.set(String(item._id), 1);
|
||||
});
|
||||
}
|
||||
return {
|
||||
...(await authUserPer({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
per: ReadPermissionVal
|
||||
})),
|
||||
dataset: undefined
|
||||
};
|
||||
})();
|
||||
return map;
|
||||
})
|
||||
]);
|
||||
const myPerList = perList.filter(
|
||||
(item) => String(item.tmbId) === String(tmbId) || myGroupMap.has(String(item.groupId))
|
||||
);
|
||||
|
||||
const findDatasetQuery = (() => {
|
||||
const searchMatch = searchKey
|
||||
@@ -63,61 +82,43 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
|
||||
]
|
||||
}
|
||||
: {};
|
||||
// Filter apps by permission, if not owner, only get apps that I have permission to access
|
||||
const appIdQuery = teamPer.isOwner
|
||||
? {}
|
||||
: { _id: { $in: myPerList.map((item) => item.resourceId) } };
|
||||
|
||||
if (searchKey) {
|
||||
return {
|
||||
...appIdQuery,
|
||||
teamId,
|
||||
...searchMatch
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...appIdQuery,
|
||||
teamId,
|
||||
...(type ? (Array.isArray(type) ? { type: { $in: type } } : { type }) : {}),
|
||||
...parseParentIdInMongo(parentId)
|
||||
};
|
||||
})();
|
||||
|
||||
const myGroupIds = (
|
||||
await getGroupsByTmbId({
|
||||
tmbId,
|
||||
teamId
|
||||
const myDatasets = await MongoDataset.find(findDatasetQuery)
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
).map((item) => String(item._id));
|
||||
.lean();
|
||||
|
||||
const [myDatasets, perList] = await Promise.all([
|
||||
MongoDataset.find(findDatasetQuery)
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
.lean(),
|
||||
MongoResourcePermission.find({
|
||||
resourceType: PerResourceTypeEnum.dataset,
|
||||
teamId,
|
||||
resourceId: {
|
||||
$exists: true
|
||||
}
|
||||
}).lean()
|
||||
]);
|
||||
|
||||
const filterDatasets = myDatasets
|
||||
const formatDatasets = myDatasets
|
||||
.map((dataset) => {
|
||||
const { Per, privateDataset } = (() => {
|
||||
const myPerList = perList.filter(
|
||||
(item) =>
|
||||
String(item.tmbId) === String(tmbId) || myGroupIds.includes(String(item.groupId))
|
||||
);
|
||||
|
||||
const getPer = (datasetId: string) => {
|
||||
const tmbPer = myPerList.find(
|
||||
(item) => String(item.resourceId) === datasetId && !!item.tmbId
|
||||
)?.permission;
|
||||
const groupPer = getGroupPer(
|
||||
myPerList
|
||||
.filter(
|
||||
(item) =>
|
||||
String(item.resourceId) === datasetId && myGroupIds.includes(String(item.groupId))
|
||||
)
|
||||
.filter((item) => String(item.resourceId) === datasetId && !!item.groupId)
|
||||
.map((item) => item.permission)
|
||||
);
|
||||
|
||||
@@ -126,14 +127,14 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
|
||||
return {
|
||||
Per: new DatasetPermission({
|
||||
per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal,
|
||||
isOwner: String(dataset.tmbId) === String(tmbId) || myPer.isOwner
|
||||
isOwner: String(dataset.tmbId) === String(tmbId) || teamPer.isOwner
|
||||
}),
|
||||
privateDataset: dataset.type === 'folder' ? clbCount <= 1 : clbCount === 0
|
||||
};
|
||||
};
|
||||
// inherit
|
||||
if (dataset.inheritPermission && parentDataset && dataset.type !== DatasetTypeEnum.folder) {
|
||||
return getPer(String(parentDataset._id));
|
||||
if (dataset.inheritPermission && parentId && dataset.type !== DatasetTypeEnum.folder) {
|
||||
return getPer(String(parentId));
|
||||
} else {
|
||||
return getPer(String(dataset._id));
|
||||
}
|
||||
@@ -148,7 +149,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
|
||||
.filter((app) => app.permission.hasReadPer);
|
||||
|
||||
const data = await Promise.all(
|
||||
filterDatasets.map<DatasetListItemType>((item) => ({
|
||||
formatDatasets.map<DatasetListItemType>((item) => ({
|
||||
_id: item._id,
|
||||
avatar: item.avatar,
|
||||
name: item.name,
|
||||
|
||||
@@ -41,8 +41,18 @@ async function handler(
|
||||
req: ApiRequestProps<DatasetUpdateBody, DatasetUpdateQuery>,
|
||||
_res: ApiResponseType<any>
|
||||
): Promise<DatasetUpdateResponse> {
|
||||
const { id, parentId, name, avatar, intro, agentModel, websiteConfig, externalReadUrl, status } =
|
||||
req.body;
|
||||
const {
|
||||
id,
|
||||
parentId,
|
||||
name,
|
||||
avatar,
|
||||
intro,
|
||||
agentModel,
|
||||
websiteConfig,
|
||||
externalReadUrl,
|
||||
apiServer,
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
if (!id) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
@@ -103,6 +113,10 @@ async function handler(
|
||||
...(status && { status }),
|
||||
...(intro !== undefined && { intro }),
|
||||
...(externalReadUrl !== undefined && { externalReadUrl }),
|
||||
...(!!apiServer?.baseUrl && { 'apiServer.baseUrl': apiServer.baseUrl }),
|
||||
...(!!apiServer?.authorization && {
|
||||
'apiServer.authorization': apiServer.authorization
|
||||
}),
|
||||
...(isMove && { inheritPermission: true })
|
||||
},
|
||||
{ session }
|
||||
@@ -165,7 +179,8 @@ async function updateTraining({
|
||||
{
|
||||
$set: {
|
||||
model: agentModel,
|
||||
retryCount: 5
|
||||
retryCount: 5,
|
||||
lockTime: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
|
||||
import { connectToDatabase } from '@/service/mongo';
|
||||
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 { getVectorModel } from '@fastgpt/service/core/ai/model';
|
||||
import { checkTeamAIPoints } from '@fastgpt/service/support/permission/teamLimit';
|
||||
import { EmbeddingTypeEnm } from '@fastgpt/global/core/ai/constants';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
|
||||
type Props = {
|
||||
input: string | string[];
|
||||
@@ -18,65 +17,58 @@ type Props = {
|
||||
type: `${EmbeddingTypeEnm}`;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
let { input, model, billId, type } = req.body as Props;
|
||||
await connectToDatabase();
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
let { input, model, billId, type } = req.body as Props;
|
||||
|
||||
if (!Array.isArray(input) && typeof input !== 'string') {
|
||||
throw new Error('input is nor array or string');
|
||||
if (!Array.isArray(input) && typeof input !== 'string') {
|
||||
throw new Error('input is nor array or string');
|
||||
}
|
||||
|
||||
const query = Array.isArray(input) ? input[0] : input;
|
||||
|
||||
const { teamId, tmbId, apikey, authType } = await authCert({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
await checkTeamAIPoints(teamId);
|
||||
|
||||
const { tokens, vectors } = await getVectorsByText({
|
||||
input: query,
|
||||
model: getVectorModel(model),
|
||||
type
|
||||
});
|
||||
|
||||
res.json({
|
||||
object: 'list',
|
||||
data: vectors.map((item, index) => ({
|
||||
object: 'embedding',
|
||||
index: index,
|
||||
embedding: item
|
||||
})),
|
||||
model,
|
||||
usage: {
|
||||
prompt_tokens: tokens,
|
||||
total_tokens: tokens
|
||||
}
|
||||
});
|
||||
|
||||
const query = Array.isArray(input) ? input[0] : input;
|
||||
const { totalPoints } = pushGenerateVectorUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
tokens,
|
||||
model,
|
||||
billId,
|
||||
source: getUsageSourceByAuthType({ authType })
|
||||
});
|
||||
|
||||
const { teamId, tmbId, apikey, authType } = await authCert({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
await checkTeamAIPoints(teamId);
|
||||
|
||||
const { tokens, vectors } = await getVectorsByText({
|
||||
input: query,
|
||||
model: getVectorModel(model),
|
||||
type
|
||||
});
|
||||
|
||||
res.json({
|
||||
object: 'list',
|
||||
data: vectors.map((item, index) => ({
|
||||
object: 'embedding',
|
||||
index: index,
|
||||
embedding: item
|
||||
})),
|
||||
model,
|
||||
usage: {
|
||||
prompt_tokens: tokens,
|
||||
total_tokens: tokens
|
||||
}
|
||||
});
|
||||
|
||||
const { totalPoints } = pushGenerateVectorUsage({
|
||||
teamId,
|
||||
tmbId,
|
||||
tokens,
|
||||
model,
|
||||
billId,
|
||||
source: getUsageSourceByAuthType({ authType })
|
||||
});
|
||||
|
||||
if (apikey) {
|
||||
updateApiKeyUsage({
|
||||
apikey,
|
||||
totalPoints: totalPoints
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
if (apikey) {
|
||||
updateApiKeyUsage({
|
||||
apikey,
|
||||
totalPoints: totalPoints
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
@@ -50,13 +50,13 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { errors },
|
||||
handleSubmit
|
||||
} = useForm({
|
||||
defaultValues: appDetail
|
||||
});
|
||||
const avatar = getValues('avatar');
|
||||
const avatar = watch('avatar');
|
||||
|
||||
// submit config
|
||||
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(
|
||||
|
||||
@@ -283,7 +283,7 @@ const RenderList = React.memo(function RenderList({
|
||||
{t(item.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
|
||||
{t(item.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
{showCost && <CostTooltip cost={item.currentCost} />}
|
||||
|
||||
@@ -35,8 +35,7 @@ export const compareSimpleAppSnapshot = (
|
||||
ttsConfig: appForm1.chatConfig?.ttsConfig || undefined,
|
||||
whisperConfig: appForm1.chatConfig?.whisperConfig || undefined,
|
||||
chatInputGuide: appForm1.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined,
|
||||
instruction: appForm1.chatConfig?.instruction || ''
|
||||
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined
|
||||
},
|
||||
{
|
||||
welcomeText: appForm2.chatConfig?.welcomeText || '',
|
||||
@@ -45,8 +44,7 @@ export const compareSimpleAppSnapshot = (
|
||||
ttsConfig: appForm2.chatConfig?.ttsConfig || undefined,
|
||||
whisperConfig: appForm2.chatConfig?.whisperConfig || undefined,
|
||||
chatInputGuide: appForm2.chatConfig?.chatInputGuide || undefined,
|
||||
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined,
|
||||
instruction: appForm2.chatConfig?.instruction || ''
|
||||
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -223,25 +223,44 @@ function ExportPopover({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const { flowData2StoreDataAndCheck } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
|
||||
|
||||
const onExportWorkflow = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
t('app:export_config_successful')
|
||||
);
|
||||
}
|
||||
}, [chatConfig, copyData, flowData2StoreDataAndCheck, t]);
|
||||
const onExportWorkflow = useCallback(
|
||||
async (mode: 'copy' | 'json') => {
|
||||
const data = flowData2StoreData();
|
||||
if (data) {
|
||||
if (mode === 'copy') {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
t('app:export_config_successful')
|
||||
);
|
||||
} else if (mode === 'json') {
|
||||
fileDownload({
|
||||
text: JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
type: 'application/json;charset=utf-8',
|
||||
filename: `${appName}.json`
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[appName, chatConfig, copyData, flowData2StoreData, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
@@ -269,7 +288,7 @@ function ExportPopover({
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={onExportWorkflow}
|
||||
onClick={() => onExportWorkflow('copy')}
|
||||
>
|
||||
<MyIcon name={'copy'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.copy_to_clipboard')}</Box>
|
||||
@@ -284,25 +303,7 @@ function ExportPopover({
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
|
||||
if (!data) return;
|
||||
|
||||
fileDownload({
|
||||
text: JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
type: 'application/json;charset=utf-8',
|
||||
filename: `${appName}.json`
|
||||
});
|
||||
}}
|
||||
onClick={() => onExportWorkflow('json')}
|
||||
>
|
||||
<MyIcon name={'configmap'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.export_to_json')}</Box>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Flex,
|
||||
Grid,
|
||||
@@ -16,7 +21,6 @@ import type {
|
||||
} from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import { useReactFlow, XYPosition } from 'reactflow';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
@@ -24,6 +28,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
getPreviewPluginNode,
|
||||
getSystemPlugTemplates,
|
||||
getPluginGroups,
|
||||
getSystemPluginPaths
|
||||
} from '@/web/core/app/api/plugin';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
@@ -45,17 +50,28 @@ import { useWorkflowUtils } from './hooks/useUtils';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
|
||||
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
|
||||
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
|
||||
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
|
||||
import MyAvatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
type ModuleTemplateListProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
type RenderHeaderProps = {
|
||||
templateType: TemplateTypeEnum;
|
||||
onClose: () => void;
|
||||
parentId: ParentIdType;
|
||||
searchKey: string;
|
||||
loadNodeTemplates: (params: any) => void;
|
||||
setSearchKey: (searchKey: string) => void;
|
||||
onUpdateParentId: (parentId: ParentIdType) => void;
|
||||
};
|
||||
type RenderListProps = {
|
||||
templates: NodeTemplateListItemType[];
|
||||
type: TemplateTypeEnum;
|
||||
@@ -73,8 +89,6 @@ enum TemplateTypeEnum {
|
||||
const sliderWidth = 460;
|
||||
|
||||
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
@@ -183,18 +197,6 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
[basicNodes, teamAndSystemApps]
|
||||
);
|
||||
|
||||
// Get paths
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => {
|
||||
if (templateType === TemplateTypeEnum.teamPlugin) return getAppFolderPath(parentId);
|
||||
return getSystemPluginPaths(parentId);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const onUpdateParentId = useCallback(
|
||||
(parentId: ParentIdType) => {
|
||||
loadNodeTemplates({
|
||||
@@ -251,119 +253,15 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
userSelect={'none'}
|
||||
overflow={isOpen ? 'none' : 'hidden'}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
{/* Tabs */}
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('common:core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/systemPlugin',
|
||||
label: t('common:core.module.template.System Plugin'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('common:core.module.template.Team app'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
width={'100%'}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={(e) => {
|
||||
loadNodeTemplates({
|
||||
type: e as TemplateTypeEnum,
|
||||
parentId: ''
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
|
||||
bg={'myGray.100'}
|
||||
_hover={{
|
||||
bg: 'myGray.200',
|
||||
'& svg': {
|
||||
color: 'primary.600'
|
||||
}
|
||||
}}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
{/* Search */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={
|
||||
templateType === TemplateTypeEnum.teamPlugin
|
||||
? t('common:plugin.Search_app')
|
||||
: t('common:plugin.Search plugin')
|
||||
}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/app/list')}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:create')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.systemPlugin &&
|
||||
feConfigs.systemPluginCourseUrl && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:plugin.contribute')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{/* paths */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) &&
|
||||
!searchKey &&
|
||||
parentId && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
<RenderHeader
|
||||
templateType={templateType}
|
||||
onClose={onClose}
|
||||
parentId={parentId}
|
||||
onUpdateParentId={onUpdateParentId}
|
||||
searchKey={searchKey}
|
||||
loadNodeTemplates={loadNodeTemplates}
|
||||
setSearchKey={setSearchKey}
|
||||
/>
|
||||
<RenderList
|
||||
templates={templates}
|
||||
type={templateType}
|
||||
@@ -378,6 +276,146 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
|
||||
export default React.memo(NodeTemplatesModal);
|
||||
|
||||
const RenderHeader = React.memo(function RenderHeader({
|
||||
templateType,
|
||||
onClose,
|
||||
parentId,
|
||||
searchKey,
|
||||
setSearchKey,
|
||||
loadNodeTemplates,
|
||||
onUpdateParentId
|
||||
}: RenderHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const router = useRouter();
|
||||
|
||||
// Get paths
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => {
|
||||
if (templateType === TemplateTypeEnum.teamPlugin) return getAppFolderPath(parentId);
|
||||
return getSystemPluginPaths(parentId);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
|
||||
{/* Tabs */}
|
||||
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<FillRowTabs
|
||||
list={[
|
||||
{
|
||||
icon: 'core/modules/basicNode',
|
||||
label: t('common:core.module.template.Basic Node'),
|
||||
value: TemplateTypeEnum.basic
|
||||
},
|
||||
{
|
||||
icon: 'phoneTabbar/tool',
|
||||
label: t('common:navbar.Toolkit'),
|
||||
value: TemplateTypeEnum.systemPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/modules/teamPlugin',
|
||||
label: t('common:core.module.template.Team app'),
|
||||
value: TemplateTypeEnum.teamPlugin
|
||||
}
|
||||
]}
|
||||
width={'100%'}
|
||||
py={'5px'}
|
||||
value={templateType}
|
||||
onChange={(e) => {
|
||||
loadNodeTemplates({
|
||||
type: e as TemplateTypeEnum,
|
||||
parentId: ''
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* close icon */}
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
|
||||
bg={'myGray.100'}
|
||||
_hover={{
|
||||
bg: 'myGray.200',
|
||||
'& svg': {
|
||||
color: 'primary.600'
|
||||
}
|
||||
}}
|
||||
variant={'grayBase'}
|
||||
aria-label={''}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Flex>
|
||||
{/* Search */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) && (
|
||||
<Flex mt={2} alignItems={'center'} h={10}>
|
||||
<InputGroup mr={4} h={'full'}>
|
||||
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
|
||||
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
h={'full'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={
|
||||
templateType === TemplateTypeEnum.teamPlugin
|
||||
? t('common:plugin.Search_app')
|
||||
: t('common:plugin.Search plugin')
|
||||
}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Box flex={1} />
|
||||
{templateType === TemplateTypeEnum.teamPlugin && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/app/list')}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:create')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
{templateType === TemplateTypeEnum.systemPlugin && feConfigs.systemPluginCourseUrl && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:plugin.contribute')}</Box>
|
||||
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{/* paths */}
|
||||
{(templateType === TemplateTypeEnum.teamPlugin ||
|
||||
templateType === TemplateTypeEnum.systemPlugin) &&
|
||||
!searchKey &&
|
||||
parentId && (
|
||||
<Flex alignItems={'center'} mt={2}>
|
||||
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
const RenderList = React.memo(function RenderList({
|
||||
templates,
|
||||
type,
|
||||
@@ -386,10 +424,9 @@ const RenderList = React.memo(function RenderList({
|
||||
setParentId
|
||||
}: RenderListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs, setLoading } = useSystemStore();
|
||||
const { setLoading } = useSystemStore();
|
||||
|
||||
const { isPc } = useSystem();
|
||||
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
|
||||
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { computedNewNodeName } = useWorkflowUtils();
|
||||
@@ -398,18 +435,48 @@ const RenderList = React.memo(function RenderList({
|
||||
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
|
||||
const formatTemplates = useMemo<NodeTemplateListType>(() => {
|
||||
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
||||
templates.forEach((item) => {
|
||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||
if (index === -1) return;
|
||||
copy[index].list.push(item);
|
||||
});
|
||||
return copy.filter((item) => item.list.length > 0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [templates, parentId]);
|
||||
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const onAddNode = useCallback(
|
||||
const formatTemplatesArray = useMemo<{ list: NodeTemplateListType; label: string }[]>(() => {
|
||||
const data = (() => {
|
||||
if (type === TemplateTypeEnum.systemPlugin) {
|
||||
return pluginGroups.map((group) => {
|
||||
const copy: NodeTemplateListType = group.groupTypes.map((type) => ({
|
||||
list: [],
|
||||
type: type.typeId,
|
||||
label: type.typeName
|
||||
}));
|
||||
templates.forEach((item) => {
|
||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||
if (index === -1) return;
|
||||
copy[index].list.push(item);
|
||||
});
|
||||
return {
|
||||
label: group.groupName,
|
||||
list: copy.filter((item) => item.list.length > 0)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
|
||||
templates.forEach((item) => {
|
||||
const index = copy.findIndex((template) => template.type === item.templateType);
|
||||
if (index === -1) return;
|
||||
copy[index].list.push(item);
|
||||
});
|
||||
return [
|
||||
{
|
||||
label: '',
|
||||
list: copy.filter((item) => item.list.length > 0)
|
||||
}
|
||||
];
|
||||
})();
|
||||
return data.filter(({ list }) => list.length > 0);
|
||||
}, [type, templates, pluginGroups]);
|
||||
|
||||
const onAddNode = useMemoizedFn(
|
||||
async ({
|
||||
template,
|
||||
position
|
||||
@@ -527,8 +594,7 @@ const RenderList = React.memo(function RenderList({
|
||||
.concat(newNodes);
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
|
||||
}
|
||||
);
|
||||
|
||||
const gridStyle = useMemo(() => {
|
||||
@@ -551,118 +617,157 @@ const RenderList = React.memo(function RenderList({
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={t('app:module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
|
||||
<Box mx={'auto'}>
|
||||
{formatTemplates.map((item, i) => (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
_notLast={{ mb: 5 }}
|
||||
>
|
||||
{item.label && formatTemplates.length > 1 && (
|
||||
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item, i) => {
|
||||
return (
|
||||
<Box
|
||||
key={item.type}
|
||||
css={css({
|
||||
span: {
|
||||
display: 'block'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
||||
{item.list.map((template) => (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
||||
{item.list.map((template) => {
|
||||
return (
|
||||
<MyTooltip
|
||||
key={template.id}
|
||||
placement={'right'}
|
||||
label={
|
||||
<Box py={2}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
|
||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
<CostTooltip
|
||||
cost={template.currentCost}
|
||||
hasTokenFee={template.hasTokenFee}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
draggable={!template.isFolder}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.isFolder) {
|
||||
return setParentId(template.id);
|
||||
}
|
||||
if (isPc) {
|
||||
return onAddNode({
|
||||
template,
|
||||
position: { x: sliderWidth * 1.5, y: 200 }
|
||||
});
|
||||
}
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
whiteSpace={'nowrap'}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
>
|
||||
<MyAvatar
|
||||
src={template.avatar}
|
||||
w={'1.75rem'}
|
||||
w={gridStyle.avatarSize}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
|
||||
<Box
|
||||
color={'myGray.900'}
|
||||
fontWeight={'500'}
|
||||
fontSize={'sm'}
|
||||
flex={'1 0 0'}
|
||||
ml={3}
|
||||
className="textEllipsis"
|
||||
>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
{t(template.intro as any) || t('common:core.workflow.Not intro')}
|
||||
</Box>
|
||||
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'sm'}
|
||||
draggable={!template.isFolder}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.isFolder) {
|
||||
return setParentId(template.id);
|
||||
}
|
||||
if (isPc) {
|
||||
return onAddNode({
|
||||
template,
|
||||
position: { x: sliderWidth * 1.5, y: 200 }
|
||||
});
|
||||
}
|
||||
onAddNode({
|
||||
template,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={template.avatar}
|
||||
w={gridStyle.avatarSize}
|
||||
objectFit={'contain'}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
<Box ml={3} flex={'1'}>
|
||||
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
{gridStyle.authorInName && template.author !== undefined && (
|
||||
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
|
||||
{`by ${template.author || feConfigs.systemTitle}`}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
||||
<HStack spacing={1} maxW={'120px'}>
|
||||
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||
<Box fontSize={'xs'} className="textEllipsis">
|
||||
{template.author}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
||||
<HStack spacing={1} maxW={'120px'} flexShrink={0}>
|
||||
<MyAvatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||
<Box fontSize={'xs'} className="textEllipsis">
|
||||
{template.author}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
return templates.length === 0 ? (
|
||||
<EmptyTip text={t('app:module.No Modules')} />
|
||||
) : (
|
||||
<Box flex={'1 0 0'} overflow={'overlay'} px={formatTemplatesArray.length > 1 ? 2 : 5}>
|
||||
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
|
||||
{formatTemplatesArray.length > 1 ? (
|
||||
<>
|
||||
{formatTemplatesArray.map(({ list, label }, index) => (
|
||||
<AccordionItem key={index} border={'none'}>
|
||||
<AccordionButton
|
||||
fontSize={'sm'}
|
||||
fontWeight={'500'}
|
||||
color={'myGray.900'}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
px={3}
|
||||
>
|
||||
{t(label as any)}
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={0}>
|
||||
<PluginListRender list={list} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
|
||||
)}
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ export const useDebug = () => {
|
||||
});
|
||||
return Promise.reject();
|
||||
}
|
||||
}, [edges, onUpdateNodeError, t, toast]);
|
||||
}, [edges, getNodes, onUpdateNodeError, t, toast]);
|
||||
|
||||
const openDebugNode = useCallback(
|
||||
async ({ entryNodeId }: { entryNodeId: string }) => {
|
||||
@@ -163,7 +163,7 @@ export const useDebug = () => {
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
globalVariables: defaultGlobalVariables
|
||||
variables: defaultGlobalVariables
|
||||
}
|
||||
});
|
||||
const { register, getValues, setValue, handleSubmit } = variablesForm;
|
||||
@@ -207,11 +207,11 @@ export const useDebug = () => {
|
||||
: node
|
||||
),
|
||||
runtimeEdges: runtimeEdges,
|
||||
variables: data.globalVariables
|
||||
variables: data.variables
|
||||
});
|
||||
|
||||
// Filter global variables and set them as default global variable values
|
||||
setDefaultGlobalVariables(data.globalVariables);
|
||||
setDefaultGlobalVariables(data.variables);
|
||||
|
||||
onClose();
|
||||
};
|
||||
@@ -225,8 +225,7 @@ export const useDebug = () => {
|
||||
}
|
||||
|
||||
const hasRequiredGlobalVar =
|
||||
e.globalVariables &&
|
||||
Object.values(e.globalVariables).some((item) => item.type === 'required');
|
||||
e.variables && Object.values(e.variables).some((item) => item.type === 'required');
|
||||
|
||||
if (hasRequiredGlobalVar) {
|
||||
setCurrentTab(TabEnum.global);
|
||||
@@ -260,7 +259,7 @@ export const useDebug = () => {
|
||||
{filteredVar.map((item) => (
|
||||
<VariableInputItem
|
||||
key={item.id}
|
||||
item={{ ...item, key: `globalVariables.${item.key}` }}
|
||||
item={{ ...item, key: item.key }}
|
||||
variablesForm={variablesForm}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -33,13 +33,13 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const CustomComponent = useMemo(() => {
|
||||
const quoteList = inputs.filter((item) => item.canEdit);
|
||||
const tokenLimit = (() => {
|
||||
let maxTokens = 13000;
|
||||
let maxTokens = 16000;
|
||||
|
||||
nodeList.forEach((item) => {
|
||||
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
|
||||
const model =
|
||||
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
|
||||
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
|
||||
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 16000;
|
||||
|
||||
maxTokens = Math.max(maxTokens, quoteMaxToken);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,8 @@ const InputTypeConfig = ({
|
||||
FlowNodeInputTypeEnum.JSONEditor,
|
||||
FlowNodeInputTypeEnum.numberInput,
|
||||
FlowNodeInputTypeEnum.switch,
|
||||
FlowNodeInputTypeEnum.select
|
||||
FlowNodeInputTypeEnum.select,
|
||||
VariableInputEnum.custom
|
||||
];
|
||||
|
||||
return list.includes(inputType as FlowNodeInputTypeEnum);
|
||||
@@ -301,7 +302,8 @@ const InputTypeConfig = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.input && (
|
||||
{(inputType === FlowNodeInputTypeEnum.input ||
|
||||
inputType === VariableInputEnum.custom) && (
|
||||
<MyTextarea
|
||||
{...register('defaultValue')}
|
||||
bg={'myGray.50'}
|
||||
|
||||
@@ -373,7 +373,7 @@ const NodeCard = (props: Props) => {
|
||||
{RenderToolHandle}
|
||||
|
||||
<ConfirmSyncModal />
|
||||
<EditTitleModal maxLength={20} />
|
||||
<EditTitleModal maxLength={50} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
import { defaultDatasetMaxTokens } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
@@ -31,13 +32,13 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
|
||||
});
|
||||
|
||||
const tokenLimit = useMemo(() => {
|
||||
let maxTokens = 13000;
|
||||
let maxTokens = defaultDatasetMaxTokens;
|
||||
|
||||
nodeList.forEach((item) => {
|
||||
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
|
||||
const model =
|
||||
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
|
||||
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
|
||||
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || defaultDatasetMaxTokens;
|
||||
|
||||
maxTokens = Math.max(maxTokens, quoteMaxToken);
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ const TemplateMarketModal = ({
|
||||
position={'relative'}
|
||||
>
|
||||
<Avatar src={'/imgs/app/templateFill.svg'} w={'2rem'} objectFit={'fill'} />
|
||||
<Box color={'myGray.900'}>{t('app:templateMarket.Template_market')}</Box>
|
||||
<Box color={'myGray.900'}>{t('app:template_market')}</Box>
|
||||
|
||||
<Box flex={'1'} />
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TemplateMarketModal from './components/TemplateMarketModal';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
|
||||
const CreateModal = dynamic(() => import('./components/CreateModal'));
|
||||
const EditFolderModal = dynamic(
|
||||
@@ -134,7 +135,7 @@ const MyApps = () => {
|
||||
flex={'1 0 0'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
pr={folderDetail ? [3, 2] : [3, 10]}
|
||||
pr={folderDetail ? [3, 2] : [3, 8]}
|
||||
pl={3}
|
||||
overflowY={'auto'}
|
||||
overflowX={'hidden'}
|
||||
@@ -179,6 +180,33 @@ const MyApps = () => {
|
||||
|
||||
{isPc && RenderSearchInput}
|
||||
|
||||
{isPc && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
gap={1.5}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.250'}
|
||||
h={9}
|
||||
px={4}
|
||||
fontSize={'14px'}
|
||||
fontWeight={'medium'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
_hover={{
|
||||
bg: 'primary.50',
|
||||
color: 'primary.600'
|
||||
}}
|
||||
onClick={() => setTemplateModalType('all')}
|
||||
>
|
||||
<MyImage src={'/imgs/app/templateFill.svg'} w={'18px'} />
|
||||
{t('app:template_market')}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{(folderDetail
|
||||
? folderDetail.permission.hasWritePer && folderDetail?.type !== AppTypeEnum.httpPlugin
|
||||
: userInfo?.team.permission.hasWritePer) && (
|
||||
@@ -218,16 +246,20 @@ const MyApps = () => {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: '/imgs/app/templateFill.svg',
|
||||
label: t('app:template_market'),
|
||||
description: t('app:template_market_description'),
|
||||
onClick: () => setTemplateModalType('all')
|
||||
}
|
||||
]
|
||||
},
|
||||
...(isPc
|
||||
? []
|
||||
: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: '/imgs/app/templateFill.svg',
|
||||
label: t('app:template_market'),
|
||||
description: t('app:template_market_description'),
|
||||
onClick: () => setTemplateModalType('all')
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
{
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useChatBox } from '@/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { Box, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatContext } from '@/web/core/chat/context/chatContext';
|
||||
|
||||
const ToolMenu = ({
|
||||
history,
|
||||
@@ -16,7 +17,8 @@ const ToolMenu = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { onExportChat } = useChatBox();
|
||||
const router = useRouter();
|
||||
|
||||
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
|
||||
|
||||
return history.length > 0 ? (
|
||||
<MyMenu
|
||||
@@ -35,12 +37,7 @@ const ToolMenu = ({
|
||||
icon: 'core/chat/chatLight',
|
||||
label: t('common:core.chat.New Chat'),
|
||||
onClick: () => {
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
chatId: ''
|
||||
}
|
||||
});
|
||||
onChangeChatId();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -73,7 +73,7 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) =>
|
||||
|
||||
// website config
|
||||
const { openConfirm: openWebSyncConfirm, ConfirmModal: ConfirmWebSyncModal } = useConfirm({
|
||||
content: t('common:core.dataset.collection.Start Sync Tip')
|
||||
content: t('dataset:start_sync_website_tip')
|
||||
});
|
||||
const {
|
||||
isOpen: isOpenWebsiteModal,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -55,7 +55,6 @@ const Header = ({}: {}) => {
|
||||
const { parentId = '' } = router.query as { parentId: string };
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const lastSearch = useRef('');
|
||||
const { searchText, setSearchText, total, getData, pageNum, onOpenWebsiteModal } =
|
||||
useContextSelector(CollectionPageContext, (v) => v);
|
||||
|
||||
@@ -386,6 +385,34 @@ const Header = ({}: {}) => {
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{/* apiDataset */}
|
||||
{datasetDetail?.type === DatasetTypeEnum.apiDataset && (
|
||||
<Flex
|
||||
px={3.5}
|
||||
py={2}
|
||||
borderRadius={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'primary.500'}
|
||||
overflow={'hidden'}
|
||||
color={'white'}
|
||||
onClick={() =>
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: TabEnum.import,
|
||||
source: ImportDataSourceEnum.apiDataset
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flex h={'20px'} alignItems={'center'}>
|
||||
<MyIcon name={'common/folderImport'} mr={2} w={'18px'} h={'18px'} color={'white'} />
|
||||
</Flex>
|
||||
<Box h={'20px'} fontSize={'sm'} fontWeight={'500'}>
|
||||
{t('dataset:add_file')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
DatasetStatusEnum,
|
||||
DatasetCollectionSyncResultMap
|
||||
DatasetCollectionSyncResultMap,
|
||||
DatasetTypeEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import { TabEnum } from '../../index';
|
||||
@@ -41,7 +42,6 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollectionPageContext } from './Context';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import {
|
||||
@@ -60,7 +60,6 @@ const CollectionCard = () => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { datasetT } = useI18n();
|
||||
const { datasetDetail, loadDatasetDetail } = useContextSelector(DatasetPageContext, (v) => v);
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
@@ -68,9 +67,6 @@ const CollectionCard = () => {
|
||||
content: t('common:dataset.Confirm to delete the file'),
|
||||
type: 'delete'
|
||||
});
|
||||
const { openConfirm: openSyncConfirm, ConfirmModal: ConfirmSyncModal } = useConfirm({
|
||||
content: t('common:core.dataset.collection.Start Sync Tip')
|
||||
});
|
||||
|
||||
const { onOpenModal: onOpenEditTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('common:Rename')
|
||||
@@ -89,7 +85,7 @@ const CollectionCard = () => {
|
||||
const status = (() => {
|
||||
if (collection.trainingAmount > 0) {
|
||||
return {
|
||||
statusText: t('dataset.collections.Collection Embedding', {
|
||||
statusText: t('common:dataset.collections.Collection Embedding', {
|
||||
total: collection.trainingAmount
|
||||
}),
|
||||
colorSchema: 'gray'
|
||||
@@ -134,6 +130,9 @@ const CollectionCard = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { openConfirm: openSyncConfirm, ConfirmModal: ConfirmSyncModal } = useConfirm({
|
||||
content: t('dataset:collection_sync_confirm_tip')
|
||||
});
|
||||
const { runAsync: onclickStartSync, loading: isSyncing } = useRequest2(postLinkCollectionSync, {
|
||||
onSuccess(res: DatasetCollectionSyncResultEnum) {
|
||||
getData(pageNum);
|
||||
@@ -195,11 +194,11 @@ const CollectionCard = () => {
|
||||
<Thead draggable={false}>
|
||||
<Tr>
|
||||
<Th py={4}>{t('common:common.Name')}</Th>
|
||||
<Th py={4}>{datasetT('collection.Training type')}</Th>
|
||||
<Th py={4}>{t('dataset:collection.Training type')}</Th>
|
||||
<Th py={4}>{t('common:dataset.collections.Data Amount')}</Th>
|
||||
<Th py={4}>{datasetT('collection.Create update time')}</Th>
|
||||
<Th py={4}>{t('dataset:collection.Create update time')}</Th>
|
||||
<Th py={4}>{t('common:common.Status')}</Th>
|
||||
<Th py={4}>{datasetT('Enable')}</Th>
|
||||
<Th py={4}>{t('dataset:Enable')}</Th>
|
||||
<Th py={4} />
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -219,14 +218,14 @@ const CollectionCard = () => {
|
||||
if (collection.type === DatasetCollectionTypeEnum.folder) {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
datasetId: datasetDetail._id,
|
||||
parentId: collection._id
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
datasetId: datasetDetail._id,
|
||||
collectionId: collection._id,
|
||||
currentTab: TabEnum.dataCard
|
||||
}
|
||||
@@ -311,7 +310,8 @@ const CollectionCard = () => {
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
...(collection.type === DatasetCollectionTypeEnum.link
|
||||
...(collection.type === DatasetCollectionTypeEnum.link ||
|
||||
datasetDetail.type === DatasetTypeEnum.apiDataset
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
@@ -321,7 +321,7 @@ const CollectionCard = () => {
|
||||
w={'0.9rem'}
|
||||
mr={2}
|
||||
/>
|
||||
{t('common:core.dataset.collection.Sync')}
|
||||
{t('dataset:collection_sync')}
|
||||
</Flex>
|
||||
),
|
||||
onClick: () =>
|
||||
|
||||
@@ -28,6 +28,11 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { TabEnum } from './NavBar';
|
||||
import {
|
||||
DatasetCollectionTypeEnum,
|
||||
ImportDataSourceEnum
|
||||
} from '@fastgpt/global/core/dataset/constants';
|
||||
|
||||
const DataCard = () => {
|
||||
const theme = useTheme();
|
||||
@@ -137,20 +142,37 @@ const DataCard = () => {
|
||||
<TagsPopOver currentCollection={collection} />
|
||||
)}
|
||||
</Box>
|
||||
{datasetDetail.type !== 'websiteDataset' && !!collection?.chunkSize && (
|
||||
<Button
|
||||
ml={2}
|
||||
variant={'whitePrimary'}
|
||||
size={['sm', 'md']}
|
||||
onClick={() => {
|
||||
router.push({
|
||||
query: {
|
||||
datasetId,
|
||||
currentTab: TabEnum.import,
|
||||
source: ImportDataSourceEnum.reTraining,
|
||||
collectionId
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('dataset:retain_collection')}
|
||||
</Button>
|
||||
)}
|
||||
{canWrite && (
|
||||
<Box>
|
||||
<Button
|
||||
ml={2}
|
||||
variant={'whitePrimary'}
|
||||
size={['sm', 'md']}
|
||||
onClick={() => {
|
||||
if (!collection) return;
|
||||
setEditDataId('');
|
||||
}}
|
||||
>
|
||||
{t('common:dataset.Insert Data')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
ml={2}
|
||||
variant={'whitePrimary'}
|
||||
size={['sm', 'md']}
|
||||
isDisabled={!collection}
|
||||
onClick={() => {
|
||||
setEditDataId('');
|
||||
}}
|
||||
>
|
||||
{t('common:dataset.Insert Data')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Box justifyContent={'center'} px={6} pos={'relative'} w={'100%'}>
|
||||
|
||||
@@ -86,6 +86,10 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
|
||||
// step
|
||||
const modeSteps: Record<ImportDataSourceEnum, { title: string }[]> = {
|
||||
[ImportDataSourceEnum.reTraining]: [
|
||||
{ title: t('dataset:core.dataset.import.Adjust parameters') },
|
||||
{ title: t('common:core.dataset.import.Upload data') }
|
||||
],
|
||||
[ImportDataSourceEnum.fileLocal]: [
|
||||
{
|
||||
title: t('common:core.dataset.import.Select file')
|
||||
@@ -140,6 +144,17 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
|
||||
{
|
||||
title: t('common:core.dataset.import.Upload data')
|
||||
}
|
||||
],
|
||||
[ImportDataSourceEnum.apiDataset]: [
|
||||
{
|
||||
title: t('common:core.dataset.import.Select file')
|
||||
},
|
||||
{
|
||||
title: t('common:core.dataset.import.Data Preprocessing')
|
||||
},
|
||||
{
|
||||
title: t('common:core.dataset.import.Upload data')
|
||||
}
|
||||
]
|
||||
};
|
||||
const steps = modeSteps[source];
|
||||
|
||||
@@ -260,7 +260,7 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box flex={'1 0 0'} w={['auto', '0']} h={['auto', '100%']} overflow={'auto'} pl={[0, 3]}>
|
||||
<Box flex={'1 0 0'} w={['auto', '0']} h={['auto', '100%']} pl={[0, 3]}>
|
||||
<Preview showPreviewChunks={showPreviewChunks} />
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Box,
|
||||
@@ -17,29 +17,34 @@ import {
|
||||
import { ImportDataSourceEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useRouter } from 'next/router';
|
||||
import { TabEnum } from '../../../index';
|
||||
import {
|
||||
postCreateDatasetApiDatasetCollection,
|
||||
postCreateDatasetCsvTableCollection,
|
||||
postCreateDatasetExternalFileCollection,
|
||||
postCreateDatasetFileCollection,
|
||||
postCreateDatasetLinkCollection,
|
||||
postCreateDatasetTextCollection
|
||||
postCreateDatasetTextCollection,
|
||||
postReTrainingDatasetFileCollection
|
||||
} from '@/web/core/dataset/api';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { DatasetImportContext, type ImportFormType } from '../Context';
|
||||
|
||||
const Upload = () => {
|
||||
const { t } = useTranslation();
|
||||
const { fileT } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { collectionId = '' } = router.query as {
|
||||
collectionId: string;
|
||||
};
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
const retrainNewCollectionId = useRef('');
|
||||
|
||||
const { importSource, parentId, sources, setSources, processParamsForm, chunkSize } =
|
||||
useContextSelector(DatasetImportContext, (v) => v);
|
||||
|
||||
@@ -71,8 +76,8 @@ const Upload = () => {
|
||||
}
|
||||
}, [waitingFilesCount, totalFilesCount, allFinished, t]);
|
||||
|
||||
const { mutate: startUpload, isLoading } = useRequest({
|
||||
mutationFn: async ({ mode, customSplitChar, qaPrompt, webSelector }: ImportFormType) => {
|
||||
const { runAsync: startUpload, loading: isLoading } = useRequest2(
|
||||
async ({ mode, customSplitChar, qaPrompt, webSelector }: ImportFormType) => {
|
||||
if (sources.length === 0) return;
|
||||
const filterWaitingSources = sources.filter((item) => item.createStatus === 'waiting');
|
||||
|
||||
@@ -100,7 +105,13 @@ const Upload = () => {
|
||||
|
||||
name: item.sourceName
|
||||
};
|
||||
if (importSource === ImportDataSourceEnum.fileLocal && item.dbFileId) {
|
||||
if (importSource === ImportDataSourceEnum.reTraining) {
|
||||
const res = await postReTrainingDatasetFileCollection({
|
||||
...commonParams,
|
||||
collectionId
|
||||
});
|
||||
retrainNewCollectionId.current = res.collectionId;
|
||||
} else if (importSource === ImportDataSourceEnum.fileLocal && item.dbFileId) {
|
||||
await postCreateDatasetFileCollection({
|
||||
...commonParams,
|
||||
fileId: item.dbFileId
|
||||
@@ -131,6 +142,11 @@ const Upload = () => {
|
||||
externalFileId: item.externalFileId,
|
||||
filename: item.sourceName
|
||||
});
|
||||
} else if (importSource === ImportDataSourceEnum.apiDataset && item.apiFileId) {
|
||||
await postCreateDatasetApiDatasetCollection({
|
||||
...commonParams,
|
||||
apiFileId: item.apiFileId
|
||||
});
|
||||
}
|
||||
|
||||
setSources((state) =>
|
||||
@@ -145,40 +161,46 @@ const Upload = () => {
|
||||
);
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
if (!sources.some((file) => file.errorMsg !== undefined)) {
|
||||
toast({
|
||||
title: t('common:core.dataset.import.Import success'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
// close import page
|
||||
router.replace({
|
||||
query: {
|
||||
...router.query,
|
||||
currentTab: TabEnum.collectionCard
|
||||
{
|
||||
onSuccess() {
|
||||
if (!sources.some((file) => file.errorMsg !== undefined)) {
|
||||
toast({
|
||||
title:
|
||||
importSource === ImportDataSourceEnum.reTraining
|
||||
? t('dataset:retrain_task_submitted')
|
||||
: t('common:core.dataset.import.Import success'),
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
setSources((state) =>
|
||||
state.map((source) =>
|
||||
source.createStatus === 'creating'
|
||||
? {
|
||||
...source,
|
||||
createStatus: 'waiting',
|
||||
errorMsg: error.message || fileT('upload_failed')
|
||||
}
|
||||
: source
|
||||
)
|
||||
);
|
||||
},
|
||||
errorToast: fileT('upload_failed')
|
||||
});
|
||||
|
||||
// Close import page
|
||||
router.replace({
|
||||
query: {
|
||||
datasetId: datasetDetail._id,
|
||||
currentTab: retrainNewCollectionId.current ? TabEnum.dataCard : TabEnum.collectionCard,
|
||||
collectionId: retrainNewCollectionId.current
|
||||
}
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
setSources((state) =>
|
||||
state.map((source) =>
|
||||
source.createStatus === 'creating'
|
||||
? {
|
||||
...source,
|
||||
createStatus: 'waiting',
|
||||
errorMsg: error.message || t('file:upload_failed')
|
||||
}
|
||||
: source
|
||||
)
|
||||
);
|
||||
},
|
||||
errorToast: t('file:upload_failed')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box h={'100%'} overflow={'auto'}>
|
||||
<TableContainer>
|
||||
<Table variant={'simple'} fontSize={'sm'} draggable={false}>
|
||||
<Thead draggable={false}>
|
||||
|
||||
@@ -24,7 +24,7 @@ const Preview = ({ showPreviewChunks }: { showPreviewChunks: boolean }) => {
|
||||
<MyIcon name={'core/dataset/fileCollection'} w={'20px'} />
|
||||
<Box fontSize={'md'}>{t('common:core.dataset.import.Sources list')}</Box>
|
||||
</Flex>
|
||||
<Box mt={3} flex={'1 0 0'} width={'100%'} overflowY={'auto'}>
|
||||
<Box mt={3} flex={'1 0 0'} h={['auto', 0]} width={'100%'} overflowY={'auto'}>
|
||||
<Grid w={'100%'} gap={3} gridTemplateColumns={['1fr', '1fr', '1fr', '1fr', '1fr 1fr']}>
|
||||
{sources.map((source) => (
|
||||
<Flex
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { ImportSourceItemType } from '@/web/core/dataset/type';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import MyRightDrawer from '@fastgpt/web/components/common/MyDrawer/MyRightDrawer';
|
||||
import { getPreviewChunks } from '@/web/core/dataset/api';
|
||||
import { ImportDataSourceEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetImportContext } from '../Context';
|
||||
import { importType2ReadType } from '@fastgpt/global/core/dataset/read';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { getPreviewSourceReadType } from '../utils';
|
||||
|
||||
const PreviewChunks = ({
|
||||
previewSource,
|
||||
@@ -20,11 +18,11 @@ const PreviewChunks = ({
|
||||
previewSource: ImportSourceItemType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const { importSource, chunkSize, chunkOverlapRatio, processParamsForm } = useContextSelector(
|
||||
DatasetImportContext,
|
||||
(v) => v
|
||||
);
|
||||
const datasetId = useContextSelector(DatasetPageContext, (v) => v.datasetId);
|
||||
|
||||
const { data = [], loading: isLoading } = useRequest2(
|
||||
async () => {
|
||||
@@ -41,28 +39,24 @@ const PreviewChunks = ({
|
||||
a: ''
|
||||
}));
|
||||
}
|
||||
if (importSource === ImportDataSourceEnum.csvTable) {
|
||||
return getPreviewChunks({
|
||||
type: importType2ReadType(importSource),
|
||||
sourceId:
|
||||
previewSource.dbFileId || previewSource.link || previewSource.externalFileUrl || '',
|
||||
chunkSize,
|
||||
overlapRatio: chunkOverlapRatio,
|
||||
customSplitChar: processParamsForm.getValues('customSplitChar'),
|
||||
selector: processParamsForm.getValues('webSelector'),
|
||||
isQAImport: true
|
||||
});
|
||||
}
|
||||
|
||||
return getPreviewChunks({
|
||||
type: importType2ReadType(importSource),
|
||||
datasetId,
|
||||
type: getPreviewSourceReadType(previewSource),
|
||||
sourceId:
|
||||
previewSource.dbFileId || previewSource.link || previewSource.externalFileUrl || '',
|
||||
previewSource.dbFileId ||
|
||||
previewSource.link ||
|
||||
previewSource.externalFileUrl ||
|
||||
previewSource.apiFileId ||
|
||||
'',
|
||||
|
||||
chunkSize,
|
||||
overlapRatio: chunkOverlapRatio,
|
||||
customSplitChar: processParamsForm.getValues('customSplitChar'),
|
||||
|
||||
selector: processParamsForm.getValues('webSelector'),
|
||||
isQAImport: false
|
||||
isQAImport: importSource === ImportDataSourceEnum.csvTable,
|
||||
externalFileId: previewSource.externalFileId
|
||||
});
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { ImportSourceItemType } from '@/web/core/dataset/type';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPreviewFileContent } from '@/web/common/file/api';
|
||||
import MyRightDrawer from '@fastgpt/web/components/common/MyDrawer/MyRightDrawer';
|
||||
import { ImportDataSourceEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
@@ -9,7 +8,9 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetImportContext } from '../Context';
|
||||
import { importType2ReadType } from '@fastgpt/global/core/dataset/read';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getPreviewSourceReadType } from '../utils';
|
||||
|
||||
const PreviewRawText = ({
|
||||
previewSource,
|
||||
@@ -20,32 +21,34 @@ const PreviewRawText = ({
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const { importSource, processParamsForm } = useContextSelector(DatasetImportContext, (v) => v);
|
||||
const datasetId = useContextSelector(DatasetPageContext, (v) => v.datasetId);
|
||||
|
||||
const { data, isLoading } = useQuery(
|
||||
['previewSource', previewSource.dbFileId, previewSource.link, previewSource.externalFileUrl],
|
||||
() => {
|
||||
const { data, loading: isLoading } = useRequest2(
|
||||
async () => {
|
||||
if (importSource === ImportDataSourceEnum.fileCustom && previewSource.rawText) {
|
||||
return {
|
||||
previewContent: previewSource.rawText.slice(0, 3000)
|
||||
};
|
||||
}
|
||||
if (importSource === ImportDataSourceEnum.csvTable && previewSource.dbFileId) {
|
||||
return getPreviewFileContent({
|
||||
type: importType2ReadType(importSource),
|
||||
sourceId: previewSource.dbFileId,
|
||||
isQAImport: true
|
||||
});
|
||||
}
|
||||
|
||||
return getPreviewFileContent({
|
||||
type: importType2ReadType(importSource),
|
||||
datasetId,
|
||||
type: getPreviewSourceReadType(previewSource),
|
||||
sourceId:
|
||||
previewSource.dbFileId || previewSource.link || previewSource.externalFileUrl || '',
|
||||
isQAImport: false,
|
||||
selector: processParamsForm.getValues('webSelector')
|
||||
previewSource.dbFileId ||
|
||||
previewSource.link ||
|
||||
previewSource.externalFileUrl ||
|
||||
previewSource.apiFileId ||
|
||||
'',
|
||||
|
||||
isQAImport: importSource === ImportDataSourceEnum.csvTable,
|
||||
selector: processParamsForm.getValues('webSelector'),
|
||||
externalFileId: previewSource.externalFileId
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [previewSource.dbFileId, previewSource.link, previewSource.externalFileUrl],
|
||||
manual: false,
|
||||
onError(err) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetImportContext } from '../Context';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Loading from '@fastgpt/web/components/common/MyLoading';
|
||||
import { Box, Button, Checkbox, Flex } from '@chakra-ui/react';
|
||||
import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getApiDatasetFileList, getApiDatasetFileListExistId } from '@/web/core/dataset/api';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { APIFileItem } from '@fastgpt/global/core/dataset/apiDataset';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { useMount } from 'ahooks';
|
||||
|
||||
const DataProcess = dynamic(() => import('../commonProgress/DataProcess'), {
|
||||
loading: () => <Loading fixed={false} />
|
||||
});
|
||||
const Upload = dynamic(() => import('../commonProgress/Upload'));
|
||||
|
||||
const APIDatasetCollection = () => {
|
||||
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeStep === 0 && <CustomAPIFileInput />}
|
||||
{activeStep === 1 && <DataProcess showPreviewChunks={true} />}
|
||||
{activeStep === 2 && <Upload />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(APIDatasetCollection);
|
||||
|
||||
const CustomAPIFileInput = () => {
|
||||
const { t } = useTranslation();
|
||||
const datasetDetail = useContextSelector(DatasetPageContext, (v) => v.datasetDetail);
|
||||
const goToNext = useContextSelector(DatasetImportContext, (v) => v.goToNext);
|
||||
|
||||
const sources = useContextSelector(DatasetImportContext, (v) => v.sources);
|
||||
const setSources = useContextSelector(DatasetImportContext, (v) => v.setSources);
|
||||
|
||||
const [selectFiles, setSelectFiles] = useState<APIFileItem[]>([]);
|
||||
const [parent, setParent] = useState<ParentTreePathItemType>({
|
||||
parentId: '',
|
||||
parentName: ''
|
||||
});
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const { data: fileList = [], loading } = useRequest2(
|
||||
async () =>
|
||||
datasetDetail?.apiServer
|
||||
? getApiDatasetFileList({
|
||||
datasetId: datasetDetail._id,
|
||||
parentId: parent?.parentId,
|
||||
searchKey: searchKey
|
||||
})
|
||||
: [],
|
||||
{
|
||||
refreshDeps: [datasetDetail._id, datasetDetail.apiServer, parent, searchKey],
|
||||
throttleWait: 500,
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
const { data: existIdList = [] } = useRequest2(
|
||||
() => getApiDatasetFileListExistId({ datasetId: datasetDetail._id }),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
// Init selected files
|
||||
useMount(() => {
|
||||
setSelectFiles(sources.map((item) => item.apiFile).filter(Boolean) as APIFileItem[]);
|
||||
});
|
||||
|
||||
const { runAsync: onclickNext, loading: onNextLoading } = useRequest2(
|
||||
async () => {
|
||||
// Computed all selected files
|
||||
const getFilesRecursively = async (files: APIFileItem[]): Promise<APIFileItem[]> => {
|
||||
const allFiles: APIFileItem[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.type === 'folder') {
|
||||
const folderFiles = await getApiDatasetFileList({
|
||||
datasetId: datasetDetail._id,
|
||||
parentId: file?.id
|
||||
});
|
||||
const subFiles = await getFilesRecursively(folderFiles);
|
||||
allFiles.push(...subFiles);
|
||||
} else {
|
||||
allFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
};
|
||||
|
||||
const allFiles = await getFilesRecursively(selectFiles);
|
||||
|
||||
setSources(
|
||||
allFiles
|
||||
.filter((item) => !existIdList.includes(item.id))
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
apiFileId: item.id,
|
||||
apiFile: item,
|
||||
createStatus: 'waiting',
|
||||
sourceName: item.name,
|
||||
icon: getSourceNameIcon({ sourceName: item.name }) as any
|
||||
}))
|
||||
);
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
goToNext();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
(item: APIFileItem) => {
|
||||
if (item.type === 'folder') {
|
||||
return setParent({
|
||||
parentId: item.id,
|
||||
parentName: item.name
|
||||
});
|
||||
}
|
||||
|
||||
const isCurrentlySelected = selectFiles.some((file) => file.id === item.id);
|
||||
if (isCurrentlySelected) {
|
||||
setSelectFiles((state) => state.filter((file) => file.id !== item.id));
|
||||
} else {
|
||||
setSelectFiles((state) => [...state, item]);
|
||||
}
|
||||
},
|
||||
[selectFiles, setSelectFiles]
|
||||
);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
const isAllSelected = fileList.length === selectFiles.length;
|
||||
|
||||
if (isAllSelected) {
|
||||
setSelectFiles([]);
|
||||
} else {
|
||||
setSelectFiles(fileList);
|
||||
}
|
||||
}, [fileList, selectFiles]);
|
||||
|
||||
const paths = useMemo(() => [parent || { parentId: '', parentName: '' }], [parent]);
|
||||
|
||||
return (
|
||||
<MyBox isLoading={loading} position="relative" h="full">
|
||||
<Flex flexDirection={'column'} h="full">
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<FolderPath
|
||||
paths={paths}
|
||||
onClick={(parentId) => {
|
||||
if (parentId !== parent?.parentId) {
|
||||
setParent({
|
||||
parentId,
|
||||
parentName: ''
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box w={'240px'}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={t('common:core.workflow.template.Search')}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box flex={1} overflowY="auto" mb={16}>
|
||||
<Box ml={2} mt={3}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
py={3}
|
||||
cursor={'pointer'}
|
||||
bg={'myGray.50'}
|
||||
pl={7}
|
||||
rounded={'8px'}
|
||||
fontSize={'sm'}
|
||||
fontWeight={'medium'}
|
||||
color={'myGray.900'}
|
||||
onClick={(e) => {
|
||||
if (!(e.target as HTMLElement).closest('.checkbox')) {
|
||||
handleSelectAll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
className="checkbox"
|
||||
mr={2}
|
||||
isChecked={fileList.length === selectFiles.length}
|
||||
onChange={handleSelectAll}
|
||||
/>
|
||||
{t('common:Select_all')}
|
||||
</Flex>
|
||||
{fileList.map((item) => {
|
||||
const isFolder = item.type === 'folder';
|
||||
const isExists = existIdList.includes(item.id);
|
||||
const isChecked = isExists || selectFiles.some((file) => file.id === item.id);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={item.id}
|
||||
py={3}
|
||||
_hover={{ bg: 'primary.50' }}
|
||||
pl={7}
|
||||
cursor={'pointer'}
|
||||
onClick={(e) => {
|
||||
if (isExists) return;
|
||||
if (!(e.target as HTMLElement).closest('.checkbox')) {
|
||||
handleItemClick(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
className="checkbox"
|
||||
mr={2.5}
|
||||
isChecked={isChecked}
|
||||
isDisabled={isExists}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isExists) return;
|
||||
if (isChecked) {
|
||||
setSelectFiles((state) => state.filter((file) => file.id !== item.id));
|
||||
} else {
|
||||
setSelectFiles((state) => [...state, item]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<MyIcon
|
||||
name={
|
||||
!isFolder
|
||||
? (getSourceNameIcon({ sourceName: item.name }) as any)
|
||||
: 'common/folderFill'
|
||||
}
|
||||
w={'18px'}
|
||||
mr={1.5}
|
||||
/>
|
||||
<Box fontSize={'sm'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
display={'flex'}
|
||||
justifyContent={'end'}
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
p={4}
|
||||
>
|
||||
<Button
|
||||
isDisabled={selectFiles.length === 0}
|
||||
isLoading={onNextLoading}
|
||||
onClick={onclickNext}
|
||||
>
|
||||
{selectFiles.length > 0
|
||||
? `${t('common:core.dataset.import.Total files', { total: selectFiles.length })} | `
|
||||
: ''}
|
||||
{t('common:common.Next Step')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,6 @@ import Loading from '@fastgpt/web/components/common/MyLoading';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetImportContext } from '../Context';
|
||||
import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
|
||||
const DataProcess = dynamic(() => import('../commonProgress/DataProcess'), {
|
||||
@@ -45,7 +44,6 @@ export default React.memo(ExternalFileCollection);
|
||||
|
||||
const CustomLinkInput = () => {
|
||||
const { t } = useTranslation();
|
||||
const { datasetT, commonT } = useI18n();
|
||||
const { goToNext, sources, setSources } = useContextSelector(DatasetImportContext, (v) => v);
|
||||
const { register, reset, handleSubmit, control } = useForm<{
|
||||
list: {
|
||||
@@ -93,9 +91,9 @@ const CustomLinkInput = () => {
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr bg={'myGray.50'}>
|
||||
<Th>{datasetT('external_url')}</Th>
|
||||
<Th>{datasetT('external_id')}</Th>
|
||||
<Th>{datasetT('filename')}</Th>
|
||||
<Th>{t('dataset:external_url')}</Th>
|
||||
<Th>{t('dataset:external_id')}</Th>
|
||||
<Th>{t('dataset:filename')}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -159,7 +157,7 @@ const CustomLinkInput = () => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{commonT('add_new')}
|
||||
{t('common:add_new')}
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={list.filter((item) => !!item.externalFileUrl).length === 0}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { DatasetImportContext } from '../Context';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import DataProcess from '../commonProgress/DataProcess';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getDatasetCollectionById } from '@/web/core/dataset/api';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { ImportProcessWayEnum } from '@/web/core/dataset/constants';
|
||||
import { getCollectionIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
|
||||
const Upload = dynamic(() => import('../commonProgress/Upload'));
|
||||
|
||||
const ReTraining = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { collectionId = '' } = router.query as {
|
||||
collectionId: string;
|
||||
};
|
||||
|
||||
const activeStep = useContextSelector(DatasetImportContext, (v) => v.activeStep);
|
||||
const setSources = useContextSelector(DatasetImportContext, (v) => v.setSources);
|
||||
const processParamsForm = useContextSelector(DatasetImportContext, (v) => v.processParamsForm);
|
||||
|
||||
const { loading } = useRequest2(() => getDatasetCollectionById(collectionId), {
|
||||
refreshDeps: [collectionId],
|
||||
manual: false,
|
||||
onSuccess: (collection) => {
|
||||
setSources([
|
||||
{
|
||||
dbFileId: collection.fileId,
|
||||
link: collection.rawLink,
|
||||
apiFileId: collection.apiFileId,
|
||||
|
||||
createStatus: 'waiting',
|
||||
icon: getCollectionIcon(collection.type, collection.name),
|
||||
id: collection._id,
|
||||
isUploading: false,
|
||||
sourceName: collection.name,
|
||||
uploadedFileRate: 100
|
||||
}
|
||||
]);
|
||||
processParamsForm.reset({
|
||||
mode: collection.trainingType,
|
||||
way: ImportProcessWayEnum.auto,
|
||||
embeddingChunkSize: collection.chunkSize,
|
||||
qaChunkSize: collection.chunkSize,
|
||||
customSplitChar: collection.chunkSplitter,
|
||||
qaPrompt: collection.qaPrompt,
|
||||
webSelector: collection.metadata?.webSelector
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyBox isLoading={loading} h={'100%'} overflow={'auto'}>
|
||||
{activeStep === 0 && <DataProcess showPreviewChunks={true} />}
|
||||
{activeStep === 1 && <Upload />}
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReTraining);
|
||||
@@ -10,20 +10,24 @@ const FileLink = dynamic(() => import('./diffSource/FileLink'));
|
||||
const FileCustomText = dynamic(() => import('./diffSource/FileCustomText'));
|
||||
const TableLocal = dynamic(() => import('./diffSource/TableLocal'));
|
||||
const ExternalFileCollection = dynamic(() => import('./diffSource/ExternalFile'));
|
||||
const APIDatasetCollection = dynamic(() => import('./diffSource/APIDataset'));
|
||||
const ReTraining = dynamic(() => import('./diffSource/ReTraining'));
|
||||
|
||||
const ImportDataset = () => {
|
||||
const importSource = useContextSelector(DatasetImportContext, (v) => v.importSource);
|
||||
|
||||
const ImportComponent = useMemo(() => {
|
||||
if (importSource === ImportDataSourceEnum.reTraining) return ReTraining;
|
||||
if (importSource === ImportDataSourceEnum.fileLocal) return FileLocal;
|
||||
if (importSource === ImportDataSourceEnum.fileLink) return FileLink;
|
||||
if (importSource === ImportDataSourceEnum.fileCustom) return FileCustomText;
|
||||
if (importSource === ImportDataSourceEnum.csvTable) return TableLocal;
|
||||
if (importSource === ImportDataSourceEnum.externalFile) return ExternalFileCollection;
|
||||
if (importSource === ImportDataSourceEnum.apiDataset) return APIDatasetCollection;
|
||||
}, [importSource]);
|
||||
|
||||
return ImportComponent ? (
|
||||
<Box flex={'1 0 0'} overflow={'auto'} position={'relative'}>
|
||||
<Box flex={'1 0 0'} overflow={'auto'}>
|
||||
<ImportComponent />
|
||||
</Box>
|
||||
) : null;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ImportSourceItemType } from '@/web/core/dataset/type';
|
||||
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
|
||||
|
||||
export const getPreviewSourceReadType = (previewSource: ImportSourceItemType) => {
|
||||
if (previewSource.dbFileId) {
|
||||
return DatasetSourceReadTypeEnum.fileLocal;
|
||||
}
|
||||
if (previewSource.link) {
|
||||
return DatasetSourceReadTypeEnum.link;
|
||||
}
|
||||
if (previewSource.apiFileId) {
|
||||
return DatasetSourceReadTypeEnum.apiFile;
|
||||
}
|
||||
if (previewSource.externalFileId) {
|
||||
return DatasetSourceReadTypeEnum.externalFile;
|
||||
}
|
||||
|
||||
return DatasetSourceReadTypeEnum.fileLocal;
|
||||
};
|
||||
|
||||
export default function Dom() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { ModalFooter, ModalBody, Input, Button, Flex } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal/index';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { APIFileServer } from '@fastgpt/global/core/dataset/apiDataset';
|
||||
|
||||
export type EditAPIDatasetInfoFormType = {
|
||||
id: string;
|
||||
apiServer?: APIFileServer;
|
||||
};
|
||||
|
||||
const EditAPIDatasetInfoModal = ({
|
||||
onClose,
|
||||
onEdit,
|
||||
title,
|
||||
...defaultForm
|
||||
}: EditAPIDatasetInfoFormType & {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
onEdit: (data: EditAPIDatasetInfoFormType) => any;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { register, handleSubmit } = useForm<EditAPIDatasetInfoFormType>({
|
||||
defaultValues: defaultForm
|
||||
});
|
||||
|
||||
const { runAsync: onSave, loading } = useRequest2(
|
||||
(data: EditAPIDatasetInfoFormType) => onEdit(data),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
toast({
|
||||
title: t('common:common.Update Success'),
|
||||
status: 'success'
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} w={'450px'} iconSrc="modal/edit" title={title}>
|
||||
<ModalBody>
|
||||
<Flex>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
flex={['', '0 0 110px']}
|
||||
color={'myGray.900'}
|
||||
fontWeight={500}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{t('dataset:api_url')}
|
||||
</Flex>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={t('dataset:api_url')}
|
||||
maxLength={200}
|
||||
{...register('apiServer.baseUrl', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={6}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
flex={['', '0 0 110px']}
|
||||
color={'myGray.900'}
|
||||
fontWeight={500}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
Authorization
|
||||
</Flex>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={t('dataset:request_headers')}
|
||||
maxLength={200}
|
||||
{...register('apiServer.authorization')}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button isLoading={loading} onClick={handleSubmit(onSave)} px={6}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditAPIDatasetInfoModal;
|
||||
@@ -21,7 +21,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
|
||||
import MemberManager from '../../component/MemberManager';
|
||||
import MemberManager from '../../../component/MemberManager';
|
||||
import {
|
||||
getCollaboratorList,
|
||||
postUpdateDatasetCollaborators,
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
} from '@/web/core/dataset/api/collaborator';
|
||||
import DatasetTypeTag from '@/components/core/dataset/DatasetTypeTag';
|
||||
import dynamic from 'next/dynamic';
|
||||
import EditAPIDatasetInfoModal, {
|
||||
EditAPIDatasetInfoFormType
|
||||
} from './components/EditApiServiceModal';
|
||||
import { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
|
||||
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
|
||||
|
||||
@@ -37,6 +40,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
|
||||
const { datasetDetail, loadDatasetDetail, updateDataset, rebuildingCount, trainingCount } =
|
||||
useContextSelector(DatasetPageContext, (v) => v);
|
||||
const [editedDataset, setEditedDataset] = useState<EditResourceInfoFormType>();
|
||||
const [editedAPIDataset, setEditedAPIDataset] = useState<EditAPIDatasetInfoFormType>();
|
||||
const refetchDatasetTraining = useContextSelector(
|
||||
DatasetPageContext,
|
||||
(v) => v.refetchDatasetTraining
|
||||
@@ -126,7 +130,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
|
||||
|
||||
useEffect(() => {
|
||||
reset(datasetDetail);
|
||||
}, [datasetDetail._id]);
|
||||
}, [datasetDetail, datasetDetail._id, reset]);
|
||||
|
||||
return (
|
||||
<Box w={'100%'} h={'100%'} p={6}>
|
||||
@@ -174,12 +178,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
|
||||
<MyDivider my={4} h={'2px'} maxW={'500px'} />
|
||||
|
||||
<Box overflow={'hidden'}>
|
||||
<Flex justify={'space-between'} alignItems={'center'} fontSize={'mini'} h={'24px'}>
|
||||
<Box fontWeight={'500'} color={'myGray.900'} userSelect={'none'}>
|
||||
{t('common:common.base_config')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={3} w={'100%'} flexDir={'column'}>
|
||||
<Flex w={'100%'} flexDir={'column'}>
|
||||
<FormLabel fontSize={'mini'} fontWeight={'500'}>
|
||||
{t('common:core.dataset.Dataset ID')}
|
||||
</FormLabel>
|
||||
@@ -267,6 +266,31 @@ const Info = ({ datasetId }: { datasetId: string }) => {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{datasetDetail.type === DatasetTypeEnum.apiDataset && (
|
||||
<>
|
||||
<Box w={'100%'} alignItems={'center'} pt={4}>
|
||||
<Flex justifyContent={'space-between'} mb={1}>
|
||||
<FormLabel fontSize={'mini'} fontWeight={'500'}>
|
||||
{t('dataset:api_url')}
|
||||
</FormLabel>
|
||||
<MyIcon
|
||||
name={'edit'}
|
||||
w={'14px'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
setEditedAPIDataset({
|
||||
id: datasetDetail._id,
|
||||
apiServer: datasetDetail.apiServer
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Box fontSize={'mini'}>{datasetDetail.apiServer?.baseUrl}</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{datasetDetail.permission.hasManagePer && (
|
||||
@@ -321,6 +345,19 @@ const Info = ({ datasetId }: { datasetId: string }) => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{editedAPIDataset && (
|
||||
<EditAPIDatasetInfoModal
|
||||
{...editedAPIDataset}
|
||||
title={t('common:dataset.Edit API Service')}
|
||||
onClose={() => setEditedAPIDataset(undefined)}
|
||||
onEdit={(data) =>
|
||||
updateDataset({
|
||||
id: datasetId,
|
||||
apiServer: data.apiServer
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -67,7 +67,7 @@ const MetaDataCard = ({ datasetId }: { datasetId: string }) => {
|
||||
value: collection.rawTextLength ?? '-'
|
||||
},
|
||||
{
|
||||
label: t('common:core.dataset.collection.metadata.Training Type'),
|
||||
label: t('dataset:collection.Training type'),
|
||||
value: t(TrainingTypeMap[collection.trainingType]?.label as any)
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
const CollectionCard = dynamic(() => import('./components/CollectionCard/index'));
|
||||
const DataCard = dynamic(() => import('./components/DataCard'));
|
||||
const Test = dynamic(() => import('./components/Test'));
|
||||
const Info = dynamic(() => import('./components/Info'));
|
||||
const Info = dynamic(() => import('./components/Info/index'));
|
||||
const Import = dynamic(() => import('./components/Import'));
|
||||
|
||||
export enum TabEnum {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
@@ -20,10 +20,12 @@ import AIModelSelector from '@/components/Select/AIModelSelector';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import ComplianceTip from '@/components/common/ComplianceTip/index';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
|
||||
export type CreateDatasetType =
|
||||
| DatasetTypeEnum.dataset
|
||||
| DatasetTypeEnum.externalFile
|
||||
| DatasetTypeEnum.apiDataset
|
||||
| DatasetTypeEnum.websiteDataset;
|
||||
|
||||
const CreateModal = ({
|
||||
@@ -44,7 +46,7 @@ const CreateModal = ({
|
||||
const databaseNameMap = useMemo(() => {
|
||||
return {
|
||||
[DatasetTypeEnum.dataset]: t('dataset:common_dataset'),
|
||||
[DatasetTypeEnum.externalFile]: t('dataset:external_file'),
|
||||
[DatasetTypeEnum.apiDataset]: t('dataset:api_file'),
|
||||
[DatasetTypeEnum.websiteDataset]: t('dataset:website_dataset')
|
||||
};
|
||||
}, [t]);
|
||||
@@ -52,7 +54,7 @@ const CreateModal = ({
|
||||
const iconMap = useMemo(() => {
|
||||
return {
|
||||
[DatasetTypeEnum.dataset]: 'core/dataset/commonDatasetColor',
|
||||
[DatasetTypeEnum.externalFile]: 'core/dataset/externalDatasetColor',
|
||||
[DatasetTypeEnum.apiDataset]: 'core/dataset/externalDatasetColor',
|
||||
[DatasetTypeEnum.websiteDataset]: 'core/dataset/websiteDatasetColor'
|
||||
};
|
||||
}, []);
|
||||
@@ -90,7 +92,7 @@ const CreateModal = ({
|
||||
maxW: 300,
|
||||
maxH: 300
|
||||
});
|
||||
setValue('avatar', src);
|
||||
setValue('avatar' as const, src);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, t('common:common.avatar.Select Failed')),
|
||||
@@ -102,17 +104,16 @@ const CreateModal = ({
|
||||
);
|
||||
|
||||
/* create a new kb and router to it */
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (data: CreateDatasetParams) => {
|
||||
const id = await postCreateDataset(data);
|
||||
return id;
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
onSuccess(id) {
|
||||
router.push(`/dataset/detail?datasetId=${id}`);
|
||||
const { run: onclickCreate, loading: creating } = useRequest2(
|
||||
async (data: CreateDatasetParams) => await postCreateDataset(data),
|
||||
{
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
onSuccess(id) {
|
||||
router.push(`/dataset/detail?datasetId=${id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
@@ -129,9 +130,26 @@ const CreateModal = ({
|
||||
>
|
||||
<ModalBody py={6} px={9}>
|
||||
<Box>
|
||||
<Box color={'myGray.900'} fontWeight={500} fontSize={'sm'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
<Flex justify={'space-between'}>
|
||||
<Box color={'myGray.900'} fontWeight={500} fontSize={'sm'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
{type === DatasetTypeEnum.apiDataset && (
|
||||
<Flex
|
||||
as={'span'}
|
||||
alignItems={'center'}
|
||||
color={'primary.600'}
|
||||
fontSize={'sm'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
window.open(getDocPath('/docs/guide/knowledge_base/api_dataset/'), '_blank')
|
||||
}
|
||||
>
|
||||
<MyIcon name={'book'} w={4} mr={0.5} />
|
||||
{t('common:Instructions')}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex mt={'12px'} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.avatar.Select Avatar')}>
|
||||
<Avatar
|
||||
@@ -185,7 +203,7 @@ const CreateModal = ({
|
||||
value: item.model
|
||||
}))}
|
||||
onchange={(e) => {
|
||||
setValue('vectorModel', e);
|
||||
setValue('vectorModel' as const, e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -218,12 +236,50 @@ const CreateModal = ({
|
||||
value: item.model
|
||||
}))}
|
||||
onchange={(e) => {
|
||||
setValue('agentModel', e);
|
||||
setValue('agentModel' as const, e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{type === DatasetTypeEnum.apiDataset && (
|
||||
<>
|
||||
<Flex mt={6}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
flex={['', '0 0 110px']}
|
||||
color={'myGray.900'}
|
||||
fontWeight={500}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{t('dataset:api_url')}
|
||||
</Flex>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={t('dataset:api_url')}
|
||||
maxLength={200}
|
||||
{...register('apiServer.baseUrl', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mt={6}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
flex={['', '0 0 110px']}
|
||||
color={'myGray.900'}
|
||||
fontWeight={500}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
Authorization
|
||||
</Flex>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={t('dataset:request_headers')}
|
||||
maxLength={200}
|
||||
{...register('apiServer.authorization')}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter px={9}>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { DatasetItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { checkTeamExportDatasetLimit } from '@/web/support/user/team/api';
|
||||
import { downloadFetch } from '@/web/common/system/utils';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
@@ -40,7 +38,6 @@ function List() {
|
||||
const { setLoading } = useSystemStore();
|
||||
const { isPc } = useSystem();
|
||||
const { t } = useTranslation();
|
||||
const { commonT } = useI18n();
|
||||
const { loadAndGetTeamMembers } = useUserStore();
|
||||
const {
|
||||
loadMyDatasets,
|
||||
@@ -326,7 +323,7 @@ function List() {
|
||||
children: [
|
||||
{
|
||||
icon: 'edit',
|
||||
label: commonT('dataset.Edit Info'),
|
||||
label: t('common:dataset.Edit Info'),
|
||||
onClick: () =>
|
||||
setEditedDataset({
|
||||
id: dataset._id,
|
||||
@@ -410,7 +407,7 @@ function List() {
|
||||
{editedDataset && (
|
||||
<EditResourceModal
|
||||
{...editedDataset}
|
||||
title={commonT('dataset.Edit Info')}
|
||||
title={t('common:dataset.Edit Info')}
|
||||
onClose={() => setEditedDataset(undefined)}
|
||||
onEdit={async (data) => {
|
||||
await onUpdateDataset({
|
||||
|
||||
@@ -20,6 +20,10 @@ const SideTag = ({ type, ...props }: { type: `${DatasetTypeEnum}` } & FlexProps)
|
||||
[DatasetTypeEnum.externalFile]: {
|
||||
icon: 'core/dataset/externalDatasetOutline',
|
||||
label: t('dataset:external_file')
|
||||
},
|
||||
[DatasetTypeEnum.apiDataset]: {
|
||||
icon: 'core/dataset/externalDatasetOutline',
|
||||
label: t('dataset:api_file')
|
||||
}
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
@@ -64,10 +64,7 @@ const Dataset = () => {
|
||||
|
||||
const onSelectDatasetType = useCallback(
|
||||
(e: CreateDatasetType) => {
|
||||
if (
|
||||
!feConfigs?.isPlus &&
|
||||
(e === DatasetTypeEnum.websiteDataset || e === DatasetTypeEnum.externalFile)
|
||||
) {
|
||||
if (!feConfigs?.isPlus && e === DatasetTypeEnum.websiteDataset) {
|
||||
return toast({
|
||||
status: 'warning',
|
||||
title: t('common:common.system.Commercial version function')
|
||||
@@ -107,7 +104,7 @@ const Dataset = () => {
|
||||
overflowY={'auto'}
|
||||
overflowX={'hidden'}
|
||||
>
|
||||
<Flex pt={[4, 6]} pl={3} pr={[3, 10]}>
|
||||
<Flex pt={[4, 6]} pl={3} pr={folderDetail ? [3, 6] : [3, 8]}>
|
||||
<Flex flexGrow={1} flexDirection="column">
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<ParentPaths
|
||||
@@ -160,17 +157,17 @@ const Dataset = () => {
|
||||
description: t('dataset:common_dataset_desc'),
|
||||
onClick: () => onSelectDatasetType(DatasetTypeEnum.dataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/externalDatasetColor',
|
||||
label: t('dataset:api_file'),
|
||||
description: t('dataset:external_file_dataset_desc'),
|
||||
onClick: () => onSelectDatasetType(DatasetTypeEnum.apiDataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/websiteDatasetColor',
|
||||
label: t('dataset:website_dataset'),
|
||||
description: t('dataset:website_dataset_desc'),
|
||||
onClick: () => onSelectDatasetType(DatasetTypeEnum.websiteDataset)
|
||||
},
|
||||
{
|
||||
icon: 'core/dataset/externalDatasetColor',
|
||||
label: t('dataset:external_file'),
|
||||
description: t('dataset:external_file_dataset_desc'),
|
||||
onClick: () => onSelectDatasetType(DatasetTypeEnum.externalFile)
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import I18nLngSelector from '@/components/Select/I18nLngSelector';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
|
||||
|
||||
interface Props {
|
||||
@@ -22,6 +23,8 @@ interface Props {
|
||||
const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { setLoginStore, feConfigs } = useSystemStore();
|
||||
const { lastRoute = '/app/list' } = router.query as { lastRoute: string };
|
||||
const state = useRef(nanoid());
|
||||
@@ -83,7 +86,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
];
|
||||
|
||||
const show_oauth =
|
||||
!sessionStorage.getItem('bd_vid') && !!(feConfigs?.sso || oAuthList.length > 0);
|
||||
!sessionStorage.getItem('bd_vid') && !!(feConfigs?.sso?.url || oAuthList.length > 0);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
@@ -143,7 +146,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{feConfigs?.sso && (
|
||||
{feConfigs?.sso?.url && (
|
||||
<Box mt={4} color={'primary.700'} cursor={'pointer'} textAlign={'center'}>
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
@@ -152,7 +155,22 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
borderRadius={'sm'}
|
||||
leftIcon={<MyImage alt="" src={feConfigs.sso.icon as any} w="20px" />}
|
||||
onClick={() => {
|
||||
feConfigs.sso?.url && router.replace(feConfigs.sso?.url, '_self');
|
||||
const url = feConfigs.sso?.url;
|
||||
if (!url) {
|
||||
toast({
|
||||
title: 'SSO URL is not set',
|
||||
status: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoginStore({
|
||||
provider: OAuthEnum.sso,
|
||||
lastRoute,
|
||||
state: state.current
|
||||
});
|
||||
|
||||
const formatUrl = `${url}/login/oauth/authorize?redirect_uri=${encodeURIComponent(redirectUri)}&state=${state.current}`;
|
||||
router.replace(formatUrl, '_self');
|
||||
}}
|
||||
>
|
||||
{feConfigs.sso.title}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user