V4.8.20 feature (#3686)

* Aiproxy (#3649)

* model config

* feat: model config ui

* perf: rename variable

* feat: custom request url

* perf: model buffer

* perf: init model

* feat: json model config

* auto login

* fix: ts

* update packages

* package

* fix: dockerfile

* feat: usage filter & export & dashbord (#3538)

* feat: usage filter & export & dashbord

* adjust ui

* fix tmb scroll

* fix code & selecte all

* merge

* perf: usages list;perf: move components (#3654)

* perf: usages list

* team sub plan load

* perf: usage dashboard code

* perf: dashboard ui

* perf: move components

* add default model config (#3653)

* 4.8.20 test (#3656)

* provider

* perf: model config

* model perf (#3657)

* fix: model

* dataset quote

* perf: model config

* model tag

* doubao model config

* perf: config model

* feat: model test

* fix: POST 500 error on dingtalk bot (#3655)

* feat: default model (#3662)

* move model config

* feat: default model

* fix: false triggerd org selection (#3661)

* export usage csv i18n (#3660)

* export usage csv i18n

* fix build

* feat: markdown extension (#3663)

* feat: markdown extension

* media cros

* rerank test

* default price

* perf: default model

* fix: cannot custom provider

* fix: default model select

* update bg

* perf: default model selector

* fix: usage export

* i18n

* fix: rerank

* update init extension

* perf: ip limit check

* doubao model order

* web default modle

* perf: tts selector

* perf: tts error

* qrcode package

* reload buffer (#3665)

* reload buffer

* reload buffer

* tts selector

* fix: err tip (#3666)

* fix: err tip

* perf: training queue

* doc

* fix interactive edge (#3659)

* fix interactive edge

* fix

* comment

* add gemini model

* fix: chat model select

* perf: supplement assistant empty response (#3669)

* perf: supplement assistant empty response

* check array

* perf: max_token count;feat: support resoner output;fix: member scroll (#3681)

* perf: supplement assistant empty response

* check array

* perf: max_token count

* feat: support resoner output

* member scroll

* update provider order

* i18n

* fix: stream response (#3682)

* perf: supplement assistant empty response

* check array

* fix: stream response

* fix: model config cannot set to null

* fix: reasoning response (#3684)

* perf: supplement assistant empty response

* check array

* fix: reasoning response

* fix: reasoning response

* doc (#3685)

* perf: supplement assistant empty response

* check array

* doc

* lock

* animation

* update doc

* update compose

* doc

* doc

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
This commit is contained in:
Archer
2025-02-05 00:10:47 +08:00
committed by GitHub
parent c393002f1d
commit db2c0a0bdb
496 changed files with 9031 additions and 4726 deletions

View File

@@ -2,7 +2,7 @@ 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 AccountContainer, { TabEnum } from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const ApiKey = () => {

View File

@@ -1,283 +0,0 @@
import {
getInvoiceBillsList,
invoiceBillDataType,
submitInvoice
} from '@/web/support/wallet/bill/invoice/api';
import {
Box,
Button,
Checkbox,
Flex,
ModalBody,
ModalFooter,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} from '@chakra-ui/react';
import { billTypeMap } from '@fastgpt/global/support/wallet/bill/constants';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next';
import { useCallback, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Divider from '@/pages/app/detail/components/WorkflowComponents/Flow/components/Divider';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import { InvoiceHeaderSingleForm } from './InvoiceHeaderForm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getTeamInvoiceHeader } from '@/web/support/user/team/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
type chosenBillDataType = {
_id: string;
price: number;
};
const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const router = useRouter();
const [chosenBillDataList, setChosenBillDataList] = useState<chosenBillDataType[]>([]);
const [totalPrice, setTotalPrice] = useState(0);
const {
isOpen: isOpenSettleModal,
onOpen: onOpenSettleModal,
onClose: onCloseSettleModal
} = useDisclosure();
const {
loading: isLoading,
data: billsList,
run: getInvoiceBills
} = useRequest2(() => getInvoiceBillsList(), {
manual: false
});
const handleSingleCheck = useCallback(
(item: invoiceBillDataType) => {
if (chosenBillDataList.find((bill) => bill._id === item._id)) {
setChosenBillDataList(chosenBillDataList.filter((bill) => bill._id !== item._id));
} else {
setChosenBillDataList([...chosenBillDataList, { _id: item._id, price: item.price }]);
}
},
[chosenBillDataList]
);
const { runAsync: onSubmitApply, loading: isSubmitting } = useRequest2(
(data) =>
submitInvoice({
amount: totalPrice,
billIdList: chosenBillDataList.map((item) => item._id),
...data
}),
{
manual: true,
successToast: t('account_bill:submit_success'),
errorToast: t('account_bill:submit_failed'),
onSuccess: () => {
onClose();
router.reload();
}
}
);
const inputForm = useForm<TeamInvoiceHeaderType>({
defaultValues: {
teamName: '',
unifiedCreditCode: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: '',
needSpecialInvoice: false,
contactPhone: '',
emailAddress: ''
}
});
const { loading: isLoadingHeader } = useRequest2(() => getTeamInvoiceHeader(), {
manual: false,
onSuccess: (res) => inputForm.reset(res)
});
const handleBack = useCallback(() => {
setChosenBillDataList([]);
getInvoiceBills();
onCloseSettleModal();
}, [getInvoiceBills, onCloseSettleModal]);
return (
<MyModal
isOpen={true}
isCentered
iconSrc="/imgs/modal/invoice.svg"
w={'43rem'}
onClose={onClose}
isLoading={isLoading}
title={t('account_bill:support_wallet_apply_invoice')}
>
{isOpenSettleModal ? (
<>
<ModalBody>
<Box w={'100%'} fontSize={'0.875rem'}>
<Flex w={'100%'} justifyContent={'space-between'}>
<Box>{t('account_bill:total_amount')}</Box>
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(totalPrice) })}</Box>
</Flex>
<Box w={'100%'} py={4}>
<Divider showBorderBottom={false} />
</Box>
</Box>
<MyBox isLoading={isLoadingHeader}>
<Flex justify={'center'}>
<InvoiceHeaderSingleForm inputForm={inputForm} required />
</Flex>
</MyBox>
<Flex
align={'center'}
w={'19.8rem'}
h={'1.75rem'}
mt={4}
px={'0.75rem'}
py={'0.38rem'}
bg={'blue.50'}
borderRadius={'sm'}
color={'blue.600'}
>
<MyIcon name="infoRounded" w={'14px'} h={'14px'} />
<Box ml={2} fontSize={'0.6875rem'}>
{t('account_bill:invoice_sending_info')}
</Box>
</Flex>
</ModalBody>
<ModalFooter>
<Button variant={'outline'} mr={'0.75rem'} px="0" onClick={handleBack}>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{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('account_bill:confirm')}
</Box>
</Flex>
</Button>
</ModalFooter>
</>
) : (
<>
<ModalBody>
<Box fontWeight={500} fontSize={'1rem'} pb={'0.75rem'}>
{t('account_bill:support_wallet_apply_invoice')}
</Box>
<TableContainer minH={'50vh'}>
<Table>
<Thead>
<Tr>
<Th>
<Checkbox
isChecked={
chosenBillDataList.length === billsList?.length && billsList?.length !== 0
}
onChange={(e) => {
!e.target.checked
? setChosenBillDataList([])
: setChosenBillDataList(
billsList?.map((item) => ({
_id: item._id,
price: item.price
})) || []
);
}}
/>
</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'}>
{billsList?.map((item) => (
<Tr
cursor={'pointer'}
key={item._id}
onClick={(e: any) => {
if (e.target?.name && e.target.name === 'check') return;
handleSingleCheck(item);
}}
_hover={{
bg: 'blue.50'
}}
>
<Td>
<Checkbox
name="check"
isChecked={chosenBillDataList.some((i) => i._id === item._id)}
/>
</Td>
<Td>{t(billTypeMap[item.type]?.label as any)}</Td>
<Td>
{item.createTime
? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss')
: '-'}
</Td>
<Td>
{t('account_bill:yuan', { amount: formatStorePrice2Read(item.price) })}
</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && billsList && billsList.length === 0 && (
<Flex
mt={'20vh'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('account_bill:no_invoice_record')}
</Box>
</Flex>
)}
</TableContainer>
</ModalBody>
<ModalFooter>
<Button
variant={'primary'}
px="0"
isDisabled={!chosenBillDataList.length}
onClick={() => {
let total = chosenBillDataList.reduce((acc, cur) => acc + Number(cur.price), 0);
if (!total) return;
setTotalPrice(total);
onOpenSettleModal();
}}
>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('account_bill:confirm')}
</Box>
</Flex>
</Button>
</ModalFooter>
</>
)}
</MyModal>
);
};
export default ApplyInvoiceModal;

View File

@@ -1,264 +0,0 @@
import React, { useState, useMemo, useEffect } from 'react';
import {
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box,
ModalBody
} from '@chakra-ui/react';
import { getBills, checkBalancePayResult } from '@/web/support/wallet/bill/api';
import type { BillSchemaType } from '@fastgpt/global/support/wallet/bill/type.d';
import dayjs from 'dayjs';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import {
BillTypeEnum,
billPayWayMap,
billStatusMap,
billTypeMap
} from '@fastgpt/global/support/wallet/bill/constants';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const BillTable = () => {
const { t } = useTranslation();
const { toast } = useToast();
const [billType, setBillType] = useState<BillTypeEnum | undefined>(undefined);
const [billDetail, setBillDetail] = useState<BillSchemaType>();
const billTypeList = useMemo(
() =>
[
{ label: t('account_bill:all'), value: undefined },
...Object.entries(billTypeMap).map(([key, value]) => ({
label: t(value.label as any),
value: key
}))
] as {
label: string;
value: BillTypeEnum | undefined;
}[],
[t]
);
const {
data: bills,
isLoading,
Pagination,
getData,
total
} = usePagination(getBills, {
pageSize: 20,
params: {
type: billType
},
defaultRequest: false
});
const { mutate: handleRefreshPayOrder, isLoading: isRefreshing } = useRequest({
mutationFn: async (payId: string) => {
try {
const data = await checkBalancePayResult(payId);
toast({
title: data,
status: 'success'
});
} catch (error: any) {
toast({
title: error?.message,
status: 'warning'
});
console.log(error);
}
try {
getData(1);
} catch (error) {}
}
});
useEffect(() => {
getData(1);
}, [billType]);
return (
<MyBox
isLoading={isLoading || isRefreshing}
position={'relative'}
h={'100%'}
minH={'50vh'}
overflow={'overlay'}
>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>#</Th>
<Th>
<MySelect
list={billTypeList}
value={billType}
size={'sm'}
onchange={(e) => {
setBillType(e);
}}
w={'130px'}
></MySelect>
</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>
<Tbody fontSize={'sm'}>
{bills.map((item, i) => (
<Tr key={item._id}>
<Td>{i + 1}</Td>
<Td>{t(billTypeMap[item.type]?.label as any)}</Td>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</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('account_bill:update')}
</Button>
)}
<Button variant={'whiteBase'} size={'sm'} onClick={() => setBillDetail(item)}>
{t('account_bill:detail')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{total >= 20 && (
<Flex mt={3} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
{!isLoading && bills.length === 0 && (
<Flex
mt={'20vh'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('account_bill:no_invoice_record')}
</Box>
</Flex>
)}
</TableContainer>
{!!billDetail && (
<BillDetailModal bill={billDetail} onClose={() => setBillDetail(undefined)} />
)}
</MyBox>
);
};
export default BillTable;
function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: () => void }) {
const { t } = useTranslation();
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('account_bill:bill_detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<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('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('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('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('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('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('account_bill:has_invoice')}:</FormLabel>
{bill.metadata.payWay === 'balance' ? (
t('user:bill.not_need_invoice')
) : (
<Box>
{
(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('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('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('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('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('account_bill:extra_ai_points')}:</FormLabel>
<Box>{bill.metadata.extraPoints}</Box>
</Flex>
)}
</ModalBody>
</MyModal>
);
}

View File

@@ -1,247 +0,0 @@
import Divider from '@/pages/app/detail/components/WorkflowComponents/Flow/components/Divider';
import { getTeamInvoiceHeader, updateTeamInvoiceHeader } from '@/web/support/user/team/api';
import { Box, Button, Flex, HStack, Input, InputProps, Radio, RadioGroup } from '@chakra-ui/react';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { UseFormReturn, useForm } from 'react-hook-form';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
export const InvoiceHeaderSingleForm = ({
inputForm,
required = false
}: {
inputForm: UseFormReturn<TeamInvoiceHeaderType, any>;
required?: boolean;
}) => {
const { t } = useTranslation();
const { watch, register } = inputForm;
const needSpecialInvoice = watch('needSpecialInvoice');
const styles: InputProps = {
bg: 'myGray.50',
w: '21.25rem'
};
return (
<>
<Flex
w={['auto', '36rem']}
flexDir={'column'}
gap={'1rem'}
fontWeight={'500'}
color={'myGray.900'}
fontSize={'14px'}
>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={required}>{t('account_bill:organization_name')}</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:organization_name')}
{...register('teamName', { required })}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={required}>{t('account_bill:unit_code')}</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:unit_code')}
{...register('unifiedCreditCode', {
required
})}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice && required}>
{t('account_bill:company_address')}
</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:company_address')}
{...register('companyAddress', { required: !!needSpecialInvoice && required })}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice && required}>
{t('account_bill:company_phone')}
</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:company_phone')}
{...register('companyPhone', { required: !!needSpecialInvoice && required })}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice && required}>
{t('account_bill:bank_name')}
</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:bank_name')}
{...register('bankName', { required: !!needSpecialInvoice && required })}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice && required}>
{t('account_bill:bank_account')}
</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:bank_account')}
{...register('bankAccount', { required: !!needSpecialInvoice && required })}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={required}>{t('account_bill:need_special_invoice')}</FormLabel>
{/* @ts-ignore */}
<RadioGroup
value={`${needSpecialInvoice}`}
onChange={(e) => {
inputForm.setValue('needSpecialInvoice', e === 'true');
}}
w={'21.25rem'}
>
<HStack h={'2rem'}>
<Radio value="true" pr={'1rem'}>
<Box fontSize={'14px'}>{t('account_bill:yes')}</Box>
</Radio>
<Radio value="false">
<Box fontSize={'14px'}>{t('account_bill:no')}</Box>
</Radio>
</HStack>
</RadioGroup>
</Flex>
<Box w={'100%'}>
<Divider showBorderBottom={false} />
</Box>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={required}>{t('account_bill:contact_phone')}</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:contact_phone')}
{...register('contactPhone', {
required,
pattern: {
value: /^[1]{1}[0-9]{10}$/,
message: t('account_bill:contact_phone_void')
}
})}
/>
</Flex>
<Flex
justify={'space-between'}
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={required}>{t('account_bill:email_address')}</FormLabel>
<Input
{...styles}
placeholder={t('account_bill:email_address')}
{...register('emailAddress', {
required,
pattern: {
value: /(^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$)/,
message: t('user:password.email_phone_error')
}
})}
/>
</Flex>
</Flex>
</>
);
};
const InvoiceHeaderForm = () => {
const inputForm = useForm<TeamInvoiceHeaderType>({
defaultValues: {
teamName: '',
unifiedCreditCode: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: '',
needSpecialInvoice: false,
emailAddress: '',
contactPhone: ''
}
});
const { loading: isLoading } = useRequest2(() => getTeamInvoiceHeader(), {
manual: false,
onSuccess: (data) => {
console.log(data, '--');
inputForm.reset(data);
}
});
const { t } = useTranslation();
const { loading: isSubmitting, runAsync: onUpdateHeader } = useRequest2(
(data: TeamInvoiceHeaderType) => updateTeamInvoiceHeader(data),
{
manual: true,
successToast: t('account_bill:save_success'),
errorToast: t('account_bill:save_failed')
}
);
return (
<>
<MyBox isLoading={isLoading} pt={['1rem', '3.5rem']}>
<Flex w={'100%'} overflow={'auto'} justify={'center'} flexDir={'column'} align={'center'}>
<InvoiceHeaderSingleForm inputForm={inputForm} />
<Flex w={'100%'} justify={'center'} mt={'3rem'}>
<Button
variant={'primary'}
px="0"
onClick={inputForm.handleSubmit(onUpdateHeader)}
isLoading={isSubmitting}
>
<Flex alignItems={'center'} px={'20px'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('account_bill:save')}
</Box>
</Flex>
</Button>
</Flex>
</Flex>
</MyBox>
</>
);
};
export default InvoiceHeaderForm;

View File

@@ -1,180 +0,0 @@
import { getInvoiceRecords } from '@/web/support/wallet/bill/invoice/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import {
Box,
Button,
Flex,
FormLabel,
ModalBody,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr
} from '@chakra-ui/react';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { InvoiceSchemaType } from '@fastgpt/global/support/wallet/bill/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dayjs from 'dayjs';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import MyModal from '@fastgpt/web/components/common/MyModal';
const InvoiceTable = () => {
const { t } = useTranslation();
const [invoiceDetailData, setInvoiceDetailData] = useState<InvoiceSchemaType | ''>('');
const {
data: invoices,
isLoading,
Pagination,
total
} = usePagination(getInvoiceRecords, {
pageSize: 20
});
return (
<MyBox isLoading={isLoading} position={'relative'} h={'100%'} overflow={'overlay'}>
<TableContainer minH={'50vh'}>
<Table>
<Thead h="3rem">
<Tr>
<Th w={'20%'}>#</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>
<Tbody fontSize={'sm'}>
{invoices.map((item, i) => (
<Tr key={item._id}>
<Td>{i + 1}</Td>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{t('account_bill:yuan', { amount: formatStorePrice2Read(item.amount) })}</Td>
<Td>
<Flex
px={'0.75rem'}
py={'0.38rem'}
w={'4.25rem'}
h={'1.75rem'}
bg={item.status === 1 ? 'blue.50' : 'green.50'}
rounded={'md'}
justify={'center'}
align={'center'}
color={item.status === 1 ? 'blue.600' : 'green.600'}
>
<MyIcon name="point" w={'6px'} h={'6px'} />
<Box ml={'0.25rem'}>
{item.status === 1
? t('account_bill:submitted')
: t('account_bill:completed')}
</Box>
</Flex>
</Td>
<Td>
<Button
onClick={() => setInvoiceDetailData(item)}
h={'2rem'}
w={'4.5rem'}
variant={'whiteBase'}
size={'sm'}
py={'0.5rem'}
px={'0.75rem'}
_hover={{
color: 'blue.600'
}}
>
<Flex>
<MyIcon name="paragraph" w={'16px'} h={'16px'} />
<Box ml={'0.38rem'}>{t('account_bill:detail')}</Box>
</Flex>
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{total >= 20 && (
<Flex mt={3} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
{!isLoading && invoices.length === 0 && (
<Flex
mt={'20vh'}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('account_bill:no_invoice_record_tip')}
</Box>
</Flex>
)}
</TableContainer>
{!!invoiceDetailData && (
<InvoiceDetailModal invoice={invoiceDetailData} onClose={() => setInvoiceDetailData('')} />
)}
</MyBox>
);
};
export default InvoiceTable;
function InvoiceDetailModal({
invoice,
onClose
}: {
invoice: InvoiceSchemaType;
onClose: () => void;
}) {
const { t } = useTranslation();
return (
<MyModal
maxW={['90vw', '700px']}
isOpen={true}
onClose={onClose}
title={
<Flex align={'center'}>
<MyIcon name="paragraph" w={'20px'} h={'20px'} color={'blue.600'} />
<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('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('account_bill:need_special_invoice')}
value={invoice.needSpecialInvoice ? t('account_bill:yes') : t('account_bill:no')}
/>
<LabelItem label={t('account_bill:contact_phone')} value={invoice.contactPhone} />
<LabelItem label={t('account_bill:email_address')} value={invoice.emailAddress} />
</Flex>
</ModalBody>
</MyModal>
);
}
function LabelItem({ label, value }: { label: string; value?: string }) {
return (
<Flex alignItems={'center'} justify={'space-between'}>
<FormLabel flex={'0 0 120px'}>{label}</FormLabel>
<Box>{value || '-'}</Box>
</Flex>
);
}

View File

@@ -3,9 +3,9 @@ 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 './components/ApplyInvoiceModal';
import ApplyInvoiceModal from '@/pageComponents/account/bill/ApplyInvoiceModal';
import { useRouter } from 'next/router';
import AccountContainer, { TabEnum } from '../components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
export enum InvoiceTabEnum {
@@ -14,9 +14,9 @@ export enum InvoiceTabEnum {
invoiceHeader = 'invoiceHeader'
}
const BillTable = dynamic(() => import('./components/BillTable'));
const InvoiceHeaderForm = dynamic(() => import('./components/InvoiceHeaderForm'));
const InvoiceTable = dynamic(() => import('./components/InvoiceTable'));
const BillTable = dynamic(() => import('@/pageComponents/account/bill/BillTable'));
const InvoiceHeaderForm = dynamic(() => import('@/pageComponents/account/bill/InvoiceHeaderForm'));
const InvoiceTable = dynamic(() => import('@/pageComponents/account/bill/InvoiceTable'));
const BillAndInvoice = () => {
const { t } = useTranslation();
const router = useRouter();

View File

@@ -1,195 +0,0 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router';
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 { useTranslation } from 'next-i18next';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
export enum TabEnum {
'info' = 'info',
'promotion' = 'promotion',
'usage' = 'usage',
'bill' = 'bill',
'inform' = 'inform',
'setting' = 'setting',
'thirdParty' = 'thirdParty',
'individuation' = 'individuation',
'apikey' = 'apikey',
'loginout' = 'loginout',
'team' = 'team',
'model' = 'model'
}
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 = useRef([
{
icon: 'support/user/userLight',
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('account:usage_records'),
value: TabEnum.usage
}
]
: []),
...(feConfigs?.show_pay && userInfo?.team?.permission.hasManagePer
? [
{
icon: 'support/bill/payRecordLight',
label: t('account:bills_and_invoices'),
value: TabEnum.bill
}
]
: []),
{
icon: 'common/thirdParty',
label: t('account:third_party'),
value: TabEnum.thirdParty
},
{
icon: 'common/model',
label: t('account:model_provider'),
value: TabEnum.model
},
...(feConfigs?.show_promotion && userInfo?.team?.permission.isOwner
? [
{
icon: 'support/account/promotionLight',
label: t('account:promotion_records'),
value: TabEnum.promotion
}
]
: []),
...(userInfo?.team?.permission.hasManagePer
? [
{
icon: 'key',
label: t('account:api_key'),
value: TabEnum.apikey
}
]
: []),
...(feConfigs.isPlus
? [
{
icon: 'support/user/informLight',
label: t('account:notifications'),
value: TabEnum.inform
}
]
: []),
{
icon: 'common/settingLight',
label: t('common:common.Setting'),
value: TabEnum.setting
},
{
icon: 'support/account/loginoutLight',
label: t('account:logout'),
value: TabEnum.loginout
}
]);
const { openConfirm, ConfirmModal } = useConfirm({
content: t('account:confirm_logout')
});
const setCurrentTab = useCallback(
(tab: string) => {
if (tab === TabEnum.loginout) {
openConfirm(() => {
setUserInfo(null);
router.replace('/login');
})();
} else {
router.replace('/account/' + tab);
}
},
[openConfirm, router, setUserInfo]
);
return (
<PageContainer isLoading={isLoading}>
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
{isPc ? (
<Flex
flexDirection={'column'}
p={4}
h={'100%'}
flex={'0 0 200px'}
borderRight={theme.borders.base}
>
<SideTabs<TabEnum>
flex={1}
mx={'auto'}
mt={2}
w={'100%'}
list={tabList.current}
value={currentTab}
onChange={setCurrentTab}
/>
<Flex alignItems={'center'}>
<Box w={'8px'} h={'8px'} borderRadius={'50%'} bg={'#67c13b'} />
<Box fontSize={'md'} ml={2}>
V{systemVersion}
</Box>
</Flex>
</Flex>
) : (
<Box mb={3}>
<LightRowTabs<TabEnum>
m={'auto'}
w={'100%'}
size={isPc ? 'md' : 'sm'}
list={tabList.current.map((item) => ({
value: item.value,
label: item.label
}))}
value={currentTab}
onChange={setCurrentTab}
/>
</Box>
)}
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]} overflow={'auto'}>
{children}
</Box>
</Flex>
<ConfirmModal />
</PageContainer>
);
};
export default AccountContainer;

View File

@@ -1,104 +0,0 @@
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={'pointer'}
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, t, teamList, router]);
return (
<Box w={'100%'}>
<MySelect {...props} value={userInfo?.team?.teamId} list={formatTeamList} />
</Box>
);
};
export default TeamSelector;

View File

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

View File

@@ -1,107 +0,0 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updatePasswordByOld } from '@/web/support/user/api';
import { PasswordRule } from '@/web/support/user/login/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
type FormType = {
oldPsw: string;
newPsw: string;
confirmPsw: string;
};
const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, handleSubmit, getValues } = useForm<FormType>({
defaultValues: {
oldPsw: '',
newPsw: '',
confirmPsw: ''
}
});
const { runAsync: onSubmit, loading: isLoading } = useRequest2(updatePasswordByOld, {
onSuccess() {
onClose();
},
successToast: t('account_info:password_update_success'),
errorToast: t('account_info:password_update_error')
});
const onSubmitErr = (err: Record<string, any>) => {
const val = Object.values(err)[0];
if (!val) return;
if (val.message) {
toast({
status: 'warning',
title: val.message,
duration: 3000,
isClosable: true
});
}
};
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="/imgs/modal/password.svg"
title={t('account_info:update_password')}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{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'} fontSize={'sm'}>
{t('account_info:new_password') + ':'}
</Box>
<Input
flex={1}
type={'password'}
placeholder={t('account_info:password_tip')}
{...register('newPsw', {
required: true,
pattern: {
value: PasswordRule,
message: t('account_info:password_tip')
}
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:confirm_password') + ':'}
</Box>
<Input
flex={1}
type={'password'}
placeholder={t('user:password.confirm')}
{...register('confirmPsw', {
required: true,
validate: (val) => (getValues('newPsw') === val ? true : t('user:password.not_match'))
})}
></Input>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data), onSubmitErr)}>
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default UpdatePswModal;

View File

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

View File

@@ -38,14 +38,17 @@ import StandardPlanContentList from '@/components/support/wallet/StandardPlanCon
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import AccountContainer from '../components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useRouter } from 'next/router';
import TeamSelector from '../components/TeamSelector';
import TeamSelector from '@/pageComponents/account/TeamSelector';
const StandDetailModal = dynamic(() => import('./components/standardDetailModal'), { ssr: false });
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
const StandDetailModal = dynamic(
() => import('@/pageComponents/account/info/standardDetailModal'),
{ ssr: false }
);
const ConversionModal = dynamic(() => import('@/pageComponents/account/info/ConversionModal'));
const UpdatePswModal = dynamic(() => import('@/pageComponents/account/info/UpdatePswModal'));
const UpdateNotification = dynamic(
() => import('@/components/support/user/inform/UpdateNotificationModal')
);

View File

@@ -6,7 +6,7 @@ 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 from './components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const InformTable = () => {

View File

@@ -1,84 +0,0 @@
import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, ModalBody } from '@chakra-ui/react';
import { MultipleRowArraySelect } from '@fastgpt/web/components/common/MySelect/MultipleRowSelect';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ModelProviderList } from '@fastgpt/global/core/ai/provider';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
import { getModelFromList } from '@fastgpt/global/core/ai/model';
const DefaultModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList } =
useSystemStore();
const [value, setValue] = useState<string[]>([]);
const modelList = useMemo(() => {
return [
...llmModelList,
...vectorModelList,
...audioSpeechModelList,
...reRankModelList,
whisperModel
].map((item) => ({
provider: item.provider,
name: item.name,
model: item.model
}));
}, [llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList]);
const selectorList = useMemo(() => {
const renderList = ModelProviderList.map<{
label: React.JSX.Element;
value: string;
children: { label: string | React.ReactNode; value: string }[];
}>((provider) => ({
label: (
<Flex alignItems={'center'} py={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={provider?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'1rem'}
/>
<Box>{t(provider.name as any)}</Box>
</Flex>
),
value: provider.id,
children: []
}));
for (const item of modelList) {
const modelData = getModelFromList(modelList, item.model);
const provider =
renderList.find((item) => item.value === (modelData?.provider || 'Other')) ??
renderList[renderList.length - 1];
provider.children.push({
label: modelData.name,
value: modelData.model
});
}
return renderList.filter((item) => item.children.length > 0);
}, [modelList, t]);
console.log(selectorList);
return (
<MyModal
isOpen
title={t('account:add_default_model')}
iconSrc="common/model"
iconColor="primary.600"
onClose={onClose}
>
<ModalBody>11</ModalBody>
</MyModal>
);
};
export default DefaultModal;

View File

@@ -1,72 +1,43 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import React, { useState } from 'react';
import AccountContainer from '../components/AccountContainer';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import React, { useMemo, useState } from 'react';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { Box, Flex } from '@chakra-ui/react';
import ModelTable from '@/components/core/ai/ModelTable';
import { useUserStore } from '@/web/support/user/useUserStore';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import dynamic from 'next/dynamic';
const DefaultModal = dynamic(() => import('./components/DefaultModal'), {
ssr: false
});
const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
type TabType = 'model' | 'config' | 'channel';
const ModelProvider = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const [tab, setTab] = useState<'model' | 'channel'>('model');
const [tab, setTab] = useState<TabType>('model');
const { isOpen: isOpenDefault, onOpen: onOpenDefault, onClose: onCloseDefault } = useDisclosure();
const Tab = useMemo(() => {
return (
<FillRowTabs<TabType>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:config_model'), value: 'config' }
// { label: t('account:channel'), value: 'channel' }
]}
value={tab}
py={1}
onChange={setTab}
/>
);
}, [t, tab]);
return (
<AccountContainer>
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
{/* Header */}
{/* <Flex justifyContent={'space-between'}>
<FillRowTabs<'model' | 'channel'>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:channel'), value: 'channel' }
]}
value={tab}
px={8}
py={1}
onChange={setTab}
/>
{tab === 'model' && (
<MyMenu
trigger="hover"
size="mini"
Button={<Button>{t('account:create_model')}</Button>}
menuList={[
{
children: [
{
label: t('account:default_model'),
onClick: onOpenDefault
},
{
label: t('account:custom_model')
}
]
}
]}
/>
)}
{tab === 'channel' && <Button>{t('account:create_channel')}</Button>}
</Flex> */}
<Box flex={'1 0 0'}>
{tab === 'model' && <ModelTable />}
{/* {tab === 'channel' && <ChannelTable />} */}
</Box>
{tab === 'model' && <ValidModelTable Tab={Tab} />}
{tab === 'config' && <ModelConfigTable Tab={Tab} />}
</Flex>
{isOpenDefault && <DefaultModal onClose={onCloseDefault} />}
</AccountContainer>
);
};
@@ -80,3 +51,16 @@ export async function getServerSideProps(content: any) {
}
export default ModelProvider;
const ValidModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
return (
<>
{isRoot && <Flex justifyContent={'space-between'}>{Tab}</Flex>}
<Box flex={'1 0 0'}>
<ModelTable />
</Box>
</>
);
};

View File

@@ -19,13 +19,13 @@ 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 { useCopyData } from '@fastgpt/web/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 from './components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const Promotion = () => {

View File

@@ -9,7 +9,7 @@ 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 AccountContainer from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
const Individuation = () => {

View File

@@ -1,9 +1,9 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import AccountContainer from '../components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { Box, Flex } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector';
import TeamSelector from '@/pageComponents/account/TeamSelector';
import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';

View File

@@ -1,77 +0,0 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import type { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import { putUpdateTeam } from '@/web/support/user/team/api';
const OpenAIAccountModal = ({
defaultData,
onClose
}: {
defaultData?: OpenaiAccountType;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const { register, handleSubmit } = useForm({
defaultValues: defaultData
});
const { runAsync: onSubmit, loading } = useRequest2(
async (data: OpenaiAccountType) => {
if (!userInfo?.team.teamId) return;
return putUpdateTeam({
openaiAccount: data
});
},
{
onSuccess: () => {
initUserInfo();
onClose();
},
successToast: t('common:common.Update Success'),
errorToast: t('common:common.Update Failed')
}
);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="common/openai"
title={t('account_thirdParty:openai_account_configuration')}
>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('account_thirdParty:open_api_notice')}
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>
<Input flex={1} {...register('key')}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>BaseUrl:</Box>
<Input
flex={1}
{...register('baseUrl')}
placeholder={t('account_thirdParty:request_address_notice')}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={loading} onClick={handleSubmit(onSubmit)}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default OpenAIAccountModal;

View File

@@ -1,81 +0,0 @@
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { ThirdPartyAccountType } from '../index';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useUserStore } from '@/web/support/user/useUserStore';
import { putUpdateTeam } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const WorkflowVariableModal = ({
defaultData,
onClose
}: {
defaultData: ThirdPartyAccountType;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const { register, handleSubmit } = useForm({
defaultValues: {
value: '',
key: defaultData.key || ''
}
});
const { runAsync: onSubmit, loading } = useRequest2(
async (data: { key: string; value: string }) => {
if (!userInfo?.team.teamId) return;
await putUpdateTeam({
externalWorkflowVariable: data
});
},
{
onSuccess: () => {
initUserInfo();
onClose();
},
successToast: t('common:common.Update Success'),
errorToast: t('common:common.Update Failed')
}
);
return (
<MyModal title={`${defaultData.name} 配置`} iconSrc={'edit'} iconColor={'primary.600'}>
<ModalBody w={'420px'}>
<Box fontSize={'14px'} color={'myGray.900'}>
{defaultData.intro}
</Box>
<Box h={'1px'} bg={'myGray.150'} my={4}></Box>
<Flex alignItems={'center'}>
<Box fontSize={'14px'} color={'myGray.900'} fontWeight={'medium'}>
{t('common:core.workflow.value')}
</Box>
<Input
ml={8}
bg={'myGray.50'}
placeholder={t('account_thirdParty:value_placeholder')}
flex={1}
{...register('value')}
/>
</Flex>
<Box mt={1} color={'myGray.500'} fontSize={'xs'}>
{t('account_thirdParty:value_not_return_tip')}
</Box>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={loading} onClick={handleSubmit(onSubmit)}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(WorkflowVariableModal);

View File

@@ -1,4 +1,4 @@
import AccountContainer from '../components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { Box, Flex, Grid, Progress, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
@@ -7,7 +7,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import dynamic from 'next/dynamic';
import { useState, useMemo } from 'react';
import WorkflowVariableModal from './components/WorkflowVariableModal';
import WorkflowVariableModal from '@/pageComponents/account/thirdParty/WorkflowVariableModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
@@ -16,7 +16,9 @@ import type { checkUsageResponse } from '@/pages/api/support/user/team/thirtdPar
import MyBox from '@fastgpt/web/components/common/MyBox';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const OpenAIAccountModal = dynamic(() => import('./components/OpenAIAccountModal'));
const OpenAIAccountModal = dynamic(
() => import('@/pageComponents/account/thirdParty/OpenAIAccountModal')
);
export type ThirdPartyAccountType = {
name: string;

View File

@@ -1,141 +0,0 @@
import React, { useMemo } from 'react';
import {
ModalBody,
Flex,
Box,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { UsageItemType } from '@fastgpt/global/support/wallet/usage/type.d';
import dayjs from 'dayjs';
import { UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const UsageDetail = ({ usage, onClose }: { usage: UsageItemType; onClose: () => void }) => {
const { t } = useTranslation();
const filterBillList = useMemo(
() => usage.list.filter((item) => item && item.moduleName),
[usage.list]
);
const { hasModel, hasToken, hasInputToken, hasOutputToken, hasCharsLen, hasDuration } =
useMemo(() => {
let hasModel = false;
let hasToken = false;
let hasInputToken = false;
let hasOutputToken = false;
let hasCharsLen = false;
let hasDuration = false;
let hasDataLen = false;
usage.list.forEach((item) => {
if (item.model !== undefined) {
hasModel = true;
}
if (typeof item.tokens === 'number') {
hasToken = true;
}
if (typeof item.inputTokens === 'number') {
hasInputToken = true;
}
if (typeof item.outputTokens === 'number') {
hasOutputToken = true;
}
if (typeof item.charsLength === 'number') {
hasCharsLen = true;
}
if (typeof item.duration === 'number') {
hasDuration = true;
}
});
return {
hasModel,
hasToken,
hasInputToken,
hasOutputToken,
hasCharsLen,
hasDuration,
hasDataLen
};
}, [usage.list]);
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('account_usage:usage_detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<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('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('account_usage:app_name')}:</FormLabel>
<Box>{t(usage.appName as any) || '-'}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<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('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('account_usage:billing_module')}
</FormLabel>
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr>
<Th>{t('account_usage:module_name')}</Th>
{hasModel && <Th>{t('account_usage:ai_model')}</Th>}
{hasToken && <Th>{t('account_usage:token_length')}</Th>}
{hasInputToken && <Th>{t('account_usage:input_token_length')}</Th>}
{hasOutputToken && <Th>{t('account_usage:output_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>
{filterBillList.map((item, i) => (
<Tr key={i}>
<Td>{t(item.moduleName as any)}</Td>
{hasModel && <Td>{item.model ?? '-'}</Td>}
{hasToken && <Td>{item.tokens ?? '-'}</Td>}
{hasInputToken && <Td>{item.inputTokens ?? '-'}</Td>}
{hasOutputToken && <Td>{item.outputTokens ?? '-'}</Td>}
{hasCharsLen && <Td>{item.charsLength ?? '-'}</Td>}
{hasDuration && <Td>{item.duration ?? '-'}</Td>}
<Td>{formatNumber(item.amount)}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</ModalBody>
</MyModal>
);
};
export default UsageDetail;

View File

@@ -1,78 +1,64 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box,
Button
} from '@chakra-ui/react';
import { Flex, Box, HStack } 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 { addDays, startOfMonth, startOfWeek } from 'date-fns';
import { useTranslation } from 'next-i18next';
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 from '../components/AccountContainer';
import AccountContainer from '@/pageComponents/account/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getTeamMembers } from '@/web/support/user/team/api';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import MultipleSelect, {
useMultipleSelect
} from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
const UsageDetail = dynamic(() => import('./UsageDetail'));
import UsageTableList from '@/pageComponents/account/usage/UsageTable';
import { UnitType } from '@/pageComponents/account/usage/type';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const UsageDashboard = dynamic(() => import('@/pageComponents/account/usage/Dashboard'));
export enum UsageTabEnum {
detail = 'detail',
dashboard = 'dashboard'
}
const UsageTable = () => {
const { t } = useTranslation();
const { Loading } = useLoading();
const { userInfo } = useUserStore();
const { isPc } = useSystem();
const router = useRouter();
const { usageTab = UsageTabEnum.detail } = router.query as { usageTab: `${UsageTabEnum}` };
const [unit, setUnit] = useState<UnitType>('day');
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystem();
const { userInfo } = 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, ScrollData } = useScrollPagination(getTeamMembers, {});
const { data: members, ScrollData, total: memberTotal } = useScrollPagination(getTeamMembers, {});
const {
value: selectTmbIds,
setValue: setSelectTmbIds,
isSelectAll: isSelectAllTmb,
setIsSelectAll: setIsSelectAllTmb
} = useMultipleSelect<string>([], true);
const tmbList = useMemo(
() =>
members.map((item) => ({
label: (
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'16px'} mr={1} />
{item.memberName}
</Flex>
<HStack spacing={1} color={'myGray.500'}>
<Avatar src={item.avatar} w={'1.2rem'} mr={1} rounded={'full'} />
<Box>{item.memberName}</Box>
</HStack>
),
value: item.tmbId
})),
@@ -80,121 +66,195 @@ const UsageTable = () => {
);
const {
data: usages,
isLoading,
Pagination,
getData
} = usePagination(getUserUsages, {
pageSize: isPc ? 20 : 10,
params: {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
source: usageSource as UsageSourceEnum,
teamMemberId: selectTmbId ?? ''
},
defaultRequest: false
});
value: usageSources,
setValue: setUsageSources,
isSelectAll: isSelectAllSource,
setIsSelectAll: setIsSelectAllSource
} = useMultipleSelect<UsageSourceEnum>(Object.values(UsageSourceEnum), true);
const sourceList = useMemo(
() =>
Object.entries(UsageSourceMap).map(([key, value]) => ({
label: t(value.label as any),
value: key as UsageSourceEnum
})),
[t]
);
const [projectName, setProjectName] = useState<string>('');
const [inputValue, setInputValue] = useState('');
const Tabs = useMemo(
() => (
<FillRowTabs
list={[
{ label: t('account_usage:usage_detail'), value: 'detail' },
{ label: t('account_usage:dashboard'), value: 'dashboard' }
]}
py={1}
value={usageTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
usageTab: e
}
});
}}
/>
),
[router, t, usageTab]
);
const Selectors = useMemo(
() => (
<Flex flexDir={['column', 'row']} alignItems={'center'} gap={3}>
<Flex alignItems={'center'} gap={2}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
{t('common:user.Time')}
</Box>
<DateRangePicker
defaultDate={dateRange}
dateRange={dateRange}
position="bottom"
onSuccess={setDateRange}
/>
{/* {usageTab === UsageTabEnum.dashboard && (
<MySelect<UnitType>
bg={'myGray.50'}
minH={'32px'}
height={'32px'}
fontSize={'mini'}
ml={1}
list={[
{ label: t('account_usage:every_day'), value: 'day' },
{ label: t('account_usage:every_month'), value: 'month' }
]}
value={unit}
onchange={setUnit}
/>
)} */}
</Flex>
{userInfo?.team?.permission.hasManagePer && (
<Flex alignItems={'center'} gap={2}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
{t('account_usage:member')}
</Box>
<Box>
<MultipleSelect<string>
list={tmbList}
value={selectTmbIds}
onSelect={(val) => {
setSelectTmbIds(val as string[]);
}}
itemWrap={false}
height={'32px'}
bg={'myGray.50'}
w={'160px'}
ScrollData={ScrollData}
isSelectAll={isSelectAllTmb}
setIsSelectAll={setIsSelectAllTmb}
/>
</Box>
</Flex>
)}
<Flex alignItems={'center'} gap={2}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
{t('account_usage:source')}
</Box>
<Box>
<MultipleSelect<UsageSourceEnum>
list={sourceList}
value={usageSources}
onSelect={setUsageSources}
isSelectAll={isSelectAllSource}
setIsSelectAll={setIsSelectAllSource}
itemWrap={false}
height={'32px'}
bg={'myGray.50'}
w={'160px'}
/>
</Box>
</Flex>
{/* {usageTab === UsageTabEnum.detail && (
<Flex alignItems={'center'}>
<Box
fontSize={'mini'}
fontWeight={'medium'}
color={'myGray.900'}
mr={4}
whiteSpace={'nowrap'}
>
{t('common:user.Application Name')}
</Box>
<SearchInput
placeholder={t('common:user.Application Name')}
w={'160px'}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</Flex>
)} */}
</Flex>
),
[
t,
dateRange,
usageTab,
unit,
userInfo?.team?.permission.hasManagePer,
tmbList,
selectTmbIds,
ScrollData,
isSelectAllTmb,
setIsSelectAllTmb,
sourceList,
usageSources,
setUsageSources,
isSelectAllSource,
setIsSelectAllSource,
setSelectTmbIds
]
);
useEffect(() => {
getData(1);
}, [usageSource, selectTmbId]);
const timer = setTimeout(() => {
setProjectName(inputValue);
}, 300);
return () => clearTimeout(timer);
}, [inputValue]);
const filterParams = useMemo(
() => ({
dateRange,
selectTmbIds,
projectName,
isSelectAllTmb,
usageSources,
isSelectAllSource,
unit
}),
[dateRange, isSelectAllSource, unit, isSelectAllTmb, projectName, selectTmbIds, usageSources]
);
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'}
ScrollData={ScrollData}
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)} />
<Box
px={[3, 8]}
pt={[0, 4]}
pb={[0, 4]}
h={'full'}
overflow={'hidden'}
display={'flex'}
flexDirection={'column'}
>
{usageTab === UsageTabEnum.detail && (
<UsageTableList filterParams={filterParams} Tabs={Tabs} Selectors={Selectors} />
)}
</Flex>
{usageTab === UsageTabEnum.dashboard && (
<UsageDashboard filterParams={filterParams} Tabs={Tabs} Selectors={Selectors} />
)}
</Box>
</AccountContainer>
);
};

View File

@@ -0,0 +1,77 @@
import { readConfigData } from '@/service/common/system';
import { NextAPI } from '@/service/middleware/entry';
import {
getFastGPTConfigFromDB,
updateFastGPTConfigBuffer
} from '@fastgpt/service/common/system/config/controller';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextApiRequest, NextApiResponse } from 'next';
import json5 from 'json5';
import { FastGPTConfigFileType } from '@fastgpt/global/common/system/types';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { loadSystemModels } from '@fastgpt/service/core/ai/config/utils';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
/*
简单版迁移:直接升级到最新镜像,会去除 MongoDatasetData 里的索引。直接执行这个脚本。
无缝迁移:
1. 移动 User 表中的 avatar 字段到 TeamMember 表中。
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
// load config
const [{ config: dbConfig }, fileConfig] = await Promise.all([
getFastGPTConfigFromDB(),
readConfigData('config.json')
]);
const fileRes = json5.parse(fileConfig) as FastGPTConfigFileType;
const llmModels = dbConfig.llmModels || fileRes.llmModels || [];
const vectorModels = dbConfig.vectorModels || fileRes.vectorModels || [];
const reRankModels = dbConfig.reRankModels || fileRes.reRankModels || [];
const audioSpeechModels = dbConfig.audioSpeechModels || fileRes.audioSpeechModels || [];
const whisperModel = dbConfig.whisperModel || fileRes.whisperModel;
const list = [
...llmModels.map((item) => ({
...item,
type: ModelTypeEnum.llm
})),
...vectorModels.map((item) => ({
...item,
type: ModelTypeEnum.embedding
})),
...reRankModels.map((item) => ({
...item,
type: ModelTypeEnum.rerank
})),
...audioSpeechModels.map((item) => ({
...item,
type: ModelTypeEnum.tts
})),
{
...whisperModel,
type: ModelTypeEnum.stt
}
];
for await (const item of list) {
try {
await MongoSystemModel.updateOne(
{ model: item.model },
{ $set: { model: item.model, metadata: { ...item, isActive: true } } },
{ upsert: true }
);
} catch (error) {
console.log(error);
}
}
await loadSystemModels(true);
await updateFastGPTConfigBuffer();
return { success: true };
}
export default NextAPI(handler);

View File

@@ -1,10 +1,29 @@
import type { NextApiResponse } from 'next';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { InitDateResponse } from '@/global/common/api/systemRes';
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
async function handler(req: ApiRequestProps<{}, { bufferId?: string }>, res: NextApiResponse) {
async function handler(
req: ApiRequestProps<{}, { bufferId?: string }>,
res: NextApiResponse
): Promise<InitDateResponse> {
const { bufferId } = req.query;
const activeModelList = global.systemActiveModelList.map((model) => ({
...model,
customCQPrompt: undefined,
customExtractPrompt: undefined,
defaultSystemChatPrompt: undefined,
fieldMap: undefined,
defaultConfig: undefined,
weight: undefined,
dbConfig: undefined,
queryConfig: undefined,
requestUrl: undefined,
requestAuth: undefined
})) as SystemModelItemType[];
// If bufferId is the same as the current bufferId, return directly
if (bufferId && global.systemInitBufferId && global.systemInitBufferId === bufferId) {
return {
@@ -17,22 +36,9 @@ async function handler(req: ApiRequestProps<{}, { bufferId?: string }>, res: Nex
bufferId: global.systemInitBufferId,
feConfigs: global.feConfigs,
subPlans: global.subPlans,
llmModels: global.llmModels.map((model) => ({
...model,
customCQPrompt: '',
customExtractPrompt: '',
defaultSystemChatPrompt: ''
})),
vectorModels: global.vectorModels,
reRankModels:
global.reRankModels?.map((item) => ({
...item,
requestUrl: '',
requestAuth: ''
})) || [],
whisperModel: global.whisperModel,
audioSpeechModels: global.audioSpeechModels,
systemVersion: global.systemVersion || '0.0.0'
systemVersion: global.systemVersion || '0.0.0',
activeModelList,
defaultModels: global.systemDefaultModel
};
}

View File

@@ -4,7 +4,7 @@ import { addLog } from '@fastgpt/service/common/system/log';
import { TrackEnum } from '@fastgpt/global/common/middle/tracks/constants';
import { TrackModel } from '@fastgpt/service/common/middle/tracks/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
export type pushQuery = {};
@@ -38,7 +38,7 @@ async function handler(
return TrackModel.create(data);
}
export default NextAPI(useReqFrequencyLimit(1, 5), handler);
export default NextAPI(useIPFrequencyLimit({ id: 'push-tracks', seconds: 1, limit: 5 }), handler);
export const config = {
api: {

View File

@@ -16,6 +16,7 @@ import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSc
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';
import { getDefaultLLMModel } from '@fastgpt/service/core/ai/model';
async function handler(
req: ApiRequestProps<
@@ -35,7 +36,7 @@ async function handler(
authApiKey: true
});
const qgModel = global.llmModels[0];
const qgModel = getDefaultLLMModel();
const { result, inputTokens, outputTokens } = await createQuestionGuide({
messages,
@@ -47,6 +48,7 @@ async function handler(
});
pushQuestionGuideUsage({
model: qgModel.model,
inputTokens,
outputTokens,
teamId,

View File

@@ -9,6 +9,7 @@ import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { getDefaultLLMModel } from '@fastgpt/service/core/ai/model';
export type CreateQuestionGuideParams = OutLinkChatAuthProps & {
appId: string;
@@ -50,7 +51,7 @@ async function handler(req: ApiRequestProps<CreateQuestionGuideParams>, res: Nex
});
const messages = chats2GPTMessages({ messages: histories, reserveId: false });
const qgModel = questionGuide?.model || global.llmModels[0].model;
const qgModel = questionGuide?.model || getDefaultLLMModel().model;
const { result, inputTokens, outputTokens } = await createQuestionGuide({
messages,
@@ -59,6 +60,7 @@ async function handler(req: ApiRequestProps<CreateQuestionGuideParams>, res: Nex
});
pushQuestionGuideUsage({
model: qgModel,
inputTokens,
outputTokens,
teamId,

View File

@@ -0,0 +1,42 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { findModelFromAlldata } from '@fastgpt/service/core/ai/model';
import { updateFastGPTConfigBuffer } from '@fastgpt/service/common/system/config/controller';
import { loadSystemModels, updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
export type deleteQuery = {
model: string;
};
export type deleteBody = {};
export type deleteResponse = {};
async function handler(
req: ApiRequestProps<deleteBody, deleteQuery>,
res: ApiResponseType<any>
): Promise<deleteResponse> {
await authSystemAdmin({ req });
const { model } = req.query;
const modelData = findModelFromAlldata(model);
if (!modelData) {
return Promise.reject('Model not found');
}
if (!modelData.isCustom) {
return Promise.reject('System model cannot be deleted');
}
await MongoSystemModel.deleteOne({ model });
await updatedReloadSystemModel();
return {};
}
export default NextAPI(handler);

View File

@@ -0,0 +1,29 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { findModelFromAlldata } from '@fastgpt/service/core/ai/model';
export type detailQuery = {
model: string;
};
export type detailBody = {};
export type detailResponse = SystemModelItemType;
async function handler(
req: ApiRequestProps<detailBody, detailQuery>,
res: ApiResponseType<any>
): Promise<detailResponse> {
await authSystemAdmin({ req });
const { model } = req.query;
const modelItem = findModelFromAlldata(model);
if (!modelItem) {
return Promise.reject('Model not found');
}
return modelItem;
}
export default NextAPI(handler);

View File

@@ -0,0 +1,29 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
export type getConfigJsonQuery = {};
export type getConfigJsonBody = {};
export type getConfigJsonResponse = {};
async function handler(
req: ApiRequestProps<getConfigJsonBody, getConfigJsonQuery>,
res: ApiResponseType<any>
): Promise<getConfigJsonResponse> {
await authSystemAdmin({ req });
const data = await MongoSystemModel.find({}).lean();
return JSON.stringify(
data.map((item) => ({
model: item.model,
metadata: item.metadata
})),
null,
2
);
}
export default NextAPI(handler);

View File

@@ -0,0 +1,22 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { getSystemModelConfig } from '@fastgpt/service/core/ai/config/utils';
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
export type getDefaultQuery = { model: string };
export type getDefaultBody = {};
async function handler(
req: ApiRequestProps<getDefaultBody, getDefaultQuery>,
res: ApiResponseType<any>
): Promise<SystemModelItemType> {
await authSystemAdmin({ req });
const model = req.query.model;
return getSystemModelConfig(model);
}
export default NextAPI(handler);

View File

@@ -0,0 +1,57 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { ModelProviderIdType } from '@fastgpt/global/core/ai/provider';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
export type listQuery = {};
export type listBody = {};
export type listResponse = {
type: `${ModelTypeEnum}`;
name: string;
avatar: string | undefined;
provider: ModelProviderIdType;
model: string;
charsPointsPrice?: number;
inputPrice?: number;
outputPrice?: number;
isActive: boolean;
isCustom: boolean;
// Tag
contextToken?: number;
vision?: boolean;
toolChoice?: boolean;
}[];
async function handler(
req: ApiRequestProps<listBody, listQuery>,
res: ApiResponseType<any>
): Promise<listResponse> {
await authSystemAdmin({ req });
// Read db
return global.systemModelList.map((model) => ({
type: model.type,
provider: model.provider,
model: model.model,
name: model.name,
avatar: model.avatar,
charsPointsPrice: model.charsPointsPrice,
inputPrice: model.inputPrice,
outputPrice: model.outputPrice,
isActive: model.isActive ?? false,
isCustom: model.isCustom ?? false,
// Tag
contextToken:
'maxContext' in model ? model.maxContext : 'maxToken' in model ? model.maxToken : undefined,
vision: 'vision' in model ? model.vision : undefined,
toolChoice: 'toolChoice' in model ? model.toolChoice : undefined
}));
}
export default NextAPI(handler);

View File

@@ -0,0 +1,127 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { findModelFromAlldata, getReRankModel } from '@fastgpt/service/core/ai/model';
import {
EmbeddingModelItemType,
LLMModelItemType,
ReRankModelItemType,
STTModelType,
TTSModelType
} from '@fastgpt/global/core/ai/model.d';
import { getAIApi } from '@fastgpt/service/core/ai/config';
import { addLog } from '@fastgpt/service/common/system/log';
import { getVectorsByText } from '@fastgpt/service/core/ai/embedding';
import { reRankRecall } from '@fastgpt/service/core/ai/rerank';
import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions';
import { isProduction } from '@fastgpt/global/common/system/constants';
import * as fs from 'fs';
export type testQuery = { model: string };
export type testBody = {};
export type testResponse = any;
async function handler(
req: ApiRequestProps<testBody, testQuery>,
res: ApiResponseType<any>
): Promise<testResponse> {
await authSystemAdmin({ req });
const { model } = req.query;
const modelData = findModelFromAlldata(model);
if (!modelData) return Promise.reject('Model not found');
if (modelData.type === 'llm') {
return testLLMModel(modelData);
}
if (modelData.type === 'embedding') {
return testEmbeddingModel(modelData);
}
if (modelData.type === 'tts') {
return testTTSModel(modelData);
}
if (modelData.type === 'stt') {
return testSTTModel(modelData);
}
if (modelData.type === 'rerank') {
return testReRankModel(modelData);
}
return Promise.reject('Model type not supported');
}
export default NextAPI(handler);
const testLLMModel = async (model: LLMModelItemType) => {
const ai = getAIApi({});
const response = await ai.chat.completions.create(
{
model: model.model,
messages: [{ role: 'user', content: 'hi' }],
stream: false,
max_tokens: 10
},
{
...(model.requestUrl ? { path: model.requestUrl } : {}),
headers: {
...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {})
}
}
);
const responseText = response.choices?.[0]?.message?.content;
if (!responseText) {
return Promise.reject('Model response empty');
}
addLog.info(`Model test response: ${responseText}`);
};
const testEmbeddingModel = async (model: EmbeddingModelItemType) => {
return getVectorsByText({
input: 'Hi',
model
});
};
const testTTSModel = async (model: TTSModelType) => {
const ai = getAIApi();
await ai.audio.speech.create(
{
model: model.model,
voice: model.voices[0]?.value as any,
input: 'Hi',
response_format: 'mp3',
speed: 1
},
model.requestUrl && model.requestAuth
? {
path: model.requestUrl,
headers: {
Authorization: `Bearer ${model.requestAuth}`
}
}
: {}
);
};
const testSTTModel = async (model: STTModelType) => {
const path = isProduction ? '/app/data/test.mp3' : 'data/test.mp3';
const { text } = await aiTranscriptions({
model: model.model,
fileStream: fs.createReadStream(path)
});
addLog.info(`STT result: ${text}`);
};
const testReRankModel = async (model: ReRankModelItemType) => {
await reRankRecall({
model,
query: 'Hi',
documents: [{ id: '1', text: 'Hi' }]
});
};

View File

@@ -0,0 +1,64 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { findModelFromAlldata } from '@fastgpt/service/core/ai/model';
import { updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
export type updateQuery = {};
export type updateBody = {
model: string;
metadata?: Record<string, any>;
};
export type updateResponse = {};
async function handler(
req: ApiRequestProps<updateBody, updateQuery>,
res: ApiResponseType<any>
): Promise<updateResponse> {
await authSystemAdmin({ req });
let { model, metadata } = req.body;
if (!model) return Promise.reject(new Error('model is required'));
model = model.trim();
const dbModel = await MongoSystemModel.findOne({ model }).lean();
const modelData = findModelFromAlldata(model);
const metadataConcat: Record<string, any> = {
...modelData, // system config
...dbModel?.metadata, // db config
...metadata // user config
};
delete metadataConcat.avatar;
delete metadataConcat.isCustom;
// 强制赋值 model避免脏的 metadata 覆盖真实 model
metadataConcat.model = model;
metadataConcat.name = metadataConcat?.name?.trim();
// Delete null value
Object.keys(metadataConcat).forEach((key) => {
if (metadataConcat[key] === null || metadataConcat[key] === undefined) {
delete metadataConcat[key];
}
});
await MongoSystemModel.updateOne(
{ model },
{
model,
metadata: metadataConcat
},
{
upsert: true
}
);
await updatedReloadSystemModel();
return {};
}
export default NextAPI(handler);

View File

@@ -0,0 +1,75 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { loadSystemModels, updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
import { updateFastGPTConfigBuffer } from '@fastgpt/service/common/system/config/controller';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
export type updateDefaultQuery = {};
export type updateDefaultBody = {
[ModelTypeEnum.llm]?: string;
[ModelTypeEnum.embedding]?: string;
[ModelTypeEnum.tts]?: string;
[ModelTypeEnum.stt]?: string;
[ModelTypeEnum.rerank]?: string;
};
export type updateDefaultResponse = {};
async function handler(
req: ApiRequestProps<updateDefaultBody, updateDefaultQuery>,
res: ApiResponseType<any>
): Promise<updateDefaultResponse> {
await authSystemAdmin({ req });
const { llm, embedding, tts, stt, rerank } = req.body;
await mongoSessionRun(async (session) => {
await MongoSystemModel.updateMany({}, { $unset: { 'metadata.isDefault': 1 } }, { session });
if (llm) {
await MongoSystemModel.updateOne(
{ model: llm },
{ $set: { 'metadata.isDefault': true } },
{ session }
);
}
if (embedding) {
await MongoSystemModel.updateOne(
{ model: embedding },
{ $set: { 'metadata.isDefault': true } },
{ session }
);
}
if (tts) {
await MongoSystemModel.updateOne(
{ model: tts },
{ $set: { 'metadata.isDefault': true } },
{ session }
);
}
if (stt) {
await MongoSystemModel.updateOne(
{ model: stt },
{ $set: { 'metadata.isDefault': true } },
{ session }
);
}
if (rerank) {
await MongoSystemModel.updateOne(
{ model: rerank },
{ $set: { 'metadata.isDefault': true } },
{ session }
);
}
});
await updatedReloadSystemModel();
return {};
}
export default NextAPI(handler);

View File

@@ -0,0 +1,62 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { SystemModelSchemaType } from '@fastgpt/service/core/ai/type';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
export type updateWithJsonQuery = {};
export type updateWithJsonBody = {
config: string;
};
export type updateWithJsonResponse = {};
async function handler(
req: ApiRequestProps<updateWithJsonBody, updateWithJsonQuery>,
res: ApiResponseType<any>
): Promise<updateWithJsonResponse> {
await authSystemAdmin({ req });
const { config } = req.body;
const data = JSON.parse(config) as SystemModelSchemaType[];
// Check
for (const item of data) {
if (!item.model || !item.metadata || typeof item.metadata !== 'object') {
return Promise.reject('Invalid model or metadata');
}
if (!item.metadata.type) {
return Promise.reject(`${item.model} metadata.type is required`);
}
if (!item.metadata.model) {
return Promise.reject(`${item.model} metadata.model is required`);
}
if (!item.metadata.provider) {
return Promise.reject(`${item.model} metadata.provider is required`);
}
item.metadata.model = item.model.trim();
if (!item.metadata.name) {
item.metadata.name = item.model;
}
}
await mongoSessionRun(async (session) => {
await MongoSystemModel.deleteMany({}, { session });
for await (const item of data) {
await MongoSystemModel.updateOne(
{ model: item.model },
{ $set: { model: item.model, metadata: item.metadata } },
{ upsert: true, session }
);
}
});
await updatedReloadSystemModel();
return {};
}
export default NextAPI(handler);

View File

@@ -6,7 +6,7 @@ import { text2Speech } from '@fastgpt/service/core/ai/audio/speech';
import { pushAudioSpeechUsage } from '@/service/support/wallet/usage/push';
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 { getTTSModel } from '@fastgpt/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
import { ApiRequestProps } from '@fastgpt/service/type/next';
@@ -31,17 +31,19 @@ async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiRes
...req.body
});
const ttsModel = getAudioSpeechModel(ttsConfig.model);
const ttsModel = getTTSModel(ttsConfig.model);
const voiceData = ttsModel.voices?.find((item) => item.value === ttsConfig.voice);
if (!voiceData) {
throw new Error('voice not found');
}
const bufferId = `${ttsModel.model}-${ttsConfig.voice}`;
/* get audio from buffer */
const ttsBuffer = await MongoTTSBuffer.findOne(
{
bufferId: voiceData.bufferId,
bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed })
},
'buffer'
@@ -70,11 +72,21 @@ async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiRes
});
/* create buffer */
await MongoTTSBuffer.create({
bufferId: voiceData.bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed }),
buffer
});
await MongoTTSBuffer.create(
{
bufferId,
text: JSON.stringify({ text: input, speed: ttsConfig.speed }),
buffer
},
ttsModel.requestUrl && ttsModel.requestAuth
? {
path: ttsModel.requestUrl,
headers: {
Authorization: `Bearer ${ttsModel.requestAuth}`
}
}
: {}
);
} catch (error) {}
},
onError: (err) => {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest } from 'next';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { getEmbeddingModel } from '@fastgpt/service/core/ai/model';
import type { DatasetSimpleItemType } from '@fastgpt/global/core/dataset/type.d';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -31,7 +31,7 @@ async function handler(req: NextApiRequest): Promise<DatasetSimpleItemType[]> {
_id: item._id,
avatar: item.avatar,
name: item.name,
vectorModel: getVectorModel(item.vectorModel)
vectorModel: getEmbeddingModel(item.vectorModel)
}));
}

View File

@@ -2,7 +2,12 @@ import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import type { CreateDatasetParams } from '@/global/core/dataset/api.d';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { getLLMModel, getVectorModel, getDatasetModel } from '@fastgpt/service/core/ai/model';
import {
getLLMModel,
getEmbeddingModel,
getDatasetModel,
getDefaultEmbeddingModel
} from '@fastgpt/service/core/ai/model';
import { checkTeamDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { NextAPI } from '@/service/middleware/entry';
@@ -27,7 +32,7 @@ async function handler(
intro,
type = DatasetTypeEnum.dataset,
avatar,
vectorModel = global.vectorModels[0].model,
vectorModel = getDefaultEmbeddingModel().model,
agentModel = getDatasetModel().model,
apiServer,
feishuServer,
@@ -56,7 +61,7 @@ async function handler(
]);
// check model valid
const vectorModelStore = getVectorModel(vectorModel);
const vectorModelStore = getEmbeddingModel(vectorModel);
const agentModelStore = getLLMModel(agentModel);
if (!vectorModelStore || !agentModelStore) {
return Promise.reject(DatasetErrEnum.invalidVectorModelOrQAModel);

View File

@@ -0,0 +1,34 @@
import type { NextApiRequest } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authDatasetData } from '@fastgpt/service/support/permission/dataset/auth';
import { CollectionWithDatasetType } from '@fastgpt/global/core/dataset/type';
export type GetQuoteDataResponse = {
collection: CollectionWithDatasetType;
q: string;
a: string;
};
async function handler(req: NextApiRequest): Promise<GetQuoteDataResponse> {
const { id: dataId } = req.query as {
id: string;
};
// 凭证校验
const { datasetData, collection } = await authDatasetData({
req,
authToken: true,
authApiKey: true,
dataId,
per: ReadPermissionVal
});
return {
collection,
q: datasetData.q,
a: datasetData.a
};
}
export default NextAPI(handler);

View File

@@ -4,7 +4,7 @@
*/
import type { NextApiRequest } from 'next';
import { countPromptTokens } from '@fastgpt/service/common/string/tiktoken/index';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { getEmbeddingModel } from '@fastgpt/service/core/ai/model';
import { hasSameValue } from '@/service/core/dataset/data/utils';
import { insertData2Dataset } from '@/service/core/dataset/data/controller';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
@@ -59,7 +59,7 @@ async function handler(req: NextApiRequest) {
// token check
const token = await countPromptTokens(formatQ + formatA, '');
const vectorModelData = getVectorModel(vectorModel);
const vectorModelData = getEmbeddingModel(vectorModel);
if (token > vectorModelData.maxToken) {
return Promise.reject('Q Over Tokens');

View File

@@ -1,4 +1,4 @@
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { getLLMModel, getEmbeddingModel } from '@fastgpt/service/core/ai/model';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { NextAPI } from '@/service/middleware/entry';
@@ -50,7 +50,7 @@ async function handler(req: ApiRequestProps<Query>): Promise<DatasetItemType> {
}
: undefined,
permission,
vectorModel: getVectorModel(dataset.vectorModel),
vectorModel: getEmbeddingModel(dataset.vectorModel),
agentModel: getLLMModel(dataset.agentModel)
};
}

View File

@@ -18,7 +18,7 @@ import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGrou
import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { getEmbeddingModel } from '@fastgpt/service/core/ai/model';
export type GetDatasetListBody = {
parentId: ParentIdType;
@@ -172,7 +172,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
name: dataset.name,
intro: dataset.intro,
type: dataset.type,
vectorModel: getVectorModel(dataset.vectorModel),
vectorModel: getEmbeddingModel(dataset.vectorModel),
inheritPermission: dataset.inheritPermission,
tmbId: dataset.tmbId,
updateTime: dataset.updateTime,

View File

@@ -14,7 +14,7 @@ import {
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
async function handler(req: NextApiRequest) {
const {
@@ -100,4 +100,4 @@ async function handler(req: NextApiRequest) {
};
}
export default NextAPI(useReqFrequencyLimit(1, 15), handler);
export default NextAPI(useIPFrequencyLimit({ id: 'search-test', seconds: 1, limit: 15 }), handler);

View File

@@ -6,7 +6,7 @@ import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema';
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 { getLLMModel, getEmbeddingModel } from '@fastgpt/service/core/ai/model';
import { TrainingModeEnum } from '@fastgpt/global/core/dataset/constants';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -49,7 +49,7 @@ async function handler(req: ApiRequestProps<rebuildEmbeddingBody>): Promise<Resp
tmbId,
appName: '切换索引模型',
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel)?.name,
vectorModel: getEmbeddingModel(dataset.vectorModel)?.name,
agentModel: getLLMModel(dataset.agentModel)?.name
});

View File

@@ -18,7 +18,10 @@ async function handler(
// send to pro
const { token } = req.query;
const result = await POST<any>(`support/outLink/dingtalk/${token}`, req.body, {
headers: req.headers as any
headers: {
timestamp: (req.headers.timestamp as string) ?? '',
sign: (req.headers.sign as string) ?? ''
}
});
return result;

View File

@@ -5,7 +5,7 @@ import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { PostLoginProps } from '@fastgpt/global/support/user/api.d';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { NextAPI } from '@/service/middleware/entry';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
@@ -70,4 +70,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
};
}
export default NextAPI(useReqFrequencyLimit(120, 10, true), handler);
export default NextAPI(
useIPFrequencyLimit({ id: 'login-by-password', seconds: 120, limit: 10, force: true }),
handler
);

View File

@@ -4,14 +4,16 @@ import { getTeamPlanStatus } from '@fastgpt/service/support/wallet/sub/utils';
import { NextAPI } from '@/service/middleware/entry';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { teamId } = await authCert({
req,
authToken: true
});
try {
const { teamId } = await authCert({
req,
authToken: true
});
return getTeamPlanStatus({
teamId
});
return getTeamPlanStatus({
teamId
});
} catch (error) {}
}
export default NextAPI(handler);

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { CreateTrainingUsageProps } from '@fastgpt/global/support/wallet/usage/api.d';
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { getLLMModel, getEmbeddingModel } from '@fastgpt/service/core/ai/model';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -23,7 +23,7 @@ async function handler(req: NextApiRequest) {
tmbId,
appName: name,
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel).name,
vectorModel: getEmbeddingModel(dataset.vectorModel).name,
agentModel: getLLMModel(dataset.agentModel).name
});

View File

@@ -8,7 +8,8 @@ import { authChatCrud } from '@/service/support/permission/auth/chat';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { NextAPI } from '@/service/middleware/entry';
import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { getDefaultSTTModel } from '@fastgpt/service/core/ai/model';
const upload = getUploadModel({
maxSize: 5
@@ -36,7 +37,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
filePaths = [file.path];
if (!global.whisperModel) {
if (!getDefaultSTTModel()) {
throw new Error('whisper model not found');
}
@@ -65,7 +66,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
// }
const result = await aiTranscriptions({
model: global.whisperModel.model,
model: getDefaultSTTModel().model,
fileStream: fs.createReadStream(file.path)
});
@@ -89,7 +90,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
removeFilesByPaths(filePaths);
}
export default NextAPI(useReqFrequencyLimit(1, 1), handler);
export default NextAPI(
useIPFrequencyLimit({ id: 'transcriptions', seconds: 1, limit: 1 }),
handler
);
export const config = {
api: {

View File

@@ -4,7 +4,7 @@ import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
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 { getEmbeddingModel } 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';
@@ -36,7 +36,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { tokens, vectors } = await getVectorsByText({
input: query,
model: getVectorModel(model),
model: getEmbeddingModel(model),
type
});

View File

@@ -1,267 +0,0 @@
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { AppContext } from '@/pages/app/detail/components/context';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useI18n } from '@/web/context/I18n';
import { resumeInheritPer } from '@/web/core/app/api';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import {
Box,
Button,
Flex,
FormControl,
Input,
ModalBody,
ModalFooter,
Textarea
} from '@chakra-ui/react';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { AppSchema } from '@fastgpt/global/core/app/type.d';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { commonT } = useI18n();
const { toast } = useToast();
const { updateAppDetail, appDetail, reloadApp } = useContextSelector(AppContext, (v) => v);
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const {
register,
setValue,
watch,
formState: { errors },
handleSubmit
} = useForm({
defaultValues: appDetail
});
const avatar = watch('avatar');
// submit config
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(
async (data: AppSchema) => {
await updateAppDetail({
name: data.name,
avatar: data.avatar,
intro: data.intro
});
},
{
onSuccess() {
toast({
title: t('common:common.Update Success'),
status: 'success'
});
reloadApp();
},
errorToast: t('common:common.Update Failed')
}
);
const saveSubmitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return t('common:common.Submit failed');
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [errors, t, toast]);
const saveUpdateModel = useCallback(
() => handleSubmit((data) => saveSubmitSuccess(data).then(onClose), saveSubmitError)(),
[handleSubmit, onClose, saveSubmitError, saveSubmitSuccess]
);
const onUpdateCollaborators = ({
members,
groups,
orgs,
permission
}: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: PermissionValueType;
}) =>
postUpdateAppCollaborators({
members,
groups,
permission,
orgs,
appId: appDetail._id
});
const onDelCollaborator = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) =>
deleteAppCollaborators({
appId: appDetail._id,
...props
});
const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), {
errorToast: t('common:resume_failed'),
onSuccess: () => {
reloadApp();
}
});
return (
<MyModal
isOpen={true}
onClose={onClose}
iconSrc="/imgs/workflow/ai.svg"
title={t('common:core.app.setting')}
>
<ModalBody>
<Box fontSize={'sm'}>{t('common:core.app.Name and avatar')}</Box>
<Flex mt={2} alignItems={'center'}>
<Avatar
src={avatar}
w={['26px', '34px']}
h={['26px', '34px']}
cursor={'pointer'}
borderRadius={'md'}
mr={4}
title={t('common:common.Set Avatar')}
onClick={() => onOpenSelectFile()}
/>
<FormControl>
<Input
bg={'myWhite.600'}
placeholder={t('common:core.app.Set a name for your app')}
{...register('name', {
required: true
})}
></Input>
</FormControl>
</Flex>
<Box mt={4} mb={1} fontSize={'sm'}>
{t('common:core.app.App intro')}
</Box>
<Textarea
rows={4}
maxLength={500}
placeholder={t('common:core.app.Make a brief introduction of your app')}
bg={'myWhite.600'}
{...register('intro')}
/>
{/* role */}
{appDetail.permission.hasManagePer && (
<>
{!appDetail.inheritPermission && appDetail.parentId && (
<Box mt={3}>
<ResumeInherit onResume={resumeInheritPermission} />
</Box>
)}
<Box mt={6}>
<CollaboratorContextProvider
permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}
onUpdateCollaborators={async (props) =>
onUpdateCollaborators({
permission: props.permission,
members: props.members,
groups: props.groups,
orgs: props.orgs
})
}
onDelOneCollaborator={onDelCollaborator}
refreshDeps={[appDetail.inheritPermission]}
isInheritPermission={appDetail.inheritPermission}
hasParent={!!appDetail.parentId}
>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex
alignItems="center"
flexDirection="row"
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}>{commonT('permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('common:permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common:common.Add')}
</Button>
</Flex>
</Flex>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</>
);
}}
</CollaboratorContextProvider>
</Box>
</>
)}
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={btnLoading} onClick={saveUpdateModel}>
{t('common:common.Save')}
</Button>
</ModalFooter>
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
};
export default React.memo(InfoModal);

View File

@@ -1,200 +0,0 @@
import React, { useMemo } from 'react';
import { Flex, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants';
import { getInitChatInfo } from '@/web/core/chat/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { PcHeader } from '@/pages/chat/components/ChatHeader';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, {
ChatRecordContext
} from '@/web/core/chat/context/chatRecordContext';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
type Props = {
appId: string;
chatId: string;
onClose: () => void;
};
const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const { data: chat, loading: isFetching } = useRequest2(
async () => {
const res = await getInitChatInfo({ appId, chatId, loadCustomFeedbacks: true });
res.userAvatar = HUMAN_ICON;
setChatBoxData(res);
resetVariables({
variables: res.variables,
variableList: res.app?.chatConfig?.variables
});
return res;
},
{
manual: false,
refreshDeps: [chatId],
onError(e) {
onClose();
}
}
);
const title = chat?.title;
const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.plugin;
return (
<>
<MyBox
isLoading={isFetching}
display={'flex'}
flexDirection={'column'}
zIndex={3}
position={['fixed', 'absolute']}
top={[0, '2%']}
right={0}
h={['100%', '96%']}
w={'100%'}
maxW={['100%', '600px']}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
{/* Header */}
{isPlugin ? (
<Flex
alignItems={'flex-start'}
justifyContent={'space-between'}
px={3}
pt={3}
bg={'myGray.25'}
borderBottom={'base'}
>
<LightRowTabs<PluginRunBoxTabEnum>
list={[
{ label: t('common:common.Input'), value: PluginRunBoxTabEnum.input },
...(chatRecords.length > 0
? [
{ label: t('common:common.Output'), value: PluginRunBoxTabEnum.output },
{ label: t('common:common.all_result'), value: PluginRunBoxTabEnum.detail }
]
: [])
]}
value={pluginRunTab}
onChange={setPluginRunTab}
inlineStyles={{ px: 0.5, pb: 2 }}
gap={5}
py={0}
fontSize={'sm'}
/>
<CloseIcon onClick={onClose} />
</Flex>
) : (
<Flex
alignItems={'center'}
px={[3, 5]}
h={['46px', '60px']}
borderBottom={'base'}
borderBottomColor={'gray.200'}
color={'myGray.900'}
>
{isPc ? (
<>
<PcHeader
totalRecordsCount={totalRecordsCount}
title={title || ''}
chatModels={chatModels}
/>
<Box flex={1} />
</>
) : (
<>
<Flex px={3} alignItems={'center'} flex={'1 0 0'} w={0} justifyContent={'center'}>
<Box ml={1} className="textEllipsis">
{title}
</Box>
</Flex>
</>
)}
<CloseIcon onClick={onClose} />
</Flex>
)}
{/* Chat container */}
<Box pt={2} flex={'1 0 0'} h={0}>
{isPlugin ? (
<Box h={'100%'} overflow={'auto'}>
<Box px={5} py={2}>
<PluginRunBox appId={appId} chatId={chatId} />
</Box>
</Box>
) : (
<ChatBox
isReady
appId={appId}
chatId={chatId}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatType="log"
/>
)}
</Box>
</MyBox>
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
</>
);
};
const Render = (props: Props) => {
const { appId, chatId } = props;
const params = useMemo(() => {
return {
chatId,
appId,
loadCustomFeedbacks: true,
type: GetChatTypeEnum.normal
};
}, [appId, chatId]);
return (
<ChatItemContextProvider
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={params}>
<DetailLogsModal {...props} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
};
export default Render;

View File

@@ -1,226 +0,0 @@
import React, { useState } from 'react';
import {
Flex,
Box,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
ModalBody,
HStack
} from '@chakra-ui/react';
import UserBox from '@fastgpt/web/components/common/UserBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { getAppChatLogs } from '@/web/core/app/api';
import dayjs from 'dayjs';
import { ChatSourceMap } from '@fastgpt/global/core/chat/constants';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { addDays } from 'date-fns';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
import dynamic from 'next/dynamic';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
const Logs = () => {
const { t } = useTranslation();
const { isPc } = useSystem();
const appId = useContextSelector(AppContext, (v) => v.appId);
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const {
isOpen: isOpenMarkDesc,
onOpen: onOpenMarkDesc,
onClose: onCloseMarkDesc
} = useDisclosure();
const {
data: logs,
isLoading,
Pagination,
getData,
pageNum
} = usePagination(getAppChatLogs, {
pageSize: 20,
params: {
appId,
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1)
}
});
const [detailLogsId, setDetailLogsId] = useState<string>();
return (
<Flex flexDirection={'column'} h={'100%'}>
{isPc && (
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
{t('app:chat_logs')}
</Box>
<Box color={'myGray.500'} fontSize={'sm'}>
{t('app:chat_logs_tips')},{' '}
<Box
as={'span'}
mr={2}
textDecoration={'underline'}
cursor={'pointer'}
onClick={onOpenMarkDesc}
>
{t('common:core.chat.Read Mark Description')}
</Box>
</Box>
</Box>
)}
{/* table */}
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={[0, 4]}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
>
<TableContainer mt={[0, 3]} flex={'1 0 0'} h={0} overflowY={'auto'}>
<Table variant={'simple'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:core.app.logs.Source And Time')}</Th>
<Th>{t('app:logs_chat_user')}</Th>
<Th>{t('app:logs_title')}</Th>
<Th>{t('app:logs_message_total')}</Th>
<Th>{t('app:feedback_count')}</Th>
<Th>{t('common:core.app.feedback.Custom feedback')}</Th>
<Th>{t('app:mark_count')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'xs'}>
{logs.map((item) => (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={t('common:core.view_chat_detail')}
onClick={() => setDetailLogsId(item.id)}
>
<Td>
{/* @ts-ignore */}
<Box>{t(ChatSourceMap[item.source]?.name) || item.source}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td>
<Box>
{!!item.outLinkUid ? (
item.outLinkUid
) : (
<UserBox sourceMember={item.sourceMember} />
)}
</Box>
</Td>
<Td className="textEllipsis" maxW={'250px'}>
{item.title}
</Td>
<Td>{item.messageCount}</Td>
<Td w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/goodLight'}
color={'green.600'}
w={'14px'}
/>
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/badLight'}
color={'#C96330'}
w={'14px'}
/>
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
</Tr>
))}
</Tbody>
</Table>
{logs.length === 0 && !isLoading && <EmptyTip text={t('app:logs_empty')}></EmptyTip>}
</TableContainer>
<HStack w={'100%'} mt={3} justifyContent={'flex-end'}>
<DateRangePicker
defaultDate={dateRange}
position="top"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</HStack>
</Flex>
{!!detailLogsId && (
<DetailLogsModal
appId={appId}
chatId={detailLogsId}
onClose={() => {
setDetailLogsId(undefined);
getData(pageNum);
}}
/>
)}
<MyModal
isOpen={isOpenMarkDesc}
onClose={onCloseMarkDesc}
title={t('common:core.chat.Mark Description Title')}
>
<ModalBody whiteSpace={'pre-wrap'}>{t('common:core.chat.Mark Description')}</ModalBody>
</MyModal>
</Flex>
);
};
export default React.memo(Logs);

View File

@@ -1,273 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
ModalBody,
ModalFooter,
useDisclosure
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext, WorkflowSnapshotsType } from '../WorkflowComponents/context';
import { AppContext, TabEnum } from '../context';
import RouteTab from '../RouteTab';
import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from '../Workflow/components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
import { WorkflowStatusContext } from '../WorkflowComponents/context/workflowStatusContext';
const Header = () => {
const { t } = useTranslation();
const { isPc } = useSystem();
const router = useRouter();
const { toast } = useToast();
const { appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const {
isOpen: isOpenBackConfirm,
onOpen: onOpenBackConfirm,
onClose: onCloseBackConfirm
} = useDisclosure();
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
const flowData2StoreDataAndCheck = useContextSelector(
WorkflowContext,
(v) => v.flowData2StoreDataAndCheck
);
const setWorkflowTestData = useContextSelector(WorkflowContext, (v) => v.setWorkflowTestData);
const past = useContextSelector(WorkflowContext, (v) => v.past);
const setPast = useContextSelector(WorkflowContext, (v) => v.setPast);
const onSwitchTmpVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchTmpVersion);
const onSwitchCloudVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchCloudVersion);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const isSaved = useContextSelector(WorkflowStatusContext, (v) => v.isSaved);
const leaveSaveSign = useContextSelector(WorkflowStatusContext, (v) => v.leaveSaveSign);
const { lastAppListRouteType } = useSystemStore();
const { runAsync: onClickSave, loading } = useRequest2(
async ({
isPublish,
versionName = formatTime2YMDHMS(new Date())
}: {
isPublish?: boolean;
versionName?: string;
}) => {
const data = flowData2StoreData();
if (data) {
await onSaveApp({
...data,
isPublish,
versionName,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
// Mark the current snapshot as saved
setPast((prevPast) =>
prevPast.map((item, index) =>
index === 0
? {
...item,
isSaved: true
}
: item
)
);
}
}
);
const onBack = useCallback(async () => {
leaveSaveSign.current = false;
router.push({
pathname: '/app/list',
query: {
parentId: appDetail.parentId,
type: lastAppListRouteType
}
});
}, [appDetail.parentId, lastAppListRouteType, leaveSaveSign, router]);
const Render = useMemo(() => {
return (
<>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex
mt={[2, 0]}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
alignItems={['flex-start', 'center']}
userSelect={'none'}
h={['auto', '67px']}
flexWrap={'wrap'}
{...(currentTab === TabEnum.appEdit
? {
bg: 'myGray.25'
}
: {
bg: 'transparent',
borderBottomColor: 'transparent'
})}
>
{/* back */}
<Box
_hover={{
bg: 'myGray.200'
}}
p={0.5}
borderRadius={'sm'}
>
<MyIcon
name={'common/leftArrowLight'}
w={6}
cursor={'pointer'}
onClick={isSaved ? onBack : onOpenBackConfirm}
/>
</Box>
{/* app info */}
<Box ml={1}>
<AppCard isSaved={isSaved} showSaveStatus={isV2Workflow} />
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
<Box flex={1} />
{currentTab === TabEnum.appEdit && (
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
{!showHistoryModal && (
<IconButton
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => {
setShowHistoryModal(true);
}}
/>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('common:core.workflow.Run')}
</Button>
{!showHistoryModal && (
<SaveButton
isLoading={loading}
onClickSave={onClickSave}
checkData={flowData2StoreDataAndCheck}
/>
)}
</HStack>
)}
</Flex>
</>
);
}, [
isPc,
currentTab,
isSaved,
onBack,
onOpenBackConfirm,
isV2Workflow,
showHistoryModal,
t,
loading,
onClickSave,
flowData2StoreDataAndCheck,
setShowHistoryModal,
setWorkflowTestData
]);
return (
<>
{Render}
{showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && (
<PublishHistories<WorkflowSnapshotsType>
onClose={() => {
setShowHistoryModal(false);
}}
past={past}
onSwitchTmpVersion={onSwitchTmpVersion}
onSwitchCloudVersion={onSwitchCloudVersion}
/>
)}
<MyModal
isOpen={isOpenBackConfirm}
onClose={onCloseBackConfirm}
iconSrc="common/warn"
title={t('common:common.Exit')}
w={'400px'}
>
<ModalBody>
<Box>{t('workflow:workflow.exit_tips')}</Box>
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteDanger'} onClick={onBack}>
{t('common:common.Exit Directly')}
</Button>
<Button
isLoading={loading}
onClick={async () => {
try {
await onClickSave({});
onCloseBackConfirm();
onBack();
toast({
status: 'success',
title: t('app:saved_success'),
position: 'top-right'
});
} catch (error) {}
}}
>
{t('common:common.Save_and_exit')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default React.memo(Header);

View File

@@ -1,75 +0,0 @@
import React from 'react';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { ReactFlowCustomProvider, WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import Flow from '../WorkflowComponents/Flow';
import { useTranslation } from 'next-i18next';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const { appDetail, currentTab } = useContextSelector(AppContext, (e) => e);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content: t('common:info.old_version_attention')
});
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))));
})();
} else {
initData(
cloneDeep({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
}),
true
);
}
});
return (
<Flex {...workflowBoxStyles}>
<Header />
{currentTab === TabEnum.appEdit ? (
<Flow />
) : (
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
{!isV2Workflow && <ConfirmModal countDown={0} />}
</Flex>
);
};
const Render = () => {
return (
<ReactFlowCustomProvider templates={pluginSystemModuleTemplates}>
<WorkflowEdit />
</ReactFlowCustomProvider>
);
};
export default Render;

View File

@@ -1,12 +0,0 @@
import React, { useEffect, useState } from 'react';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import { useI18n } from '@/web/context/I18n';
const API = ({ appId }: { appId: string }) => {
const { publishT } = useI18n();
return <ApiKeyTable tips={publishT('app_key_tips')} appId={appId} />;
};
export default API;

View File

@@ -1,152 +0,0 @@
import React from 'react';
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { DingtalkAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import BasicInfo from '../components/BasicInfo';
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const DingTalkEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<DingtalkAppType>;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
isEdit?: boolean;
}) => {
const { t } = useTranslation();
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { runAsync: onclickCreate, loading: creating } = useRequest2(
(e: Omit<OutLinkEditType<DingtalkAppType>, 'appId' | 'type'>) =>
createShareChat({
...e,
appId,
type: PublishChannelEnum.dingtalk,
app: {
clientId: e?.app?.clientId?.trim(),
clientSecret: e.app?.clientSecret?.trim()
}
}),
{
errorToast: t('common:common.Create Failed'),
successToast: t('common:common.Create Success'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(
(e) =>
updateShareChat({
...e,
app: {
clientId: e?.app?.clientId?.trim(),
clientSecret: e.app?.clientSecret?.trim()
}
}),
{
errorToast: t('common:common.Update Failed'),
successToast: t('common:common.Update Success'),
onSuccess: onEdit
}
);
const { feConfigs } = useSystemStore();
return (
<MyModal
iconSrc="common/dingtalkFill"
title={
isEdit ? t('publish:dingtalk.edit_modal_title') : t('publish:dingtalk.create_modal_title')
}
minW={['auto', '60rem']}
>
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
<Box p={8} h={['auto', '400px']} borderRight={'base'}>
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
</Box>
<Flex p={8} h={['auto', '400px']} flexDirection="column" gap={6}>
<Flex alignItems="center">
<Box color="myGray.600">{t('publish:dingtalk.api')}</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/dingtalk/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon w={'17px'} h={'17px'} name="book" mr="1" />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Client ID
</FormLabel>
<Input
placeholder={'Client ID'}
{...register('app.clientId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Client Secret
</FormLabel>
<Input
placeholder={'Client Secret'}
{...register('app.clientSecret', {
required: true
})}
/>
</Flex>
<Box flex={1}></Box>
<Flex justifyContent={'end'}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) =>
isEdit ? onclickUpdate(data) : onclickCreate(data)
)}
>
{t('common:common.Confirm')}
</Button>
</Flex>
</Flex>
</ModalBody>
</MyModal>
);
};
export default DingTalkEditModal;

View File

@@ -1,248 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
Link,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { defaultDingtalkOutlinkForm } from '@/web/core/app/constants';
import type { DingtalkAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getDocPath } from '@/web/common/system/doc';
const DingTalkEditModal = dynamic(() => import('./DingTalkEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const DingTalk = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const [editDingTalkLinkData, setEditDingTalkLinkData] =
useState<OutLinkEditType<DingtalkAppType>>();
const [isEdit, setIsEdit] = useState<boolean>(false);
const baseUrl = useMemo(
() => feConfigs?.customApiDomain || `${location.origin}/api`,
[feConfigs?.customApiDomain]
);
const {
data: shareChatList = [],
loading: isFetching,
runAsync: refetchShareChatList
} = useRequest2(
() => getShareChatList<DingtalkAppType>({ appId, type: PublishChannelEnum.dingtalk }),
{
manual: false
}
);
const {
onOpen: openShowShareLinkModal,
isOpen: showShareLinkModalOpen,
onClose: closeShowShareLinkModal
} = useDisclosure();
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('publish:dingtalk.title')}
</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/dingtalk/')
}
target={'_blank'}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon name="book" mr="1" w={'1rem'} />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</HStack>
<Button
variant={'primary'}
colorScheme={'blue'}
size={['sm', 'md']}
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
ml={3}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: t('common:core.app.share.Amount limit tip')
}
: {})}
onClick={() => {
setEditDingTalkLinkData(defaultDingtalkOutlinkForm);
setIsEdit(false);
}}
>
{t('common:add_new')}
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:common.Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:common.Expired Time')}</Th>
</>
)}
<Th>{t('common:common.Last use time')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
: ` / ${t('common:common.Unlimited')}`
}`
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => {
setShowShareLink(`${baseUrl}/support/outLink/dingtalk/${item.shareId}`);
openShowShareLinkModal();
}}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('publish:request_address')}
</Button>
<MyMenu
Button={
<MyIcon
name={'more'}
_hover={{ bg: 'myGray.100' }}
cursor={'pointer'}
borderRadius={'md'}
w={'14px'}
p={2}
/>
}
menuList={[
{
children: [
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () => {
setEditDingTalkLinkData({
_id: item._id,
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});
setIsEdit(true);
}
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}
}
]
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{editDingTalkLinkData && (
<DingTalkEditModal
appId={appId}
defaultData={editDingTalkLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditDingTalkLinkData(undefined)])}
onClose={() => setEditDingTalkLinkData(undefined)}
isEdit={isEdit}
/>
)}
{shareChatList.length === 0 && !isFetching && (
<EmptyTip text={t('common:core.app.share.Not share link')}></EmptyTip>
)}
<Loading loading={isFetching} fixed={false} />
{showShareLinkModalOpen && (
<ShowShareLinkModal
shareLink={showShareLink ?? ''}
onClose={closeShowShareLinkModal}
img="/imgs/outlink/dingtalk-copylink-instruction.png"
/>
)}
</Box>
);
};
export default React.memo(DingTalk);

View File

@@ -1,157 +0,0 @@
import React from 'react';
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import BasicInfo from '../components/BasicInfo';
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const FeiShuEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<FeishuAppType>;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
isEdit?: boolean;
}) => {
const { t } = useTranslation();
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { runAsync: onclickCreate, loading: creating } = useRequest2(
(e: Omit<OutLinkEditType<FeishuAppType>, 'appId' | 'type'>) =>
createShareChat({
...e,
appId,
type: PublishChannelEnum.feishu,
app: {
appId: e?.app?.appId?.trim(),
appSecret: e.app?.appSecret?.trim(),
encryptKey: e.app?.encryptKey?.trim()
}
}),
{
errorToast: t('common:common.Create Failed'),
successToast: t('common:common.Create Success'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(
(e) =>
updateShareChat({
...e,
app: {
appId: e?.app?.appId?.trim(),
appSecret: e.app?.appSecret?.trim(),
encryptKey: e.app?.encryptKey?.trim()
}
}),
{
errorToast: t('common:common.Update Failed'),
successToast: t('common:common.Update Success'),
onSuccess: onEdit
}
);
const { feConfigs } = useSystemStore();
return (
<MyModal
iconSrc="core/app/publish/lark"
title={isEdit ? t('publish:edit_feishu_bot') : t('publish:new_feishu_bot')}
minW={['auto', '60rem']}
>
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
<Box p={8} h={['auto', '400px']} borderRight={'base'}>
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
</Box>
<Flex p={8} h={['auto', '400px']} flexDirection="column" gap={6}>
<Flex alignItems="center">
<Box color="myGray.600">{t('publish:feishu_api')}</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/feishu/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon w={'17px'} h={'17px'} name="book" mr="1" />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
App ID
</FormLabel>
<Input
placeholder={t('common:core.module.http.AppId')}
{...register('app.appId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
App Secret
</FormLabel>
<Input
placeholder={'App Secret'}
{...register('app.appSecret', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'}>Encrypt Key</FormLabel>
<Input placeholder="Encrypt Key" {...register('app.encryptKey')} />
</Flex>
<Box flex={1}></Box>
<Flex justifyContent={'end'}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) =>
isEdit ? onclickUpdate(data) : onclickCreate(data)
)}
>
{t('common:common.Confirm')}
</Button>
</Flex>
</Flex>
</ModalBody>
</MyModal>
);
};
export default FeiShuEditModal;

View File

@@ -1,247 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
Link,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { defaultFeishuOutLinkForm } from '@/web/core/app/constants';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getDocPath } from '@/web/common/system/doc';
const FeiShuEditModal = dynamic(() => import('./FeiShuEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const FeiShu = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const [editFeiShuLinkData, setEditFeiShuLinkData] = useState<OutLinkEditType<FeishuAppType>>();
const [isEdit, setIsEdit] = useState<boolean>(false);
const baseUrl = useMemo(
() => feConfigs?.customApiDomain || `${location.origin}/api`,
[feConfigs?.customApiDomain]
);
const {
data: shareChatList = [],
loading: isFetching,
runAsync: refetchShareChatList
} = useRequest2(
() => getShareChatList<FeishuAppType>({ appId, type: PublishChannelEnum.feishu }),
{
manual: false
}
);
const {
onOpen: openShowShareLinkModal,
isOpen: showShareLinkModalOpen,
onClose: closeShowShareLinkModal
} = useDisclosure();
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common:core.app.publish.Fei shu bot publish')}
</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/feishu/')
}
target={'_blank'}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon name="book" mr="1" w={'1rem'} />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</HStack>
<Button
variant={'primary'}
colorScheme={'blue'}
size={['sm', 'md']}
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
ml={3}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: t('common:core.app.share.Amount limit tip')
}
: {})}
onClick={() => {
setEditFeiShuLinkData(defaultFeishuOutLinkForm);
setIsEdit(false);
}}
>
{t('common:add_new')}
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:common.Name')}</Th>
<Th>{t('common:support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:common.Expired Time')}</Th>
</>
)}
<Th>{t('common:common.Last use time')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
: ` / ${t('common:common.Unlimited')}`
}`
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => {
setShowShareLink(`${baseUrl}/support/outLink/feishu/${item.shareId}`);
openShowShareLinkModal();
}}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('publish:request_address')}
</Button>
<MyMenu
Button={
<MyIcon
name={'more'}
_hover={{ bg: 'myGray.100' }}
cursor={'pointer'}
borderRadius={'md'}
w={'14px'}
p={2}
/>
}
menuList={[
{
children: [
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () => {
setEditFeiShuLinkData({
_id: item._id,
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});
setIsEdit(true);
}
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}
}
]
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{editFeiShuLinkData && (
<FeiShuEditModal
appId={appId}
defaultData={editFeiShuLinkData}
onCreate={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onClose={() => setEditFeiShuLinkData(undefined)}
isEdit={isEdit}
/>
)}
{shareChatList.length === 0 && !isFetching && (
<EmptyTip text={t('common:core.app.share.Not share link')}></EmptyTip>
)}
<Loading loading={isFetching} fixed={false} />
{showShareLinkModalOpen && (
<ShowShareLinkModal
shareLink={showShareLink ?? ''}
onClose={closeShowShareLinkModal}
img="/imgs/outlink/feishu-copylink-instruction.png"
/>
)}
</Box>
);
};
export default React.memo(FeiShu);

View File

@@ -1,221 +0,0 @@
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import React, { useCallback, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, FlexProps, Grid, ModalBody, Switch, useTheme } from '@chakra-ui/react';
import MyRadio from '@/components/common/MyRadio';
import { useForm } from 'react-hook-form';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { fileToBase64 } from '@/web/common/file/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { subRoute } from '@fastgpt/web/common/system/utils';
enum UsingWayEnum {
link = 'link',
iframe = 'iframe',
script = 'script'
}
const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose: () => void }) => {
const { t } = useTranslation();
const theme = useTheme();
const { copyData } = useCopyData();
const { File, onOpen } = useSelectFile({
multiple: false,
fileType: 'image/*'
});
const { feConfigs } = useSystemStore();
const VariableTypeList = [
{
title: <MyImage src={'/imgs/outlink/link.svg'} alt={''} />,
value: UsingWayEnum.link
},
{
title: <MyImage src={'/imgs/outlink/iframe.svg'} alt={''} />,
value: UsingWayEnum.iframe
},
{
title: <MyImage src={'/imgs/outlink/script.svg'} alt={''} />,
value: UsingWayEnum.script
}
];
const [refresh, setRefresh] = useState(false);
const { getValues, setValue, register, watch } = useForm({
defaultValues: {
usingWay: UsingWayEnum.link,
showHistory: true,
scriptIconCanDrag: false,
scriptDefaultOpen: false,
scriptOpenIcon:
'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNTMyNzg1NjY0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjQxMzIiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNNTEyIDMyQzI0Ny4wNCAzMiAzMiAyMjQgMzIgNDY0QTQxMC4yNCA0MTAuMjQgMCAwIDAgMTcyLjQ4IDc2OEwxNjAgOTY1LjEyYTI1LjI4IDI1LjI4IDAgMCAwIDM5LjA0IDIyLjRsMTY4LTExMkE1MjguNjQgNTI4LjY0IDAgMCAwIDUxMiA4OTZjMjY0Ljk2IDAgNDgwLTE5MiA0ODAtNDMyUzc3Ni45NiAzMiA1MTIgMzJ6IG0yNDQuOCA0MTZsLTM2MS42IDMwMS43NmExMi40OCAxMi40OCAwIDAgMS0xOS44NC0xMi40OGw1OS4yLTIzMy45MmgtMTYwYTEyLjQ4IDEyLjQ4IDAgMCAxLTcuMzYtMjMuMzZsMzYxLjYtMzAxLjc2YTEyLjQ4IDEyLjQ4IDAgMCAxIDE5Ljg0IDEyLjQ4bC01OS4yIDIzMy45MmgxNjBhMTIuNDggMTIuNDggMCAwIDEgOCAyMi4wOHoiIGZpbGw9IiM0ZTgzZmQiIHAtaWQ9IjQxMzMiPjwvcGF0aD48L3N2Zz4=',
scriptCloseIcon:
'data:image/svg+xml;base64,PHN2ZyB0PSIxNjkwNTM1NDQxNTI2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjYzNjciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48cGF0aCBkPSJNNTEyIDEwMjRBNTEyIDUxMiAwIDEgMSA1MTIgMGE1MTIgNTEyIDAgMCAxIDAgMTAyNHpNMzA1Ljk1NjU3MSAzNzAuMzk1NDI5TDQ0Ny40ODggNTEyIDMwNS45NTY1NzEgNjUzLjYwNDU3MWE0NS41NjggNDUuNTY4IDAgMSAwIDY0LjQzODg1OCA2NC40Mzg4NThMNTEyIDU3Ni41MTJsMTQxLjYwNDU3MSAxNDEuNTMxNDI5YTQ1LjU2OCA0NS41NjggMCAwIDAgNjQuNDM4ODU4LTY0LjQzODg1OEw1NzYuNTEyIDUxMmwxNDEuNTMxNDI5LTE0MS42MDQ1NzFhNDUuNTY4IDQ1LjU2OCAwIDEgMC02NC40Mzg4NTgtNjQuNDM4ODU4TDUxMiA0NDcuNDg4IDM3MC4zOTU0MjkgMzA1Ljk1NjU3MWE0NS41NjggNDUuNTY4IDAgMCAwLTY0LjQzODg1OCA2NC40Mzg4NTh6IiBmaWxsPSIjNGU4M2ZkIiBwLWlkPSI2MzY4Ij48L3BhdGg+PC9zdmc+'
}
});
const selectFile = useCallback(
async (files: File[], key: 'scriptOpenIcon' | 'scriptCloseIcon') => {
const file = files[0];
if (!file) return;
// image to base64
const base64 = await fileToBase64(file);
setValue(key, base64);
},
[setValue]
);
watch(() => {
setRefresh(!refresh);
});
const baseUrl = feConfigs?.customSharePageDomain || location?.origin;
const linkUrl = `${baseUrl}${subRoute ? `${subRoute}/` : '/'}chat/share?shareId=${share?.shareId}${
getValues('showHistory') ? '' : '&showHistory=0'
}`;
const wayMap = {
[UsingWayEnum.link]: {
blockTitle: t('common:core.app.outLink.Link block title'),
code: linkUrl
},
[UsingWayEnum.iframe]: {
blockTitle: t('common:core.app.outLink.Iframe block title'),
code: `<iframe
src="${linkUrl}"
style="width: 100%; height: 100%;"
frameborder="0"
allow="*"
/>`
},
[UsingWayEnum.script]: {
blockTitle: t('common:core.app.outLink.Script block title'),
code: `<script
type="text/javascript"
src="${baseUrl}/js/iframe.js"
id="chatbot-iframe"
data-bot-src="${linkUrl}"
data-default-open="${getValues('scriptDefaultOpen') ? 'true' : 'false'}"
data-drag="${getValues('scriptIconCanDrag') ? 'true' : 'false'}"
data-open-icon="${getValues('scriptOpenIcon')}"
data-close-icon="${getValues('scriptCloseIcon')}"
defer
></script>`
}
};
const gridItemStyle: FlexProps = {
alignItems: 'center',
bg: 'myWhite.600',
p: 2,
borderRadius: 'md',
border: theme.borders.sm
};
return (
<MyModal
isOpen
isCentered
iconSrc="/imgs/modal/usingWay.svg"
title={t('common:core.app.outLink.Select Using Way')}
onClose={onClose}
maxW={['90vw', '700px']}
>
<ModalBody py={4}>
<MyRadio
gridGap={2}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(3,1fr)']}
value={getValues('usingWay')}
list={VariableTypeList}
hiddenCircle
p={0}
onChange={(e) => {
setValue('usingWay', e);
}}
/>
{/* config */}
<Grid
gridTemplateColumns={['repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={4}
my={5}
fontSize={'sm'}
>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Show History')}</Box>
<Switch {...register('showHistory')} />
</Flex>
{getValues('usingWay') === UsingWayEnum.script && (
<>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Can Drag')}</Box>
<Switch {...register('scriptIconCanDrag')} />
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Default open')}</Box>
<Switch {...register('scriptDefaultOpen')} />
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Open Icon')}</Box>
<MyImage
src={getValues('scriptOpenIcon')}
alt={''}
w={'20px'}
h={'20px'}
cursor={'pointer'}
onClick={() => onOpen('scriptOpenIcon')}
/>
</Flex>
<Flex {...gridItemStyle}>
<Box flex={1}>{t('common:core.app.outLink.Script Close Icon')}</Box>
<MyImage
src={getValues('scriptCloseIcon')}
alt={''}
w={'20px'}
h={'20px'}
cursor={'pointer'}
onClick={() => onOpen('scriptCloseIcon')}
/>
</Flex>
</>
)}
</Grid>
{/* code */}
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
<Flex
p={3}
bg={'myWhite.500'}
border={theme.borders.base}
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
>
<Box flex={1}>{wayMap[getValues('usingWay')].blockTitle}</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => {
copyData(wayMap[getValues('usingWay')].code);
}}
/>
</Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
{wayMap[getValues('usingWay')].code}
</Box>
</Box>
</ModalBody>
<File onSelect={selectFile} />
</MyModal>
);
};
export default SelectUsingWayModal;

View File

@@ -1,463 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
ModalFooter,
ModalBody,
Input,
Switch,
Link,
IconButton,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';
import {
getShareChatList,
delShareChatById,
createShareChat,
putShareChat
} from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useForm } from 'react-hook-form';
import { defaultOutLinkForm } from '@/web/core/app/constants';
import type { OutLinkEditType, OutLinkSchema } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyModal from '@fastgpt/web/components/common/MyModal';
import dayjs from 'dayjs';
import { getDocPath } from '@/web/common/system/doc';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const SelectUsingWayModal = dynamic(() => import('./SelectUsingWayModal'));
const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const { copyData } = useCopyData();
const [editLinkData, setEditLinkData] = useState<OutLinkEditType>();
const [selectedLinkData, setSelectedLinkData] = useState<OutLinkSchema>();
const { toast } = useToast();
const { ConfirmModal, openConfirm } = useConfirm({
content: t('common:support.outlink.Delete link tip'),
type: 'delete'
});
const {
isFetching,
data: shareChatList = [],
refetch: refetchShareChatList
} = useQuery(['initShareChatList', appId], () =>
getShareChatList({ appId, type: PublishChannelEnum.share })
);
return (
<MyBox h={'100%'} isLoading={isFetching} position={'relative'}>
<Flex justifyContent={'space-between'}>
<HStack>
<Box color={'myGray.900'} fontSize={'lg'}>
{t('common:core.app.Share link')}
</Box>
<QuestionTip label={t('common:core.app.Share link desc detail')} />
</HStack>
<Button
variant={'whitePrimary'}
colorScheme={'blue'}
size={['sm', 'md']}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: t('common:core.app.share.Amount limit tip')
}
: {})}
onClick={() => setEditLinkData(defaultOutLinkForm)}
>
{t('common:core.app.share.Create link')}
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:common.Name')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:common.Expired Time')}</Th>
</>
)}
<Th>{t('common:support.outlink.Usage points')}</Th>
<Th>{t('common:core.app.share.Is response quote')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')}</Th>
<Th>{t('common:core.app.share.Role check')}</Th>
</>
)}
<Th>{t('common:common.Last use time')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name}</Td>
{feConfigs?.isPlus && (
<>
<Td>
{item.limit?.expiredTime
? dayjs(item.limit.expiredTime).format('YYYY-MM-DD HH:mm')
: '-'}
</Td>
</>
)}
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
: ` / ${t('common:common.Unlimited')}`
}`
: ''}
</Td>
<Td>{item.responseDetail ? '✔' : '✖'}</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'}</Td>
<Th>{item?.limit?.hookUrl ? '✔' : '✖'}</Th>
</>
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => setSelectedLinkData(item as OutLinkSchema)}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('common:core.app.outLink.Select Mode')}
</Button>
<MyMenu
Button={
<IconButton
icon={<MyIcon name={'more'} w={'14px'} />}
name={'more'}
variant={'whiteBase'}
size={'sm'}
aria-label={''}
/>
}
menuList={[
{
children: [
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () =>
setEditLinkData({
_id: item._id,
name: item.name,
responseDetail: item.responseDetail ?? false,
showRawSource: item.showRawSource ?? false,
showNodeStatus: item.showNodeStatus ?? false,
limit: item.limit
})
},
{
label: t('common:common.Delete'),
icon: 'delete',
type: 'danger',
onClick: () =>
openConfirm(async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
})()
}
]
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{shareChatList.length === 0 && !isFetching && (
<EmptyTip text={t('common:core.app.share.Not share link')} />
)}
{!!editLinkData && (
<EditLinkModal
appId={appId}
type={PublishChannelEnum.share}
defaultData={editLinkData}
onCreate={(id) => {
const url = `${location.origin}/chat/share?shareId=${id}`;
copyData(url, t('common:core.app.share.Create link tip'));
refetchShareChatList();
setEditLinkData(undefined);
}}
onEdit={() => {
toast({
status: 'success',
title: t('common:common.Update Successful')
});
refetchShareChatList();
setEditLinkData(undefined);
}}
onClose={() => setEditLinkData(undefined)}
/>
)}
{!!selectedLinkData && (
<SelectUsingWayModal
share={selectedLinkData}
onClose={() => setSelectedLinkData(undefined)}
/>
)}
<ConfirmModal />
</MyBox>
);
};
// edit link modal
function EditLinkModal({
appId,
type,
defaultData,
onClose,
onCreate,
onEdit
}: {
appId: string;
type: PublishChannelEnum;
defaultData: OutLinkEditType;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
}) {
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { publishT } = useI18n();
const {
register,
setValue,
watch,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const responseDetail = watch('responseDetail');
const showRawSource = watch('showRawSource');
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const { runAsync: onclickCreate, loading: creating } = useRequest2(
async (e: OutLinkEditType) =>
createShareChat({
...e,
appId,
type
}),
{
errorToast: t('common:common.Create Failed'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(putShareChat, {
errorToast: t('common:common.Update Failed'),
onSuccess: onEdit
});
return (
<MyModal
isOpen={true}
iconSrc="/imgs/modal/shareFill.svg"
title={isEdit ? publishT('edit_link') : publishT('create_link')}
maxW={['90vw', '700px']}
w={'100%'}
h={['90vh', 'auto']}
>
<ModalBody
p={6}
display={['block', 'flex']}
flex={['1 0 0', 'auto']}
overflow={'auto'}
gap={4}
>
<Box pr={[0, 4]} flex={1} borderRight={['0px', '1px']} borderColor={['', 'myGray.150']}>
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
{t('publish:basic_info')}
</Box>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('link_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
{feConfigs?.isPlus && (
<>
<Flex alignItems={'center'} mt={4}>
<FormLabel flex={'0 0 90px'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>QPM</FormLabel>
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
</Flex>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{t('common:support.outlink.Max usage points')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
<FormLabel>{publishT('token_auth')}</FormLabel>
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
</Flex>
<Input
placeholder={publishT('token_auth_tips') || ''}
fontSize={'sm'}
{...register('limit.hookUrl')}
/>
</Flex>
<Link
href={getDocPath('/docs/development/openapi/share')}
target={'_blank'}
fontSize={'xs'}
color={'myGray.500'}
>
{publishT('token_auth_use_cases')}
</Link>
</>
)}
</Box>
<Box flex={1} pt={[6, 0]}>
<Box fontSize={'sm'} fontWeight={'500'} color={'myGray.600'}>
{t('publish:private_config')}
</Box>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<FormLabel>{t('publish:show_node')}</FormLabel>
<Switch {...register('showNodeStatus')} />
</Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.Response Quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.Response Quote tips' || '')}
></QuestionTip>
</Flex>
<Switch {...register('responseDetail')} isChecked={responseDetail} />
</Flex>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
<QuestionTip
ml={1}
label={t('common:support.outlink.share.show_complete_quote_tips' || '')}
></QuestionTip>
</Flex>
<Switch
{...register('showRawSource', {
onChange(e) {
if (e.target.checked) {
setValue('responseDetail', true);
}
}
})}
isChecked={showRawSource}
/>
</Flex>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) => (isEdit ? onclickUpdate(data) : onclickCreate(data)))}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default React.memo(Share);

View File

@@ -1,174 +0,0 @@
import React from 'react';
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { OffiAccountAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import BasicInfo from '../components/BasicInfo';
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const OffiAccountEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<OffiAccountAppType>;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
isEdit?: boolean;
}) => {
const { t } = useTranslation();
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { runAsync: onclickCreate, loading: creating } = useRequest2(
(e: OutLinkEditType<OffiAccountAppType>) => {
if (e?.app) {
e.app.appId = e.app.appId?.trim();
e.app.secret = e.app.secret?.trim();
e.app.CallbackToken = e.app.CallbackToken?.trim();
e.app.CallbackEncodingAesKey = e.app.CallbackEncodingAesKey?.trim();
}
return createShareChat({
...e,
appId,
type: PublishChannelEnum.officialAccount
});
},
{
errorToast: t('common:common.Create Failed'),
successToast: t('common:common.Create Success'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2(
(e) => {
if (e?.app) {
e.app.appId = e.app.appId?.trim();
e.app.secret = e.app.secret?.trim();
e.app.CallbackToken = e.app.CallbackToken?.trim();
e.app.CallbackEncodingAesKey = e.app.CallbackEncodingAesKey?.trim();
}
return updateShareChat(e);
},
{
errorToast: t('common:common.Update Failed'),
successToast: t('common:common.Update Success'),
onSuccess: onEdit
}
);
const { feConfigs } = useSystemStore();
return (
<MyModal
iconSrc="/imgs/modal/shareFill.svg"
title={
isEdit
? t('publish:official_account.edit_modal_title')
: t('publish:official_account.create_modal_title')
}
minW={['auto', '60rem']}
>
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
<Box p={8} minH={['auto', '400px']} borderRight={'base'}>
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
</Box>
<Flex p={8} minH={['auto', '400px']} flexDirection="column" gap={6}>
<Flex alignItems="center">
<Box color="myGray.600">{t('publish:official_account.params')}</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/official_account/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon name="book" w={'17px'} h={'17px'} mr="1" />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
App ID
</FormLabel>
<Input
placeholder="App ID"
{...register('app.appId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Secret
</FormLabel>
<Input
placeholder="Secret"
{...register('app.secret', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Token
</FormLabel>
<Input
placeholder="Token"
{...register('app.CallbackToken', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'}>AES Key</FormLabel>
<Input placeholder="AES Key" {...register('app.CallbackEncodingAesKey')} />
</Flex>
<Box flex={1}></Box>
<Flex justifyContent={'end'}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) =>
isEdit ? onclickUpdate(data) : onclickCreate(data)
)}
>
{t('common:common.Confirm')}
</Button>
</Flex>
</Flex>
</ModalBody>
</MyModal>
);
};
export default OffiAccountEditModal;

View File

@@ -1,250 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure,
Link,
HStack
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { defaultOutLinkForm } from '@/web/core/app/constants';
import type { OutLinkEditType, OffiAccountAppType } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getDocPath } from '@/web/common/system/doc';
const OffiAccountEditModal = dynamic(() => import('./OffiAccountEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const OffiAccount = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const [editOffiAccountData, setEditOffiAccountData] =
useState<OutLinkEditType<OffiAccountAppType>>();
const [isEdit, setIsEdit] = useState<boolean>(false);
const baseUrl = useMemo(
() => feConfigs?.customApiDomain || `${location.origin}/api`,
[feConfigs?.customApiDomain]
);
const {
data: shareChatList = [],
loading: isFetching,
runAsync: refetchShareChatList
} = useRequest2(
() => getShareChatList<OffiAccountAppType>({ appId, type: PublishChannelEnum.officialAccount }),
{
manual: false
}
);
const {
onOpen: openShowShareLinkModal,
isOpen: showShareLinkModalOpen,
onClose: closeShowShareLinkModal
} = useDisclosure();
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<HStack>
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('publish:official_account.name')}
</Box>
{feConfigs?.docUrl && (
<Link
href={
feConfigs.openAPIDocUrl ||
getDocPath('/docs/use-cases/external-integration/official_account/')
}
target={'_blank'}
ml={2}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon name="book" mr="1" w={'1rem'} />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</HStack>
<Button
variant={'primary'}
colorScheme={'blue'}
size={['sm', 'md']}
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
ml={3}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: t('common:core.app.share.Amount limit tip')
}
: {})}
onClick={() => {
setEditOffiAccountData(defaultOutLinkForm as any); // HACK
setIsEdit(false);
}}
>
{t('common:add_new')}
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:common.Name')} </Th>
<Th> {t('common:support.outlink.Usage points')} </Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')} </Th>
<Th> {t('common:common.Expired Time')} </Th>
</>
)}
<Th>{t('common:common.Last use time')} </Th>
<Th> </Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name} </Td>
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
: ` / ${t('common:common.Unlimited')}`
}`
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'} </Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => {
setShowShareLink(`${baseUrl}/support/outLink/offiaccount/${item.shareId}`);
openShowShareLinkModal();
}}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('publish:request_address')}
</Button>
<MyMenu
Button={
<MyIcon
name={'more'}
_hover={{ bg: 'myGray.100' }}
cursor={'pointer'}
borderRadius={'md'}
w={'14px'}
p={2}
/>
}
menuList={[
{
children: [
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () => {
setEditOffiAccountData({
_id: item._id,
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});
setIsEdit(true);
}
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}
}
]
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{editOffiAccountData && (
<OffiAccountEditModal
appId={appId}
defaultData={editOffiAccountData}
onCreate={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
onClose={() => setEditOffiAccountData(undefined)}
isEdit={isEdit}
/>
)}
{shareChatList.length === 0 && !isFetching && (
<EmptyTip text={t('common:core.app.share.Not share link')}> </EmptyTip>
)}
<Loading loading={isFetching} fixed={false} />
{showShareLinkModalOpen && (
<ShowShareLinkModal
shareLink={showShareLink ?? ''}
onClose={closeShowShareLinkModal}
img="/imgs/outlink/offiaccount-copylink-instruction.png"
/>
)}
</Box>
);
};
export default React.memo(OffiAccount);

View File

@@ -1,166 +0,0 @@
import React from 'react';
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { WecomAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import BasicInfo from '../components/BasicInfo';
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const WecomEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<WecomAppType>;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
isEdit?: boolean;
}) => {
const { t } = useTranslation();
const {
register,
setValue,
handleSubmit: submitShareChat
} = useForm({
defaultValues: defaultData
});
const { runAsync: onclickCreate, loading: creating } = useRequest2(
(e) =>
createShareChat({
...e,
appId,
type: PublishChannelEnum.wecom
}),
{
errorToast: t('common:common.Create Failed'),
successToast: t('common:common.Create Success'),
onSuccess: onCreate
}
);
const { runAsync: onclickUpdate, loading: updating } = useRequest2((e) => updateShareChat(e), {
errorToast: t('common:common.Update Failed'),
successToast: t('common:common.Update Success'),
onSuccess: onEdit
});
const { feConfigs } = useSystemStore();
return (
<MyModal
iconSrc="core/app/publish/wecom"
title={isEdit ? t('publish:wecom.edit_modal_title') : t('publish:wecom.create_modal_title')}
minW={['auto', '60rem']}
>
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
<Box p={8} minH={['auto', '400px']} borderRight={'base'}>
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
</Box>
<Flex p={8} minH={['auto', '400px']} flexDirection="column" gap={6}>
<Flex alignItems="center">
<Box color="myGray.600">{t('publish:wecom.api')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/use-cases/wecom-bot')}
target={'_blank'}
ml={2}
color={'primary.500'}
fontSize={'sm'}
>
<Flex alignItems={'center'}>
<MyIcon name="book" w={'17px'} h={'17px'} mr="1" />
{t('common:common.Read document')}
</Flex>
</Link>
)}
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Corp ID
</FormLabel>
<Input
placeholder="Corp ID"
{...register('app.CorpId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Agent ID
</FormLabel>
<Input
placeholder="Agent ID"
{...register('app.AgentId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Secret
</FormLabel>
<Input
placeholder="Secret"
{...register('app.SuiteSecret', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Token
</FormLabel>
<Input
placeholder="Token"
{...register('app.CallbackToken', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
AES Key
</FormLabel>
<Input
placeholder="AES Key"
{...(register('app.CallbackEncodingAesKey'), { required: true })}
/>
</Flex>
<Box flex={1}></Box>
<Flex justifyContent={'end'}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
isLoading={creating || updating}
onClick={submitShareChat((data) =>
isEdit ? onclickUpdate(data) : onclickCreate(data)
)}
>
{t('common:common.Confirm')}
</Button>
</Flex>
</Flex>
</ModalBody>
</MyModal>
);
};
export default WecomEditModal;

View File

@@ -1,223 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
Button,
TableContainer,
Table,
Thead,
Tr,
Th,
Td,
Tbody,
useDisclosure
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { defaultOutLinkForm } from '@/web/core/app/constants';
import type { WecomAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import dayjs from 'dayjs';
import dynamic from 'next/dynamic';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const WecomEditModal = dynamic(() => import('./WecomEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const Wecom = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const [editWecomData, setEditWecomData] = useState<OutLinkEditType<WecomAppType>>();
const [isEdit, setIsEdit] = useState<boolean>(false);
const baseUrl = useMemo(
() => feConfigs?.customApiDomain || `${location.origin}/api`,
[feConfigs?.customApiDomain]
);
const {
data: shareChatList = [],
loading: isFetching,
runAsync: refetchShareChatList
} = useRequest2(() => getShareChatList<WecomAppType>({ appId, type: PublishChannelEnum.wecom }), {
manual: false
});
const {
onOpen: openShowShareLinkModal,
isOpen: showShareLinkModalOpen,
onClose: closeShowShareLinkModal
} = useDisclosure();
const [showShareLink, setShowShareLink] = useState<string | null>(null);
return (
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('publish:wecom.title')}
</Box>
<Button
variant={'primary'}
colorScheme={'blue'}
size={['sm', 'md']}
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
ml={3}
{...(shareChatList.length >= 10
? {
isDisabled: true,
title: t('common:core.app.share.Amount limit tip')
}
: {})}
onClick={() => {
setEditWecomData(defaultOutLinkForm as any); // HACK
setIsEdit(false);
}}
>
{t('common:add_new')}
</Button>
</Flex>
<TableContainer mt={3}>
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:common.Name')} </Th>
<Th> {t('common:support.outlink.Usage points')} </Th>
{feConfigs?.isPlus && (
<>
<Th>{t('common:core.app.share.Ip limit title')} </Th>
<Th> {t('common:common.Expired Time')} </Th>
</>
)}
<Th>{t('common:common.Last use time')} </Th>
<Th> </Th>
</Tr>
</Thead>
<Tbody>
{shareChatList.map((item) => (
<Tr key={item._id}>
<Td>{item.name} </Td>
<Td>
{Math.round(item.usagePoints)}
{feConfigs?.isPlus
? `${
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
? ` / ${item.limit.maxUsagePoints}`
: ` / ${t('common:common.Unlimited')}`
}`
: ''}
</Td>
{feConfigs?.isPlus && (
<>
<Td>{item?.limit?.QPM || '-'} </Td>
<Td>
{item?.limit?.expiredTime
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
: '-'}
</Td>
</>
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => {
setShowShareLink(`${baseUrl}/support/outLink/wecom/${item.shareId}`);
openShowShareLinkModal();
}}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('publish:request_address')}
</Button>
<MyMenu
Button={
<MyIcon
name={'more'}
_hover={{ bg: 'myGray.100' }}
cursor={'pointer'}
borderRadius={'md'}
w={'14px'}
p={2}
/>
}
menuList={[
{
children: [
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () => {
setEditWecomData({
_id: item._id,
name: item.name,
limit: item.limit,
app: item.app,
responseDetail: item.responseDetail,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
});
setIsEdit(true);
}
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
}
}
]
}
]}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{editWecomData && (
<WecomEditModal
appId={appId}
defaultData={editWecomData}
onCreate={() => Promise.all([refetchShareChatList(), setEditWecomData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditWecomData(undefined)])}
onClose={() => setEditWecomData(undefined)}
isEdit={isEdit}
/>
)}
{shareChatList.length === 0 && !isFetching && (
<EmptyTip text={t('common:core.app.share.Not share link')}> </EmptyTip>
)}
<Loading loading={isFetching} fixed={false} />
{showShareLinkModalOpen && (
<ShowShareLinkModal
shareLink={showShareLink ?? ''}
onClose={closeShowShareLinkModal}
img="/imgs/outlink/wecom-copylink-instruction.png"
/>
)}
</Box>
);
};
export default React.memo(Wecom);

View File

@@ -1,87 +0,0 @@
import React from 'react';
import { Box, Flex, Input } from '@chakra-ui/react';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import dayjs from 'dayjs';
import { useTranslation } from 'next-i18next';
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
import { OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
function BasicInfo({
register,
setValue,
defaultData
}: {
register: UseFormRegister<OutLinkEditType<any>>;
setValue: UseFormSetValue<OutLinkEditType<any>>;
defaultData: OutLinkEditType<any>;
}) {
const { t } = useTranslation();
return (
<Flex flexDirection="column" gap={6}>
<Box color="myGray.600">{t('publish:basic_info')}</Box>
<Flex alignItems={'center'}>
<FormLabel required flex={'0 0 6.25rem'}>
{t('common:Name')}
</FormLabel>
<Input
placeholder={t('publish:publish_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty')
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} alignItems={'center'}>
QPM
<QuestionTip ml={1} label={t('publish:qpm_tips')}></QuestionTip>
</FormLabel>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: t('publish:qpm_is_empty')
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} alignItems={'center'}>
{t('common:support.outlink.Max usage points')}
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</FormLabel>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
</Flex>
);
}
export default BasicInfo;

View File

@@ -1,51 +0,0 @@
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { Box, Image, Flex, ModalBody } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
export type ShowShareLinkModalProps = {
shareLink: string;
onClose: () => void;
img: string;
};
function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) {
const { copyData } = useCopyData();
const { t } = useTranslation();
return (
<MyModal onClose={onClose} title={t('publish:show_share_link_modal_title')}>
<ModalBody>
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
<Flex
p={3}
bg={'myWhite.500'}
border="base"
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
>
<Box flex={1}>{t('publish:copy_link_hint')}</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => copyData(shareLink)}
/>
</Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
{shareLink}
</Box>
</Box>
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
<MyImage src={img} borderRadius="0.5rem" alt="" />
</Box>
</ModalBody>
</MyModal>
);
}
export default ShowShareLinkModal;

View File

@@ -1,133 +0,0 @@
import React, { useRef, useState } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import dynamic from 'next/dynamic';
import MyRadio from '@/components/common/MyRadio';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
const Link = dynamic(() => import('./Link'));
const API = dynamic(() => import('./API'));
const FeiShu = dynamic(() => import('./FeiShu'));
const DingTalk = dynamic(() => import('./DingTalk'));
// const Wecom = dynamic(() => import('./Wecom'));
const OffiAccount = dynamic(() => import('./OffiAccount'));
const OutLink = () => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { toast } = useToast();
const appId = useContextSelector(AppContext, (v) => v.appId);
const publishList = useRef([
{
icon: '/imgs/modal/shareFill.svg',
title: t('common:core.app.Share link'),
desc: t('common:core.app.Share link desc'),
value: PublishChannelEnum.share,
isProFn: false
},
{
icon: 'support/outlink/apikeyFill',
title: t('common:core.app.Api request'),
desc: t('common:core.app.Api request desc'),
value: PublishChannelEnum.apikey,
isProFn: false
},
{
icon: 'core/app/publish/lark',
title: t('publish:feishu_bot'),
desc: t('publish:feishu_bot_desc'),
value: PublishChannelEnum.feishu,
isProFn: true
},
{
icon: 'common/dingtalkFill',
title: t('publish:dingtalk.bot'),
desc: t('publish:dingtalk.bot_desc'),
value: PublishChannelEnum.dingtalk,
isProFn: true
},
// {
// icon: 'core/app/publish/wecom',
// title: t('publish:wecom.bot'),
// desc: t('publish:wecom.bot_desc'),
// value: PublishChannelEnum.wecom,
// isProFn: true
// },
{
icon: 'core/app/publish/offiaccount',
title: t('publish:official_account.name'),
desc: t('publish:official_account.desc'),
value: PublishChannelEnum.officialAccount,
isProFn: true
}
]);
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
return (
<Box
display={['block', 'flex']}
overflowY={'auto'}
overflowX={'hidden'}
h={'100%'}
flexDirection={'column'}
>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
<MyRadio
gridTemplateColumns={[
'repeat(1,1fr)',
'repeat(2, 1fr)',
'repeat(3, 1fr)',
'repeat(3, 1fr)',
'repeat(4, 1fr)'
]}
iconSize={'20px'}
list={publishList.current}
value={linkType}
onChange={(e) => {
const config = publishList.current.find((v) => v.value === e)!;
if (!feConfigs.isPlus && config.isProFn) {
toast({
status: 'warning',
title: t('common:common.system.Commercial version function')
});
} else {
setLinkType(e as PublishChannelEnum);
}
}}
/>
</Box>
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={4}
px={[4, 8]}
py={[4, 6]}
flex={1}
>
{linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} />
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
{linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />}
{/* {linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />} */}
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />}
</Flex>
</Box>
);
};
export default OutLink;

View File

@@ -1,358 +0,0 @@
import React, { useState } from 'react';
import {
getAppVersionDetail,
getWorkflowVersionList,
updateAppVersion
} from '@/web/core/app/api/version';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next';
import { Box, BoxProps, Button, Flex, Input } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from './context';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import type { WorkflowSnapshotsType } from './WorkflowComponents/context';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import Avatar from '@fastgpt/web/components/common/Avatar';
import Tag from '@fastgpt/web/components/common/Tag';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version';
import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots';
const PublishHistoriesSlider = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({
onClose,
past,
onSwitchTmpVersion,
onSwitchCloudVersion,
positionStyles
}: {
onClose: () => void;
past: T[];
onSwitchTmpVersion: (params: T, customTitle: string) => void;
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => void;
positionStyles?: BoxProps;
}) => {
const { t } = useTranslation();
const [currentTab, setCurrentTab] = useState<'myEdit' | 'teamCloud'>('myEdit');
return (
<>
<CustomRightDrawer
onClose={() => onClose()}
title={
(
<>
<LightRowTabs
list={[
{ label: t('workflow:workflow.My edit'), value: 'myEdit' },
{ label: t('workflow:workflow.Team cloud'), value: 'teamCloud' }
]}
value={currentTab}
onChange={setCurrentTab}
inlineStyles={{ px: 0.5, pb: 2 }}
gap={5}
py={0}
fontSize={'sm'}
/>
</>
) as any
}
maxW={'340px'}
px={0}
showMask={false}
overflow={'unset'}
{...positionStyles}
>
{currentTab === 'myEdit' ? (
<MyEdit past={past} onSwitchTmpVersion={onSwitchTmpVersion} />
) : (
<TeamCloud onSwitchCloudVersion={onSwitchCloudVersion} />
)}
</CustomRightDrawer>
</>
);
};
export default PublishHistoriesSlider;
const MyEdit = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({
past,
onSwitchTmpVersion
}: {
past: T[];
onSwitchTmpVersion: (params: T, customTitle: string) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
return (
<Flex px={5} flex={'1 0 0'} flexDirection={'column'}>
{past.length > 0 && (
<Box py={2} px={3}>
<Button
variant={'whiteBase'}
w={'full'}
h={'30px'}
onClick={async () => {
const initialSnapshot = past[past.length - 1];
onSwitchTmpVersion(initialSnapshot, t(`app:version_initial_copy`));
toast({
title: t('workflow:workflow.Switch_success'),
status: 'success'
});
}}
>
{t('app:version_back')}
</Button>
</Box>
)}
<Flex flex={'1 0 0'} flexDirection={'column'} overflow={'auto'}>
{past.map((item, index) => {
return (
<Flex
key={index}
alignItems={'center'}
py={2}
px={3}
borderRadius={'md'}
cursor={'pointer'}
fontWeight={500}
_hover={{
bg: 'primary.50'
}}
onClick={() => {
onSwitchTmpVersion(item, `${t('app:version_copy')}-${item.title}`);
toast({
title: t('workflow:workflow.Switch_success'),
status: 'success'
});
}}
>
<Box
w={'12px'}
h={'12px'}
borderWidth={'2px'}
borderColor={'primary.600'}
borderRadius={'50%'}
position={'relative'}
{...(index !== past.length - 1 && {
_after: {
content: '""',
height: '26px',
width: '2px',
bgColor: 'myGray.250',
position: 'absolute',
top: '10px',
left: '3px'
}
})}
></Box>
<Box
ml={3}
flex={'1 0 0'}
fontSize={'sm'}
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
color={'myGray.900'}
>
{item.title}
</Box>
</Flex>
);
})}
<Box py={2} textAlign={'center'} color={'myGray.600'} fontSize={'xs'}>
{t('common:common.No more data')}
</Box>
</Flex>
</Flex>
);
};
const TeamCloud = ({
onSwitchCloudVersion
}: {
onSwitchCloudVersion: (appVersion: AppVersionSchemaType) => void;
}) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const {
ScrollData,
data: scrollDataList,
setData,
isLoading
} = useScrollPagination(getWorkflowVersionList, {
pageSize: 30,
params: {
appId: appDetail._id
},
refreshDeps: [appDetail._id]
});
const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
const { toast } = useToast();
const { runAsync: onChangeVersion, loading: isLoadingVersion } = useRequest2(
async (versionItem: VersionListItemType) => {
const versionDetail = await getAppVersionDetail(versionItem._id, versionItem.appId);
if (!versionDetail) return;
onSwitchCloudVersion(versionDetail);
toast({
title: t('workflow:workflow.Switch_success'),
status: 'success'
});
}
);
const { runAsync: onUpdateVersion, loading: isEditing } = useRequest2(
async (item: VersionListItemType, name: string) => {
await updateAppVersion({
appId: item.appId,
versionName: name,
versionId: item._id
});
setData((state) =>
state.map((version) =>
version._id === item._id ? { ...version, versionName: name } : version
)
);
setEditIndex(undefined);
}
);
return (
<ScrollData isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
{scrollDataList.map((item, index) => {
const firstPublishedIndex = scrollDataList.findIndex((data) => data.isPublish);
return (
<Flex
key={item._id}
alignItems={'center'}
py={editIndex !== index ? 2 : 1}
px={3}
borderRadius={'md'}
cursor={'pointer'}
fontWeight={500}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(undefined)}
_hover={{
bg: 'primary.50'
}}
onClick={() => editIndex === undefined && onChangeVersion(item)}
>
<MyPopover
trigger="hover"
placement={'bottom-end'}
w={'208px'}
h={'72px'}
Trigger={
<Box>
<Avatar
src={item.sourceMember.avatar}
borderRadius={'50%'}
w={'24px'}
h={'24px'}
/>
</Box>
}
>
{() => (
<Flex alignItems={'center'} h={'full'} pl={5} gap={2}>
<Box>
<Avatar
src={item.sourceMember.avatar}
borderRadius={'50%'}
w={'36px'}
h={'36px'}
/>
</Box>
<Box>
<Flex gap={1} fontSize={'sm'} color={'myGray.900'}>
<Box>{item.sourceMember.name}</Box>
{item.sourceMember.status === 'leave' && (
<Tag color="gray">{t('common:user_leaved')}</Tag>
)}
</Flex>
<Box fontSize={'xs'} mt={2} color={'myGray.500'}>
{formatTime2YMDHMS(item.time)}
</Box>
</Box>
</Flex>
)}
</MyPopover>
{editIndex !== index ? (
<>
<Box
ml={3}
flex={'1 0 0'}
fontSize={'sm'}
display="flex"
alignItems="center"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
<Box minWidth={0} overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
<Box as={'span'} color={'myGray.900'}>
{item.versionName || formatTime2YMDHMS(item.time)}
</Box>
</Box>
{item.isPublish && (
<Tag
ml={3}
flexShrink={0}
type="borderSolid"
colorSchema={index === firstPublishedIndex ? 'green' : 'blue'}
>
{index === firstPublishedIndex
? t('app:app.version_current')
: t('app:app.version_past')}
</Tag>
)}
</Box>
{hoveredIndex === index && (
<MyIcon
name="edit"
w={'18px'}
ml={2}
_hover={{ color: 'primary.600' }}
onClick={(e) => {
e.stopPropagation();
setEditIndex(index);
}}
/>
)}
</>
) : (
<MyBox ml={3} isLoading={isEditing} size={'md'}>
<Input
autoFocus
h={8}
defaultValue={item.versionName || formatTime2YMDHMS(item.time)}
onClick={(e) => e.stopPropagation()}
onBlur={(e) => onUpdateVersion(item, e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
// @ts-ignore
onUpdateVersion(item, e.target.value);
}
}}
/>
</MyBox>
)}
</Flex>
);
})}
</ScrollData>
);
};

View File

@@ -1,75 +0,0 @@
import { Box, HStack } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import { AppContext, TabEnum } from './context';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const RouteTab = () => {
const { t } = useTranslation();
const router = useRouter();
const { appDetail, currentTab } = useContextSelector(AppContext, (v) => v);
const setCurrentTab = useCallback(
(tab: TabEnum) => {
router.replace({
query: {
...router.query,
currentTab: tab
}
});
},
[router]
);
const tabList = useMemo(
() => [
{
label:
appDetail.type === AppTypeEnum.plugin ? t('app:setting_plugin') : t('app:setting_app'),
id: TabEnum.appEdit
},
...(appDetail.permission.hasManagePer
? [
{
label: t('app:publish_channel'),
id: TabEnum.publish
},
{ label: t('app:chat_logs'), id: TabEnum.logs }
]
: [])
],
[appDetail.permission.hasManagePer, appDetail.type]
);
return (
<HStack spacing={4} whiteSpace={'nowrap'}>
{tabList.map((tab) => (
<Box
key={tab.id}
px={2}
py={0.5}
fontWeight={'medium'}
borderRadius={'sm'}
{...(currentTab === tab.id
? {
color: 'primary.700'
}
: {
color: 'myGray.600',
cursor: 'pointer',
_hover: {
bg: 'myGray.200'
},
onClick: () => setCurrentTab(tab.id)
})}
>
{tab.label}
</Box>
))}
</HStack>
);
};
export default RouteTab;

View File

@@ -1,213 +0,0 @@
import React, { useState } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
ModalBody,
Checkbox,
ModalFooter
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { AppSchema, AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TagsEditModal from '../TagsEditModal';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { SimpleAppSnapshotType } from './useSnapshots';
import ExportConfigPopover from '@/pageComponents/app/detail/ExportConfigPopover';
const AppCard = ({
appForm,
setPast
}: {
appForm: AppSimpleEditFormType;
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
// transition to workflow
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
const { runAsync: onTransition, loading: transiting } = useRequest2(
async () => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
nodes,
edges,
chatConfig: appForm.chatConfig,
isPublish: false,
versionName: t('app:transition_to_workflow')
});
return postTransition2Workflow({ appId, createNew: transitionCreateNew });
},
{
onSuccess: ({ id }) => {
if (id) {
router.replace({
query: {
appId: id
}
});
} else {
setPast([]);
router.reload();
}
},
successToast: t('common:common.Success')
}
);
return (
<>
{/* basic info */}
<Box px={[4, 6]} py={4} position={'relative'}>
<Flex alignItems={'center'}>
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'} color={'myGray.900'}>
{appDetail.name}
</Box>
</Flex>
<Box
flex={1}
mt={3}
mb={4}
className={'textEllipsis3'}
wordBreak={'break-all'}
color={'myGray.600'}
fontSize={'xs'}
minH={'46px'}
>
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
</Box>
<HStack alignItems={'center'}>
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
onClick={() => router.push(`/chat?appId=${appId}`)}
>
{t('common:core.Chat')}
</Button>
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={onOpenInfoEdit}
>
{t('common:common.Setting')}
</Button>
)}
{appDetail.permission.isOwner && (
<MyMenu
size={'xs'}
Button={
<IconButton
variant={'whitePrimary'}
size={['smSquare', 'mdSquare']}
icon={<MyIcon name={'more'} w={'1rem'} />}
aria-label={''}
/>
}
menuList={[
{
children: [
{
label: (
<Flex>
<ExportConfigPopover
appName={appDetail.name}
appForm={appForm}
chatConfig={appDetail.chatConfig}
/>
</Flex>
)
},
{
icon: 'core/app/type/workflow',
label: t('app:transition_to_workflow'),
onClick: () => setTransitionCreateNew(true)
},
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
? [
{
icon: 'core/chat/fileSelect',
label: t('common:common.Team Tags Set'),
onClick: () => setTeamTagsSet(appDetail)
}
]
: [])
]
},
{
children: [
{
icon: 'delete',
type: 'danger',
label: t('common:common.Delete'),
onClick: onDelApp
}
]
}
]}
/>
)}
<Box flex={1} />
{/* {isPc && ( */}
{/* <MyTag */}
{/* type="borderFill" */}
{/* colorSchema="gray" */}
{/* onClick={() => (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */}
{/* > */}
{/* <PermissionIconText defaultPermission={appDetail.defaultPermission} /> */}
{/* </MyTag> */}
{/* )} */}
</HStack>
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
{transitionCreateNew !== undefined && (
<MyModal isOpen title={t('app:transition_to_workflow')} iconSrc="core/app/type/workflow">
<ModalBody>
<Box mb={3}>{t('app:transition_to_workflow_create_new_tip')}</Box>
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
<Checkbox
isChecked={transitionCreateNew}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Box>{t('app:transition_to_workflow_create_new_placeholder')}</Box>
</HStack>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={() => setTransitionCreateNew(undefined)} mr={3}>
{t('common:common.Close')}
</Button>
<Button variant={'dangerFill'} isLoading={transiting} onClick={() => onTransition()}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
)}
</>
);
};
export default React.memo(AppCard);

View File

@@ -1,107 +0,0 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useEffect, useMemo } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSafeState } from 'ahooks';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { useChatTest } from '../useChatTest';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
type Props = { appForm: AppSimpleEditFormType };
const ChatTest = ({ appForm }: Props) => {
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
// form2AppWorkflow dependent allDatasets
const { allDatasets } = useDatasetStore();
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useEffect(() => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
// console.log(form2AppWorkflow(appForm, t));
setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData, allDatasets, t]);
const { ChatContainer, restartChat, loading } = useChatTest({
...workflowData,
chatConfig: appForm.chatConfig,
isReady: true
});
return (
<MyBox
isLoading={loading}
display={'flex'}
position={'relative'}
flexDirection={'column'}
h={'100%'}
py={4}
>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
{appT('chat_debug')}
</Box>
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={(e) => {
e.stopPropagation();
restartChat();
}}
/>
</MyTooltip>
</Flex>
<Box flex={1}>
<ChatContainer />
</Box>
</MyBox>
);
};
const Render = ({ appForm }: Props) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const chatRecordProviderParams = useMemo(
() => ({
chatId: chatId,
appId: appDetail._id
}),
[appDetail._id, chatId]
);
return (
<ChatItemContextProvider
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest appForm={appForm} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
};
export default React.memo(Render);

View File

@@ -1,59 +0,0 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import ChatTest from './ChatTest';
import AppCard from './AppCard';
import EditForm from './EditForm';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { cardStyles } from '../constants';
import styles from './styles.module.scss';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { SimpleAppSnapshotType } from './useSnapshots';
const Edit = ({
appForm,
setAppForm,
setPast
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
}) => {
const { isPc } = useSystem();
return (
<Box
display={['block', 'flex']}
flex={'1 0 0'}
h={0}
mt={[4, 0]}
gap={1}
borderRadius={'lg'}
overflowY={['auto', 'unset']}
>
<Box
className={styles.EditAppBox}
pr={[0, 1]}
overflowY={'auto'}
minW={['auto', '580px']}
flex={'1'}
>
<Box {...cardStyles} boxShadow={'2'}>
<AppCard appForm={appForm} setPast={setPast} />
</Box>
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
<EditForm appForm={appForm} setAppForm={setAppForm} />
</Box>
</Box>
{isPc && (
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0} mb={3}>
<ChatTest appForm={appForm} />
</Box>
)}
</Box>
);
};
export default React.memo(Edit);

View File

@@ -1,432 +0,0 @@
import React, { useMemo, useTransition } from 'react';
import {
Box,
Flex,
Grid,
BoxProps,
useTheme,
useDisclosure,
Button,
HStack
} from '@chakra-ui/react';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import dynamic from 'next/dynamic';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { workflowSystemVariables } from '@/web/core/app/utils';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from './components/ToolSelect';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGConfig = dynamic(() => import('@/components/core/app/QGConfig'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
const InputGuideConfig = dynamic(() => import('@/components/core/app/InputGuideConfig'));
const WelcomeTextConfig = dynamic(() => import('@/components/core/app/WelcomeTextConfig'));
const FileSelectConfig = dynamic(() => import('@/components/core/app/FileSelect'));
const BoxStyles: BoxProps = {
px: [4, 6],
py: '16px',
borderBottomWidth: '1px',
borderBottomColor: 'borderColor.low'
};
const LabelStyles: BoxProps = {
w: ['60px', '100px'],
whiteSpace: 'nowrap',
flexShrink: 0,
fontSize: 'sm',
color: 'myGray.900'
};
const EditForm = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { allDatasets } = useDatasetStore();
const [, startTst] = useTransition();
const selectDatasets = useMemo(
() =>
allDatasets.filter((item) =>
appForm.dataset?.datasets.find((dataset) => dataset.datasetId === item._id)
),
[allDatasets, appForm?.dataset?.datasets]
);
const {
isOpen: isOpenDatasetSelect,
onOpen: onOpenKbSelect,
onClose: onCloseKbSelect
} = useDisclosure();
const {
isOpen: isOpenDatasetParams,
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
const formatVariables = useMemo(
() =>
formatEditorVariablePickerIcon([
...workflowSystemVariables.filter(
(variable) =>
!['appId', 'chatId', 'responseChatItemId', 'histories'].includes(variable.key)
),
...(appForm.chatConfig.variables || [])
]).map((item) => ({
...item,
label: t(item.label as any),
parent: {
id: 'VARIABLE_NODE_ID',
label: t('common:core.module.Variable'),
avatar: 'core/workflow/template/variable'
}
})),
[appForm.chatConfig.variables, t]
);
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
const tokenLimit = useMemo(() => {
return selectedModel?.quoteMaxToken || 3000;
}, [selectedModel?.quoteMaxToken]);
return (
<>
<Box>
{/* ai */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/ai'} w={'20px'} />
<FormLabel ml={2} flex={1}>
{t('app:ai_settings')}
</FormLabel>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box {...LabelStyles}>{t('common:core.ai.Model')}</Box>
<Box flex={'1 0 0'}>
<SettingLLMModel
bg="myGray.50"
llmModelType={'all'}
defaultData={{
model: appForm.aiSettings.model,
temperature: appForm.aiSettings.temperature,
maxToken: appForm.aiSettings.maxToken,
maxHistories: appForm.aiSettings.maxHistories
}}
onChange={({ model, temperature, maxToken, maxHistories }: SettingAIDataType) => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
model,
temperature,
maxToken,
maxHistories: maxHistories ?? 6
}
}));
}}
/>
</Box>
</Flex>
<Box mt={4}>
<HStack {...LabelStyles} w={'100%'}>
<Box>{t('common:core.ai.Prompt')}</Box>
<QuestionTip label={t('common:core.app.tip.systemPromptTip')} />
<Box flex={1} />
<VariableTip color={'myGray.500'} />
</HStack>
<Box mt={1}>
<PromptEditor
minH={150}
value={appForm.aiSettings.systemPrompt}
bg={'myGray.50'}
onChange={(text) => {
startTst(() => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
systemPrompt: text
}
}));
});
}}
variableLabels={formatVariables}
variables={formatVariables}
placeholder={t('common:core.app.tip.systemPromptTip')}
title={t('common:core.ai.Prompt')}
/>
</Box>
</Box>
</Box>
{/* dataset */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/simpleMode/dataset'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.dataset.Choose Dataset')}</FormLabel>
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name="common/addLight" w={'0.8rem'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenKbSelect}
>
{t('common:common.Choose')}
</Button>
<Button
variant={'transparentBase'}
leftIcon={<MyIcon name={'edit'} w={'14px'} />}
iconSpacing={1}
size={'sm'}
fontSize={'sm'}
onClick={onOpenDatasetParams}
>
{t('common:common.Params')}
</Button>
</Flex>
{appForm.dataset.datasets?.length > 0 && (
<Box my={3}>
<SearchParamsTip
searchMode={appForm.dataset.searchMode}
similarity={appForm.dataset.similarity}
limit={appForm.dataset.limit}
usingReRank={appForm.dataset.usingReRank}
datasetSearchUsingExtensionQuery={appForm.dataset.datasetSearchUsingExtensionQuery}
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
/>
</Box>
)}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => (
<MyTooltip key={item._id} label={t('common:core.dataset.Read Dataset')}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
cursor={'pointer'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item._id
}
})
}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
{/* tool choice */}
<Box {...BoxStyles}>
<ToolSelect appForm={appForm} setAppForm={setAppForm} />
</Box>
{/* File select */}
<Box {...BoxStyles}>
<FileSelectConfig
forbidVision={!selectedModel?.vision}
value={appForm.chatConfig.fileSelectConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
}}
/>
</Box>
{/* variable */}
<Box {...BoxStyles}>
<VariableEdit
variables={appForm.chatConfig.variables}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
variables: e
}
}));
}}
/>
</Box>
{/* welcome */}
<Box {...BoxStyles}>
<WelcomeTextConfig
value={appForm.chatConfig.welcomeText}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
welcomeText: e.target.value
}
}));
}}
/>
</Box>
{/* tts */}
<Box {...BoxStyles}>
<TTSSelect
value={appForm.chatConfig.ttsConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
ttsConfig: e
}
}));
}}
/>
</Box>
{/* whisper */}
<Box {...BoxStyles}>
<WhisperConfig
isOpenAudio={appForm.chatConfig.ttsConfig?.type !== TTSTypeEnum.none}
value={appForm.chatConfig.whisperConfig}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
whisperConfig: e
}
}));
}}
/>
</Box>
{/* question guide */}
<Box {...BoxStyles}>
<QGConfig
value={appForm.chatConfig.questionGuide}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
questionGuide: e
}
}));
}}
/>
</Box>
{/* question tips */}
<Box {...BoxStyles}>
<InputGuideConfig
appId={appDetail._id}
value={appForm.chatConfig.chatInputGuide}
onChange={(e) => {
setAppForm((state) => ({
...state,
chatConfig: {
...state.chatConfig,
chatInputGuide: e
}
}));
}}
/>
</Box>
</Box>
{isOpenDatasetSelect && (
<DatasetSelectModal
isOpen={isOpenDatasetSelect}
defaultSelectedDatasets={selectDatasets.map((item) => ({
datasetId: item._id,
vectorModel: item.vectorModel
}))}
onClose={onCloseKbSelect}
onChange={(e) => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets: e
}
}));
}}
/>
)}
{isOpenDatasetParams && (
<DatasetParamsModal
{...appForm.dataset}
maxTokens={tokenLimit}
onClose={onCloseDatasetParams}
onSuccess={(e) => {
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
...e
}
}));
console.dir(e);
}}
/>
)}
</>
);
};
export default React.memo(EditForm);

View File

@@ -1,257 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import FolderPath from '@/components/common/folder/Path';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppFolderPath } from '@/web/core/app/api/app';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import RouteTab from '../RouteTab';
import { useTranslation } from 'next-i18next';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { TabEnum } from '../context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import SaveButton from '../Workflow/components/SaveButton';
import { useBoolean, useDebounceEffect, useLockFn } from 'ahooks';
import { appWorkflow2Form } from '@fastgpt/global/core/app/utils';
import {
compareSimpleAppSnapshot,
onSaveSnapshotFnType,
SimpleAppSnapshotType
} from './useSnapshots';
import PublishHistories from '../PublishHistoriesSlider';
import { AppVersionSchemaType } from '@fastgpt/global/core/app/version';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { isProduction } from '@fastgpt/global/common/system/constants';
const Header = ({
forbiddenSaveSnapshot,
appForm,
setAppForm,
past,
setPast,
saveSnapshot
}: {
forbiddenSaveSnapshot: React.MutableRefObject<boolean>;
appForm: AppSimpleEditFormType;
setAppForm: (form: AppSimpleEditFormType) => void;
past: SimpleAppSnapshotType[];
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
saveSnapshot: onSaveSnapshotFnType;
}) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const router = useRouter();
const appId = useContextSelector(AppContext, (v) => v.appId);
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const currentTab = useContextSelector(AppContext, (v) => v.currentTab);
const { lastAppListRouteType } = useSystemStore();
const { allDatasets } = useDatasetStore();
const { data: paths = [] } = useRequest2(() => getAppFolderPath(appId), {
manual: false,
refreshDeps: [appId]
});
const onClickRoute = useCallback(
(parentId: string) => {
router.push({
pathname: '/app/list',
query: {
parentId,
type: lastAppListRouteType
}
});
},
[router, lastAppListRouteType]
);
const { runAsync: onClickSave, loading } = useRequest2(
async ({
isPublish,
versionName = formatTime2YMDHMS(new Date()),
autoSave
}: {
isPublish?: boolean;
versionName?: string;
autoSave?: boolean;
}) => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
nodes,
edges,
chatConfig: appForm.chatConfig,
isPublish,
versionName,
autoSave
});
setPast((prevPast) =>
prevPast.map((item, index) =>
index === 0
? {
...item,
isSaved: true
}
: item
)
);
}
);
const [isShowHistories, { setTrue: setIsShowHistories, setFalse: closeHistories }] =
useBoolean(false);
const onSwitchTmpVersion = useCallback(
(data: SimpleAppSnapshotType, customTitle: string) => {
setAppForm(data.appForm);
// Remove multiple "copy-"
const copyText = t('app:version_copy');
const regex = new RegExp(`(${copyText}-)\\1+`, 'g');
const title = customTitle.replace(regex, `$1`);
return saveSnapshot({
appForm: data.appForm,
title
});
},
[saveSnapshot, setAppForm, t]
);
const onSwitchCloudVersion = useCallback(
(appVersion: AppVersionSchemaType) => {
const appForm = appWorkflow2Form({
nodes: appVersion.nodes,
chatConfig: appVersion.chatConfig
});
const res = saveSnapshot({
appForm,
title: `${t('app:version_copy')}-${appVersion.versionName}`
});
forbiddenSaveSnapshot.current = true;
setAppForm(appForm);
return res;
},
[forbiddenSaveSnapshot, saveSnapshot, setAppForm, t]
);
// Check if the workflow is published
const [isSaved, setIsSaved] = useState(false);
useDebounceEffect(
() => {
const savedSnapshot = past.find((snapshot) => snapshot.isSaved);
const val = compareSimpleAppSnapshot(savedSnapshot?.appForm, appForm);
setIsSaved(val);
},
[past, allDatasets],
{ wait: 500 }
);
const onLeaveAutoSave = useLockFn(async () => {
if (isSaved) return;
try {
console.log('Leave auto save');
return onClickSave({ isPublish: false, autoSave: true });
} catch (error) {
console.error(error);
}
});
useEffect(() => {
return () => {
if (isProduction) {
onLeaveAutoSave();
}
};
}, []);
useBeforeunload({
tip: t('common:core.common.tip.leave page'),
callback: onLeaveAutoSave
});
return (
<Box h={14}>
{!isPc && (
<Flex justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
<Box flex={'1'}>
<FolderPath
rootName={t('app:all_apps')}
paths={paths}
hoverStyle={{ color: 'primary.600' }}
onClick={onClickRoute}
fontSize={'14px'}
/>
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
{currentTab === TabEnum.appEdit && (
<Flex alignItems={'center'}>
{!isShowHistories && (
<>
{isPc && (
<MyTag
mr={3}
type={'borderFill'}
showDot
colorSchema={
isSaved
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{t(
isSaved
? publishStatusStyle.published.text
: publishStatusStyle.unPublish.text
)}
</MyTag>
)}
<IconButton
mr={[2, 4]}
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={setIsShowHistories}
/>
<SaveButton isLoading={loading} onClickSave={onClickSave} />
</>
)}
</Flex>
)}
</Flex>
{isShowHistories && currentTab === TabEnum.appEdit && (
<PublishHistories<SimpleAppSnapshotType>
onClose={closeHistories}
past={past}
onSwitchTmpVersion={onSwitchTmpVersion}
onSwitchCloudVersion={onSwitchCloudVersion}
positionStyles={{
top: 14,
bottom: 3
}}
/>
)}
</Box>
);
};
export default Header;

View File

@@ -1,130 +0,0 @@
import { Button, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { childAppSystemKey } from './ToolSelectModal';
import { Controller, useForm } from 'react-hook-form';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import RenderPluginInput from '@/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
const ConfigToolModal = ({
configTool,
onCloseConfigTool,
onAddTool
}: {
configTool: AppSimpleEditFormType['selectedTools'][number];
onCloseConfigTool: () => void;
onAddTool: (tool: AppSimpleEditFormType['selectedTools'][number]) => void;
}) => {
const { t } = useTranslation();
const {
handleSubmit,
control,
formState: { errors }
} = useForm({
defaultValues: configTool
? configTool.inputs.reduce(
(acc, input) => {
acc[input.key] = input.value || input.defaultValue;
return acc;
},
{} as Record<string, any>
)
: {}
});
return (
<MyModal
isOpen
isCentered
title={t('common:core.app.ToolCall.Parameter setting')}
iconSrc="core/app/toolCall"
overflow={'auto'}
>
<ModalBody>
<HStack mb={4} spacing={1} fontSize={'sm'}>
<MyIcon name={'common/info'} w={'1.25rem'} />
<Box flex={1}>{t('app:tool_input_param_tip')}</Box>
{!!(configTool?.courseUrl || configTool?.userGuide) && (
<UseGuideModal
title={configTool?.name}
iconSrc={configTool?.avatar}
text={configTool?.userGuide}
link={configTool?.courseUrl}
>
{({ onClick }) => (
<Box cursor={'pointer'} color={'primary.500'} onClick={onClick}>
{t('app:workflow.Input guide')}
</Box>
)}
</UseGuideModal>
)}
</HStack>
{configTool.inputs
.filter(
(input) =>
!input.toolDescription &&
!childAppSystemKey.includes(input.key) &&
!input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) &&
!input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
)
.map((input) => {
return (
<Controller
key={input.key}
control={control}
name={input.key}
rules={{
validate: (value) => {
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
return value !== undefined;
}
return !!value;
}
}}
render={({ field: { onChange, value } }) => {
return (
<RenderPluginInput
value={value}
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
/>
);
}}
/>
);
})}
</ModalBody>
<ModalFooter gap={6}>
<Button onClick={onCloseConfigTool} variant={'whiteBase'}>
{t('common:common.Cancel')}
</Button>
<Button
variant={'primary'}
onClick={handleSubmit((data) => {
onAddTool({
...configTool,
inputs: configTool.inputs.map((input) => ({
...input,
value: data[input.key] ?? input.value
}))
});
onCloseConfigTool();
})}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ConfigToolModal);

View File

@@ -1,157 +0,0 @@
import { Box, Button, Flex, Grid, useDisclosure } from '@chakra-ui/react';
import React, { useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { SmallAddIcon } from '@chakra-ui/icons';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { theme } from '@fastgpt/web/styles/theme';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import ToolSelectModal, { childAppSystemKey } from './ToolSelectModal';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import ConfigToolModal from './ConfigToolModal';
import { getWebLLMModel } from '@/web/common/system/utils';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const ToolSelect = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const { t } = useTranslation();
const [configTool, setConfigTool] = useState<
AppSimpleEditFormType['selectedTools'][number] | null
>(null);
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
return (
<>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.app.Tool call')}</FormLabel>
<QuestionTip ml={1} label={t('app:plugin_dispatch_tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common:common.Choose')}
</Button>
</Flex>
<Grid
mt={appForm.selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => (
<MyTooltip key={item.id} label={item.intro}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2.5}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
cursor={'pointer'}
onClick={() => {
if (
item.inputs
.filter((input) => !childAppSystemKey.includes(input.key))
.every(
(input) =>
input.toolDescription ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
)
) {
return;
}
setConfigTool(item);
}}
>
<Avatar src={item.avatar} w={'1.5rem'} h={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
<DeleteIcon
onClick={(e) => {
e.stopPropagation();
setAppForm((state: AppSimpleEditFormType) => ({
...state,
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
}));
}}
/>
</Flex>
</MyTooltip>
))}
</Grid>
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={appForm.selectedTools}
chatConfig={appForm.chatConfig}
selectedModel={selectedModel}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [...state.selectedTools, e]
}));
}}
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.pluginId !== e.id)
}));
}}
onClose={onCloseToolsSelect}
/>
)}
{configTool && (
<ConfigToolModal
configTool={configTool}
onCloseConfigTool={() => setConfigTool(null)}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.map((item) =>
item.pluginId === configTool.pluginId ? e : item
)
}));
}}
/>
)}
</>
);
};
export default React.memo(ToolSelect);

View File

@@ -1,550 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
css,
Flex,
Grid
} from '@chakra-ui/react';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import {
FlowNodeTemplateType,
NodeTemplateListItemType,
NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
getPluginGroups,
getPreviewPluginNode,
getSystemPlugTemplates,
getSystemPluginPaths
} from '@/web/core/app/api/plugin';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { getAppFolderPath } from '@/web/core/app/api/app';
import FolderPath from '@/components/common/folder/Path';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../context';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useMemoizedFn } from 'ahooks';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { workflowStartNodeId } from '@/web/core/app/constants';
import ConfigToolModal from './ConfigToolModal';
type Props = {
selectedTools: FlowNodeTemplateType[];
chatConfig: AppSimpleEditFormType['chatConfig'];
selectedModel: LLMModelItemType;
onAddTool: (tool: FlowNodeTemplateType) => void;
onRemoveTool: (tool: NodeTemplateListItemType) => void;
};
export const childAppSystemKey: string[] = [
NodeInputKeyEnum.forbidStream,
NodeInputKeyEnum.history,
NodeInputKeyEnum.historyMaxAmount,
NodeInputKeyEnum.userChatInput
];
enum TemplateTypeEnum {
'systemPlugin' = 'systemPlugin',
'teamPlugin' = 'teamPlugin'
}
const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const [templateType, setTemplateType] = useState(TemplateTypeEnum.systemPlugin);
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const {
data: templates = [],
runAsync: loadTemplates,
loading: isLoading
} = useRequest2(
async ({
type = templateType,
parentId = '',
searchVal = searchKey
}: {
type?: TemplateTypeEnum;
parentId?: ParentIdType;
searchVal?: string;
}) => {
if (type === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates({ parentId, searchKey: searchVal });
} else if (type === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey: searchVal
}).then((res) => res.filter((app) => app.id !== appDetail._id));
}
},
{
onSuccess(_, [{ type = templateType, parentId = '' }]) {
setTemplateType(type);
setParentId(parentId);
},
refreshDeps: [templateType, searchKey, parentId],
errorToast: t('common:core.module.templates.Load plugin error')
}
);
const { data: paths = [] } = useRequest2(
() => {
if (templateType === TemplateTypeEnum.teamPlugin) return getAppFolderPath(parentId);
return getSystemPluginPaths(parentId);
},
{
manual: false,
refreshDeps: [parentId]
}
);
const onUpdateParentId = useCallback(
(parentId: ParentIdType) => {
loadTemplates({
parentId
});
},
[loadTemplates]
);
useRequest2(() => loadTemplates({ searchVal: searchKey }), {
manual: false,
throttleWait: 300,
refreshDeps: [searchKey]
});
return (
<MyModal
isOpen
title={t('common:core.app.Tool call')}
iconSrc="core/app/toolCall"
onClose={onClose}
maxW={['90vw', '700px']}
w={'700px'}
h={['90vh', '80vh']}
>
{/* Header: row and search */}
<Box px={[3, 6]} pt={4} display={'flex'} justifyContent={'space-between'} w={'full'}>
<FillRowTabs
list={[
{
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
}
]}
py={'5px'}
px={'15px'}
value={templateType}
onChange={(e) =>
loadTemplates({
type: e as TemplateTypeEnum,
parentId: null
})
}
/>
<Box w={300}>
<SearchInput
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={
templateType === TemplateTypeEnum.systemPlugin
? t('common:plugin.Search plugin')
: t('app:search_app')
}
/>
</Box>
</Box>
{/* route components */}
{!searchKey && parentId && (
<Flex mt={2} px={[3, 6]}>
<FolderPath paths={paths} FirstPathDom={null} onClick={() => onUpdateParentId(null)} />
</Flex>
)}
<MyBox isLoading={isLoading} mt={2} px={[3, 6]} pb={3} flex={'1 0 0'} overflowY={'auto'}>
<RenderList
templates={templates}
type={templateType}
setParentId={onUpdateParentId}
{...props}
/>
</MyBox>
</MyModal>
);
};
export default React.memo(ToolSelectModal);
const RenderList = React.memo(function RenderList({
templates,
type,
onAddTool,
onRemoveTool,
setParentId,
selectedTools,
chatConfig,
selectedModel
}: Props & {
templates: NodeTemplateListItemType[];
type: TemplateTypeEnum;
setParentId: (parentId: ParentIdType) => any;
}) {
const { t } = useTranslation();
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
const onCloseConfigTool = useCallback(() => setConfigTool(undefined), []);
const { toast } = useToast();
const { runAsync: onClickAdd, loading: isLoading } = useRequest2(
async (template: NodeTemplateListItemType) => {
const res = await getPreviewPluginNode({ appId: template.id });
/* Invalid plugin check
1. Reference type. but not tool description;
2. Has dataset select
3. Has dynamic external data
*/
const oneFileInput =
res.inputs.filter((input) =>
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
).length === 1;
const canUploadFile =
chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg;
const invalidFileInput = oneFileInput && !!canUploadFile;
if (
res.inputs.some(
(input) =>
(input.renderTypeList.length === 1 &&
input.renderTypeList[0] === FlowNodeInputTypeEnum.reference &&
!input.toolDescription) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) ||
(input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !invalidFileInput)
)
) {
return toast({
title: t('app:simple_tool_tips'),
status: 'warning'
});
}
// 判断是否可以直接添加工具,满足以下任一条件:
// 1. 有工具描述
// 2. 是模型选择类型
// 3. 是文件上传类型且:已开启文件上传、非必填、只有一个文件上传输入
const hasInputForm =
res.inputs.length > 0 &&
res.inputs.some((input) => {
if (input.toolDescription) {
return false;
}
if (input.key === NodeInputKeyEnum.forbidStream) {
return false;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.input)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.textarea)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.numberInput)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.switch)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.select)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.JSONEditor)) {
return true;
}
return false;
});
// 构建默认表单数据
const defaultForm = {
...res,
inputs: res.inputs.map((input) => {
// 如果是模型选择类型,使用当前选中的模型
// if (input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel)) {
// return {
// ...input,
// value: selectedModel.model
// };
// }
// 如果是文件上传类型,设置为从工作流开始节点获取用户文件
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
return {
...input,
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
};
}
return input;
})
};
if (hasInputForm) {
setConfigTool(defaultForm);
} else {
onAddTool(defaultForm);
}
},
{
errorToast: t('common:core.module.templates.Load plugin error')
}
);
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
manual: false
});
const formatTemplatesArray = useMemo(() => {
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)
};
});
}
return [
{
list: [
{
list: templates,
type: '',
label: ''
}
],
label: ''
}
];
})();
return data.filter(({ list }) => list.length > 0);
}, [pluginGroups, templates, type]);
const gridStyle = useMemo(() => {
if (type === TemplateTypeEnum.teamPlugin) {
return {
gridTemplateColumns: ['1fr', '1fr'],
py: 2,
avatarSize: '2rem'
};
}
return {
gridTemplateColumns: ['1fr', '1fr 1fr'],
py: 3,
avatarSize: '1.75rem'
};
}, [type]);
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'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2} columnGap={3}>
{item.list.map((template) => {
const selected = selectedTools.some((tool) => tool.pluginId === template.id);
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>
{type === TemplateTypeEnum.systemPlugin && (
<CostTooltip
cost={template.currentCost}
hasTokenFee={template.hasTokenFee}
/>
)}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
whiteSpace={'nowrap'}
overflow={'hidden'}
textOverflow={'ellipsis'}
>
<MyAvatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
flexShrink={0}
/>
<Box
color={'myGray.900'}
fontWeight={'500'}
fontSize={'sm'}
flex={'1 0 0'}
ml={3}
className="textEllipsis"
>
{t(template.name as any)}
</Box>
{selected ? (
<Button
size={'sm'}
variant={'grayDanger'}
leftIcon={<MyIcon name={'delete'} w={'16px'} mr={-1} />}
onClick={() => onRemoveTool(template)}
px={2}
fontSize={'mini'}
>
{t('common:common.Remove')}
</Button>
) : template.isFolder ? (
<Button
size={'sm'}
variant={'whiteBase'}
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
onClick={() => setParentId(template.id)}
px={2}
fontSize={'mini'}
>
{t('common:common.Open')}
</Button>
) : (
<Button
size={'sm'}
variant={'primaryOutline'}
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
isLoading={isLoading}
onClick={() => onClickAdd(template)}
px={2}
fontSize={'mini'}
>
{t('common:common.Add')}
</Button>
)}
</Flex>
</MyTooltip>
);
})}
</Grid>
</Box>
);
})}
</>
);
});
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'}>
<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>
{!!configTool && (
<ConfigToolModal
configTool={configTool}
onCloseConfigTool={onCloseConfigTool}
onAddTool={onAddTool}
/>
)}
</Box>
);
});

View File

@@ -1,133 +0,0 @@
import React, { useState } from 'react';
import { appWorkflow2Form, getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import Header from './Header';
import Edit from './Edit';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import dynamic from 'next/dynamic';
import { Box, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { SimpleAppSnapshotType, useSimpleAppSnapshots } from './useSnapshots';
import { useDebounceEffect, useMount } from 'ahooks';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { getAppConfigByDiff } from '@/web/core/app/diff';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const SimpleEdit = () => {
const { t } = useTranslation();
const { loadAllDatasets } = useDatasetStore();
const { currentTab, appDetail } = useContextSelector(AppContext, (v) => v);
const { forbiddenSaveSnapshot, past, setPast, saveSnapshot } = useSimpleAppSnapshots(
appDetail._id
);
const [appForm, setAppForm] = useState(getDefaultAppForm());
// Init app form
useMount(() => {
// show selected dataset
loadAllDatasets();
if (appDetail.version !== 'v2') {
return setAppForm(
appWorkflow2Form({
nodes: v1Workflow2V2((appDetail.modules || []) as any)?.nodes,
chatConfig: appDetail.chatConfig
})
);
}
// 读取旧的存储记录
const pastSnapshot = (() => {
try {
const pastSnapshot = localStorage.getItem(`${appDetail._id}-past`);
return pastSnapshot ? (JSON.parse(pastSnapshot) as SimpleAppSnapshotType[]) : [];
} catch (error) {
return [];
}
})();
const defaultState = pastSnapshot?.[pastSnapshot.length - 1]?.state;
if (pastSnapshot?.[0]?.diff && defaultState) {
setPast(
pastSnapshot
.map((item) => {
if (!item.state && !item.diff) return;
if (!item.diff) {
return {
title: t('app:initial_form'),
isSaved: true,
appForm: defaultState
};
}
const currentState = getAppConfigByDiff(defaultState, item.diff);
return {
title: item.title,
isSaved: item.isSaved,
appForm: currentState
};
})
.filter(Boolean) as SimpleAppSnapshotType[]
);
const pastState = getAppConfigByDiff(defaultState, pastSnapshot[0].diff);
localStorage.removeItem(`${appDetail._id}-past`);
return setAppForm(pastState);
}
// 无旧的记录,正常初始化
if (past.length === 0) {
const appForm = appWorkflow2Form({
nodes: appDetail.modules,
chatConfig: appDetail.chatConfig
});
saveSnapshot({
appForm,
title: t('app:initial_form'),
isSaved: true
});
setAppForm(appForm);
} else {
setAppForm(past[0].appForm);
}
});
// Save snapshot to local
useDebounceEffect(
() => {
saveSnapshot({
appForm
});
},
[appForm],
{ wait: 500 }
);
return (
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
<Header
appForm={appForm}
forbiddenSaveSnapshot={forbiddenSaveSnapshot}
setAppForm={setAppForm}
past={past}
setPast={setPast}
saveSnapshot={saveSnapshot}
/>
{currentTab === TabEnum.appEdit ? (
<Edit appForm={appForm} setAppForm={setAppForm} setPast={setPast} />
) : (
<Box flex={'1 0 0'} h={0} mt={[4, 0]}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Box>
)}
</Flex>
);
};
export default React.memo(SimpleEdit);

View File

@@ -1,10 +0,0 @@
.EditAppBox {
&::-webkit-scrollbar-thumb {
background: #dfe2ea !important;
transition: background 1s;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--chakra-colors-gray-300) !important;
}
}

View File

@@ -1,99 +0,0 @@
import { useMemoizedFn } from 'ahooks';
import { useRef, useState } from 'react';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { isEqual } from 'lodash';
export type SimpleAppSnapshotType = {
title: string;
isSaved?: boolean;
appForm: AppSimpleEditFormType;
// abandon
state?: AppSimpleEditFormType;
diff?: Record<string, any>;
};
export type onSaveSnapshotFnType = (props: {
appForm: AppSimpleEditFormType; // Current edited app form data
title?: string;
isSaved?: boolean;
}) => Promise<boolean>;
export const compareSimpleAppSnapshot = (
appForm1?: AppSimpleEditFormType,
appForm2?: AppSimpleEditFormType
) => {
if (
appForm1?.chatConfig &&
appForm2?.chatConfig &&
!isEqual(
{
welcomeText: appForm1.chatConfig?.welcomeText || '',
variables: appForm1.chatConfig?.variables || [],
questionGuide: appForm1.chatConfig?.questionGuide || undefined,
ttsConfig: appForm1.chatConfig?.ttsConfig || undefined,
whisperConfig: appForm1.chatConfig?.whisperConfig || undefined,
chatInputGuide: appForm1.chatConfig?.chatInputGuide || undefined,
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined
},
{
welcomeText: appForm2.chatConfig?.welcomeText || '',
variables: appForm2.chatConfig?.variables || [],
questionGuide: appForm2.chatConfig?.questionGuide || undefined,
ttsConfig: appForm2.chatConfig?.ttsConfig || undefined,
whisperConfig: appForm2.chatConfig?.whisperConfig || undefined,
chatInputGuide: appForm2.chatConfig?.chatInputGuide || undefined,
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined
}
)
) {
console.log('chatConfig not equal');
return false;
}
return isEqual({ ...appForm1, chatConfig: undefined }, { ...appForm2, chatConfig: undefined });
};
export const useSimpleAppSnapshots = (appId: string) => {
const forbiddenSaveSnapshot = useRef(false);
const [past, setPast] = useState<SimpleAppSnapshotType[]>([]);
const saveSnapshot: onSaveSnapshotFnType = useMemoizedFn(async ({ appForm, title, isSaved }) => {
if (forbiddenSaveSnapshot.current) {
forbiddenSaveSnapshot.current = false;
return false;
}
if (past.length === 0) {
setPast([
{
title: title || formatTime2YMDHMS(new Date()),
isSaved,
appForm
}
]);
return true;
}
const pastState = past[0];
const isPastEqual = compareSimpleAppSnapshot(pastState?.appForm, appForm);
if (isPastEqual) return false;
setPast((past) => [
{
appForm,
title: title || formatTime2YMDHMS(new Date()),
isSaved
},
...past.slice(0, 99)
]);
return true;
});
return { forbiddenSaveSnapshot, past, setPast, saveSnapshot };
};
export default function Snapshots() {
return <></>;
}

View File

@@ -1,143 +0,0 @@
import React, { useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import {
Button,
Flex,
Box,
ModalFooter,
ModalBody,
Menu,
MenuButton,
HStack,
Tag,
TagCloseButton,
MenuList,
Input,
MenuOptionGroup,
MenuItemOption,
TagLabel
} from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { getTeamsTags } from '@/web/support/user/team/api';
import { useQuery } from '@tanstack/react-query';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
const TagsEditModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { appDetail, updateAppDetail } = useContextSelector(AppContext, (v) => v);
const [selectedTags, setSelectedTags] = useState<string[]>(appDetail?.teamTags || []);
// submit config
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
mutationFn: async () => {
await updateAppDetail({
teamTags: selectedTags
});
},
onSuccess() {
onClose();
toast({
title: t('common:common.Update Success'),
status: 'success'
});
},
errorToast: t('common:common.Update Failed')
});
const { data: teamTags = [] } = useQuery(['getTeamsTags'], getTeamsTags);
const [searchKey, setSearchKey] = useState('');
const filterTeamTags = teamTags.filter((item) => {
return item.label.includes(searchKey);
}, []);
return (
<MyModal
style={{ width: '900px' }}
isOpen
onClose={onClose}
iconSrc="/imgs/workflow/ai.svg"
title={t('common:core.app.Team tags')}
>
<ModalBody>
<Box mb={3} fontWeight="semibold">
{t('common:team_tag')}
</Box>
<Menu closeOnSelect={false}>
<MenuButton className="menu-btn" maxHeight={'250'} w={'100%'}>
<Flex
alignItems={'center'}
borderWidth={'1px'}
borderColor={'borderColor.base'}
borderRadius={'md'}
px={3}
py={2}
flexWrap={'wrap'}
minH={'50px'}
gap={3}
>
{teamTags.map((item, index) => {
const key: string = item?.key;
if (selectedTags.indexOf(key as never) > -1) {
return (
<Tag key={index} size={'md'} colorScheme="blue" borderRadius="full">
<TagLabel>{item.label}</TagLabel>
<TagCloseButton />
</Tag>
);
}
})}
</Flex>
</MenuButton>
<MenuList>
<Box px={2}>
<Input
m={'auto'}
placeholder={t('common:core.app.Search team tags')}
value={searchKey}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
</Box>
<Box maxH={'300px'} overflow={'auto'} mt={1}>
<MenuOptionGroup
defaultValue={selectedTags}
type="checkbox"
onChange={(e) => {
//@ts-ignore
setSelectedTags(e);
}}
>
{filterTeamTags.map((item) => {
return (
<MenuItemOption
key={item.key}
value={item.key}
borderRadius={'md'}
_hover={{ bg: 'myGray.100' }}
>
{item?.label}
</MenuItemOption>
);
})}
</MenuOptionGroup>
</Box>
</MenuList>
</Menu>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button isLoading={btnLoading} onClick={(e) => saveSubmitSuccess(e)}>
{t('common:common.Save')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default TagsEditModal;

View File

@@ -1,278 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
Box,
Flex,
Button,
IconButton,
HStack,
ModalBody,
ModalFooter,
useDisclosure
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../WorkflowComponents/context';
import { AppContext, TabEnum } from '../context';
import RouteTab from '../RouteTab';
import { useRouter } from 'next/router';
import AppCard from '../WorkflowComponents/AppCard';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import SaveButton from './components/SaveButton';
import PublishHistories from '../PublishHistoriesSlider';
import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext';
import { WorkflowStatusContext } from '../WorkflowComponents/context/workflowStatusContext';
const Header = () => {
const { t } = useTranslation();
const { isPc } = useSystem();
const router = useRouter();
const { toast: backSaveToast } = useToast({
containerStyle: {
mt: '60px'
}
});
const { appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v);
const isV2Workflow = appDetail?.version === 'v2';
const {
isOpen: isOpenBackConfirm,
onOpen: onOpenBackConfirm,
onClose: onCloseBackConfirm
} = useDisclosure();
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
const flowData2StoreDataAndCheck = useContextSelector(
WorkflowContext,
(v) => v.flowData2StoreDataAndCheck
);
const setWorkflowTestData = useContextSelector(WorkflowContext, (v) => v.setWorkflowTestData);
const past = useContextSelector(WorkflowContext, (v) => v.past);
const setPast = useContextSelector(WorkflowContext, (v) => v.setPast);
const onSwitchTmpVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchTmpVersion);
const onSwitchCloudVersion = useContextSelector(WorkflowContext, (v) => v.onSwitchCloudVersion);
const showHistoryModal = useContextSelector(WorkflowEventContext, (v) => v.showHistoryModal);
const setShowHistoryModal = useContextSelector(
WorkflowEventContext,
(v) => v.setShowHistoryModal
);
const isSaved = useContextSelector(WorkflowStatusContext, (v) => v.isSaved);
const leaveSaveSign = useContextSelector(WorkflowStatusContext, (v) => v.leaveSaveSign);
const { lastAppListRouteType } = useSystemStore();
const { runAsync: onClickSave, loading } = useRequest2(
async ({
isPublish,
versionName = formatTime2YMDHMS(new Date())
}: {
isPublish?: boolean;
versionName?: string;
}) => {
const data = flowData2StoreData();
if (data) {
await onSaveApp({
...data,
isPublish,
versionName,
chatConfig: appDetail.chatConfig,
//@ts-ignore
version: 'v2'
});
// Mark the current snapshot as saved
setPast((prevPast) =>
prevPast.map((item, index) =>
index === 0
? {
...item,
isSaved: true
}
: item
)
);
}
}
);
const onBack = useCallback(async () => {
leaveSaveSign.current = false;
router.push({
pathname: '/app/list',
query: {
parentId: appDetail.parentId,
type: lastAppListRouteType
}
});
}, [appDetail.parentId, lastAppListRouteType, leaveSaveSign, router]);
const Render = useMemo(() => {
return (
<>
{!isPc && (
<Flex pt={2} justifyContent={'center'}>
<RouteTab />
</Flex>
)}
<Flex
mt={[2, 0]}
pl={[2, 4]}
pr={[2, 6]}
borderBottom={'base'}
alignItems={['flex-start', 'center']}
userSelect={'none'}
h={['auto', '67px']}
flexWrap={'wrap'}
{...(currentTab === TabEnum.appEdit
? {
bg: 'myGray.25'
}
: {
bg: 'transparent',
borderBottomColor: 'transparent'
})}
>
{/* back */}
<Box
_hover={{
bg: 'myGray.200'
}}
p={0.5}
borderRadius={'sm'}
>
<MyIcon
name={'common/leftArrowLight'}
w={6}
cursor={'pointer'}
onClick={isSaved ? onBack : onOpenBackConfirm}
/>
</Box>
{/* app info */}
<Box ml={1}>
<AppCard isSaved={isSaved} showSaveStatus={isV2Workflow} />
</Box>
{isPc && (
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
<RouteTab />
</Box>
)}
<Box flex={1} />
{currentTab === TabEnum.appEdit && (
<HStack flexDirection={['column', 'row']} spacing={[2, 3]}>
{!showHistoryModal && (
<IconButton
icon={<MyIcon name={'history'} w={'18px'} />}
aria-label={''}
size={'sm'}
w={'30px'}
variant={'whitePrimary'}
onClick={() => {
setShowHistoryModal(true);
}}
/>
)}
<Button
size={'sm'}
leftIcon={<MyIcon name={'core/workflow/debug'} w={['14px', '16px']} />}
variant={'whitePrimary'}
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (data) {
setWorkflowTestData(data);
}
}}
>
{t('common:core.workflow.Run')}
</Button>
{!showHistoryModal && (
<SaveButton
isLoading={loading}
onClickSave={onClickSave}
checkData={flowData2StoreDataAndCheck}
/>
)}
</HStack>
)}
</Flex>
</>
);
}, [
isPc,
currentTab,
isSaved,
onBack,
onOpenBackConfirm,
isV2Workflow,
showHistoryModal,
t,
loading,
onClickSave,
flowData2StoreDataAndCheck,
setShowHistoryModal,
setWorkflowTestData
]);
return (
<>
{Render}
{showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && (
<PublishHistories
onClose={() => {
setShowHistoryModal(false);
}}
past={past}
onSwitchCloudVersion={onSwitchCloudVersion}
onSwitchTmpVersion={onSwitchTmpVersion}
/>
)}
<MyModal
isOpen={isOpenBackConfirm}
onClose={onCloseBackConfirm}
iconSrc="common/warn"
title={t('common:common.Exit')}
w={'400px'}
>
<ModalBody>
<Box>{t('workflow:workflow.exit_tips')}</Box>
</ModalBody>
<ModalFooter gap={3}>
<Button variant={'whiteDanger'} onClick={onBack}>
{t('common:common.Exit Directly')}
</Button>
<Button
isLoading={loading}
onClick={async () => {
try {
await onClickSave({});
onCloseBackConfirm();
onBack();
backSaveToast({
status: 'success',
title: t('app:saved_success'),
position: 'top-right'
});
} catch (error) {}
}}
>
{t('common:common.Save_and_exit')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default React.memo(Header);

View File

@@ -1,121 +0,0 @@
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import React, { useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useToast } from '@fastgpt/web/hooks/useToast';
import SaveAndPublishModal from '../../WorkflowComponents/Flow/components/SaveAndPublish';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
const SaveButton = ({
isLoading,
onClickSave,
checkData
}: {
isLoading: boolean;
onClickSave: (options: { isPublish?: boolean; versionName?: string }) => Promise<void>;
checkData?: (hideTip?: boolean) =>
| {
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
}
| undefined;
}) => {
const { t } = useTranslation();
const [isSave, setIsSave] = useState(false);
const { toast } = useToast({
containerStyle: {
mt: '60px',
fontSize: 'sm'
}
});
const {
isOpen: isSaveAndPublishModalOpen,
onOpen: onSaveAndPublishModalOpen,
onClose: onSaveAndPublishModalClose
} = useDisclosure();
return (
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
onOpenFunc={() => setIsSave(true)}
onCloseFunc={() => setIsSave(false)}
trigger={'hover'}
Trigger={
<Button
size={'sm'}
rightIcon={
<MyIcon
name={isSave ? 'core/chat/chevronUp' : 'core/chat/chevronDown'}
w={['14px', '16px']}
/>
}
>
<Box>{t('common:common.Save')}</Box>
</Button>
}
>
{({ onClose }) => (
<Box p={1.5}>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
isLoading={isLoading}
onClick={async () => {
await onClickSave({});
toast({
status: 'success',
title: t('app:saved_success'),
position: 'top-right',
isClosable: true
});
onClose();
setIsSave(false);
}}
>
<MyIcon name={'core/workflow/upload'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('common:core.workflow.Save to cloud')}</Box>
</MyBox>
<Flex
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={() => {
const canOpen = !checkData || checkData();
if (canOpen) {
onSaveAndPublishModalOpen();
}
onClose();
setIsSave(false);
}}
>
<MyIcon name={'core/workflow/publish'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('common:core.workflow.Save and publish')}</Box>
{isSaveAndPublishModalOpen && (
<SaveAndPublishModal
isLoading={isLoading}
onClose={onSaveAndPublishModalClose}
onClickSave={onClickSave}
/>
)}
</Flex>
</Box>
)}
</MyPopover>
);
};
export default React.memo(SaveButton);

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { appSystemModuleTemplates } from '@fastgpt/global/core/workflow/template/constants';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { WorkflowContext } from '../WorkflowComponents/context';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import { useMount } from 'ahooks';
import Header from './Header';
import { Flex } from '@chakra-ui/react';
import { workflowBoxStyles } from '../constants';
import dynamic from 'next/dynamic';
import { cloneDeep } from 'lodash';
import { useTranslation } from 'next-i18next';
import Flow from '../WorkflowComponents/Flow';
import { ReactFlowCustomProvider } from '../WorkflowComponents/context/index';
const Logs = dynamic(() => import('../Logs/index'));
const PublishChannel = dynamic(() => import('../Publish'));
const WorkflowEdit = () => {
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const currentTab = useContextSelector(AppContext, (v) => v.currentTab);
const isV2Workflow = appDetail?.version === 'v2';
const { t } = useTranslation();
const { openConfirm, ConfirmModal } = useConfirm({
showCancel: false,
content: t('common:info.old_version_attention')
});
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
useMount(() => {
if (!isV2Workflow) {
openConfirm(() => {
initData(JSON.parse(JSON.stringify(v1Workflow2V2((appDetail.modules || []) as any))), true);
})();
} else {
initData(
cloneDeep({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
}),
true
);
}
});
return (
<Flex {...workflowBoxStyles}>
<Header />
{currentTab === TabEnum.appEdit ? (
<Flow />
) : (
<Flex flexDirection={'column'} h={'100%'} px={4} pb={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
)}
{!isV2Workflow && <ConfirmModal countDown={0} />}
</Flex>
);
};
const Render = () => {
return (
<ReactFlowCustomProvider templates={appSystemModuleTemplates}>
<WorkflowEdit />
</ReactFlowCustomProvider>
);
};
export default Render;

View File

@@ -1,215 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, HStack, useDisclosure } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { WorkflowContext } from './context';
import dynamic from 'next/dynamic';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
const ExportConfigPopover = dynamic(
() => import('@/pageComponents/app/detail/ExportConfigPopover')
);
const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved: boolean }) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
const onOpenTeamTagModal = useContextSelector(AppContext, (v) => v.onOpenTeamTagModal);
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
const InfoMenu = useCallback(
({ children }: { children: React.ReactNode }) => {
return (
<MyPopover
placement={'bottom-end'}
hasArrow={false}
offset={[2, 4]}
w={'116px'}
trigger={'hover'}
Trigger={children}
>
{({ onClose }) => (
<Box p={1.5}>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'edit'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:edit_info')}</Box>
</MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'key'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Role_setting')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenImport}
>
<MyIcon name={'common/importLight'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:import_configs')}</Box>
</MyBox>
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
>
<ExportConfigPopover
chatConfig={appDetail.chatConfig}
appName={appDetail.name}
getWorkflowData={flowData2StoreData}
/>
</MyBox>
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
_hover={{ color: 'primary.600', bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onOpenTeamTagModal}
>
<MyIcon name={'core/dataset/tag'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Team_Tags')}</Box>
</MyBox>
</>
)}
{appDetail.permission.isOwner && (
<>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />
<MyBox
display={'flex'}
size={'md'}
px={1}
py={1.5}
rounded={'4px'}
color={'red.600'}
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
cursor={'pointer'}
onClick={onDelApp}
>
<MyIcon name={'delete'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('common:common.Delete')}</Box>
</MyBox>
</>
)}
</Box>
)}
</MyPopover>
);
},
[
appDetail.chatConfig,
appDetail.name,
appDetail.permission.hasWritePer,
appDetail.permission.isOwner,
feConfigs?.show_team_chat,
flowData2StoreData,
onDelApp,
onOpenImport,
onOpenInfoEdit,
onOpenTeamTagModal,
t
]
);
const Render = useMemo(() => {
return (
<HStack>
<Avatar src={appDetail.avatar} w={'1.75rem'} borderRadius={'md'} />
<Box>
<InfoMenu>
<HStack
spacing={1}
cursor={'pointer'}
pl={1}
ml={-1}
borderRadius={'xs'}
_hover={{ bg: 'myGray.150' }}
>
<Box color={'myGray.900'}>{appDetail.name}</Box>
<MyIcon name={'common/select'} w={'1rem'} color={'myGray.500'} />
</HStack>
</InfoMenu>
{showSaveStatus && (
<Flex alignItems={'center'} fontSize={'mini'} lineHeight={1}>
<MyTag
py={0}
px={1}
showDot
bg={'transparent'}
colorSchema={
isSaved
? publishStatusStyle.published.colorSchema
: publishStatusStyle.unPublish.colorSchema
}
>
{t(isSaved ? publishStatusStyle.published.text : publishStatusStyle.unPublish.text)}
</MyTag>
</Flex>
)}
</Box>
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
</HStack>
);
}, [
InfoMenu,
appDetail.avatar,
appDetail.name,
isOpenImport,
isSaved,
onCloseImport,
showSaveStatus,
t
]);
return Render;
};
export default AppCard;

View File

@@ -1,174 +0,0 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import React, { useMemo } from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import { useChatTest } from '@/pages/app/detail/components/useChatTest';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, {
ChatRecordContext
} from '@/web/core/chat/context/chatRecordContext';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
type Props = {
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
};
const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const isPlugin = appDetail.type === AppTypeEnum.plugin;
const { restartChat, ChatContainer, loading } = useChatTest({
nodes,
edges,
chatConfig: appDetail.chatConfig,
isReady: isOpen
});
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
return (
<>
<Box
zIndex={300}
display={isOpen ? 'block' : 'none'}
position={'fixed'}
top={0}
left={0}
bottom={0}
right={0}
onClick={onClose}
/>
<MyBox
isLoading={loading}
zIndex={300}
display={'flex'}
flexDirection={'column'}
position={'absolute'}
top={5}
right={0}
h={isOpen ? '95%' : '0'}
w={isOpen ? ['100%', '460px'] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'md'}
overflow={'hidden'}
transition={'.2s ease'}
>
{isPlugin ? (
<Flex
alignItems={'flex-start'}
justifyContent={'space-between'}
px={3}
pt={3}
bg={'myGray.25'}
borderBottom={'base'}
>
<LightRowTabs<PluginRunBoxTabEnum>
list={[
{ label: t('common:common.Input'), value: PluginRunBoxTabEnum.input },
...(chatRecords.length > 0
? [
{ label: t('common:common.Output'), value: PluginRunBoxTabEnum.output },
{ label: t('common:common.all_result'), value: PluginRunBoxTabEnum.detail }
]
: [])
]}
value={pluginRunTab}
onChange={setPluginRunTab}
inlineStyles={{ px: 0.5, pb: 2 }}
gap={5}
py={0}
fontSize={'sm'}
/>
<CloseIcon mt={1} onClick={onClose} />
</Flex>
) : (
<Flex
py={2.5}
px={5}
whiteSpace={'nowrap'}
bg={'myGray.25'}
borderBottom={'1px solid #F4F4F7'}
>
<Flex fontSize={'16px'} fontWeight={'bold'} flex={1} alignItems={'center'}>
<MyIcon name={'common/paused'} w={'14px'} mr={2.5} />
{t('common:core.chat.Run test')}
</Flex>
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
size={'smSquare'}
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
variant={'whiteDanger'}
borderRadius={'md'}
aria-label={'delete'}
onClick={restartChat}
/>
</MyTooltip>
<MyTooltip label={t('common:common.Close')}>
<IconButton
ml={4}
icon={<SmallCloseIcon fontSize={'22px'} />}
variant={'grayBase'}
size={'smSquare'}
aria-label={''}
onClick={onClose}
bg={'none'}
/>
</MyTooltip>
</Flex>
)}
<Box flex={'1 0 0'} overflow={'auto'}>
<ChatContainer />
</Box>
</MyBox>
</>
);
};
const Render = (Props: Props) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const chatRecordProviderParams = useMemo(
() => ({
chatId: chatId,
appId: appDetail._id
}),
[appDetail._id, chatId]
);
return (
<ChatItemContextProvider
showRouteToAppDetail={true}
showRouteToDatasetDetail={true}
isShowReadRawSource={true}
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest {...Props} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
};
export default React.memo(Render);

View File

@@ -1,68 +0,0 @@
import React, { useState } from 'react';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
const ImportAppConfigEditor = dynamic(() => import('@/pageComponents/app/ImportAppConfigEditor'), {
ssr: false
});
type Props = {
onClose: () => void;
};
const ImportSettings = ({ onClose }: Props) => {
const { toast } = useToast();
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
const { t } = useTranslation();
const [value, setValue] = useState('');
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="common/importLight"
iconColor="primary.600"
title={t('app:import_configs')}
size={'md'}
>
<ModalBody>
<ImportAppConfigEditor value={value} onChange={setValue} rows={16} />
</ModalBody>
<ModalFooter justifyItems={'flex-end'}>
<Button
px={5}
py={2}
onClick={async () => {
if (!value) {
return onClose();
}
try {
const data = JSON.parse(value);
await initData(data);
toast({
title: t('app:import_configs_success'),
status: 'success'
});
onClose();
} catch (error) {
toast({
title: t('app:import_configs_failed')
});
}
}}
fontWeight={'500'}
>
{t('common:common.Save')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ImportSettings);

View File

@@ -1,758 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Flex,
Grid,
HStack,
IconButton,
Input,
InputGroup,
InputLeftElement,
css
} from '@chakra-ui/react';
import type {
NodeTemplateListItemType,
NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import { useReactFlow, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
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';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { workflowNodeTemplateList } from '@fastgpt/web/core/workflow/constants';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import FolderPath from '@/components/common/folder/Path';
import { getAppFolderPath } from '@/web/core/app/api/app';
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 { 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;
onClose: () => void;
parentId: ParentIdType;
setParentId: (parenId: ParentIdType) => any;
};
enum TemplateTypeEnum {
'basic' = 'basic',
'systemPlugin' = 'systemPlugin',
'teamPlugin' = 'teamPlugin'
}
const sliderWidth = 460;
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const { feConfigs } = useSystemStore();
const basicNodeTemplates = useContextSelector(WorkflowContext, (v) => v.basicNodeTemplates);
const hasToolNode = useContextSelector(WorkflowContext, (v) => v.hasToolNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const appId = useContextSelector(WorkflowContext, (v) => v.appId);
const [templateType, setTemplateType] = useState(TemplateTypeEnum.basic);
const { data: basicNodes } = useRequest2(
async () => {
if (templateType === TemplateTypeEnum.basic) {
return basicNodeTemplates
.filter((item) => {
// unique node filter
if (item.unique) {
const nodeExist = nodeList.some((node) => node.flowNodeType === item.flowNodeType);
if (nodeExist) {
return false;
}
}
// special node filter
if (item.flowNodeType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
return false;
}
// tool stop or tool params
if (
!hasToolNode &&
(item.flowNodeType === FlowNodeTypeEnum.stopTool ||
item.flowNodeType === FlowNodeTypeEnum.toolParams)
) {
return false;
}
return true;
})
.map<NodeTemplateListItemType>((item) => ({
id: item.id,
flowNodeType: item.flowNodeType,
templateType: item.templateType,
avatar: item.avatar,
name: item.name,
intro: item.intro
}));
}
},
{
manual: false,
throttleWait: 100,
refreshDeps: [basicNodeTemplates, nodeList, hasToolNode, templateType]
}
);
const {
data: teamAndSystemApps,
loading: isLoading,
runAsync: loadNodeTemplates
} = useRequest2(
async ({
parentId = '',
type = templateType,
searchVal = searchKey
}: {
parentId?: ParentIdType;
type?: TemplateTypeEnum;
searchVal?: string;
}) => {
if (type === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey: searchVal
}).then((res) => res.filter((app) => app.id !== appId));
}
if (type === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates({
searchKey: searchVal,
parentId
});
}
},
{
onSuccess(res, [{ parentId = '', type = templateType }]) {
setParentId(parentId);
setTemplateType(type);
},
refreshDeps: [searchKey, templateType]
}
);
const templates = useMemo(
() => basicNodes || teamAndSystemApps || [],
[basicNodes, teamAndSystemApps]
);
const onUpdateParentId = useCallback(
(parentId: ParentIdType) => {
loadNodeTemplates({
parentId
});
},
[loadNodeTemplates]
);
// Init load refresh templates
useRequest2(
() =>
loadNodeTemplates({
parentId: '',
searchVal: searchKey
}),
{
manual: false,
throttleWait: 300,
refreshDeps: [searchKey]
}
);
return (
<>
<Box
zIndex={2}
display={isOpen ? 'block' : 'none'}
position={'absolute'}
top={0}
left={0}
bottom={0}
w={`${sliderWidth}px`}
maxW={'100%'}
onClick={onClose}
fontSize={'sm'}
/>
<MyBox
isLoading={isLoading}
display={'flex'}
zIndex={3}
flexDirection={'column'}
position={'absolute'}
top={'10px'}
left={0}
pt={5}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
bg={'white'}
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
borderRadius={'0 20px 20px 0'}
transition={'.2s ease'}
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
<RenderHeader
templateType={templateType}
onClose={onClose}
parentId={parentId}
onUpdateParentId={onUpdateParentId}
searchKey={searchKey}
loadNodeTemplates={loadNodeTemplates}
setSearchKey={setSearchKey}
/>
<RenderList
templates={templates}
type={templateType}
onClose={onClose}
parentId={parentId}
setParentId={onUpdateParentId}
/>
</MyBox>
</>
);
};
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,
onClose,
setParentId
}: RenderListProps) {
const { t } = useTranslation();
const { setLoading } = useSystemStore();
const { isPc } = useSystem();
const { screenToFlowPosition } = useReactFlow();
const { computedNewNodeName } = useWorkflowUtils();
const { toast } = useToast();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
manual: false
});
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
}: {
template: NodeTemplateListItemType;
position: XYPosition;
}) => {
// Load template node
const templateNode = await (async () => {
try {
// get plugin preview module
if (
template.flowNodeType === FlowNodeTypeEnum.pluginModule ||
template.flowNodeType === FlowNodeTypeEnum.appModule
) {
setLoading(true);
const res = await getPreviewPluginNode({ appId: template.id });
setLoading(false);
return res;
}
// base node
const baseTemplate = moduleTemplatesFlat.find((item) => item.id === template.id);
if (!baseTemplate) {
throw new Error('baseTemplate not found');
}
return { ...baseTemplate };
} catch (e) {
toast({
status: 'error',
title: getErrText(e, t('common:core.plugin.Get Plugin Module Detail Failed'))
});
setLoading(false);
return Promise.reject(e);
}
})();
const nodePosition = screenToFlowPosition(position);
const mouseX = nodePosition.x - 100;
const mouseY = nodePosition.y - 20;
// Add default values to some inputs
const defaultValueMap: Record<string, any> = {
[NodeInputKeyEnum.userChatInput]: undefined,
[NodeInputKeyEnum.fileUrlList]: undefined
};
nodeList.forEach((node) => {
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
defaultValueMap[NodeInputKeyEnum.userChatInput] = [
node.nodeId,
NodeOutputKeyEnum.userChatInput
];
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
[node.nodeId, NodeOutputKeyEnum.userFiles]
];
}
});
const newNode = nodeTemplate2FlowNode({
template: {
...templateNode,
name: computedNewNodeName({
templateName: t(templateNode.name as any),
flowNodeType: templateNode.flowNodeType,
pluginId: templateNode.pluginId
}),
intro: t(templateNode.intro as any),
inputs: templateNode.inputs.map((input) => ({
...input,
value: defaultValueMap[input.key] ?? input.value,
valueDesc: t(input.valueDesc as any),
label: t(input.label as any),
description: t(input.description as any),
debugLabel: t(input.debugLabel as any),
toolDescription: t(input.toolDescription as any)
})),
outputs: templateNode.outputs.map((output) => ({
...output,
valueDesc: t(output.valueDesc as any),
label: t(output.label as any),
description: t(output.description as any)
}))
},
position: { x: mouseX, y: mouseY },
selected: true,
t
});
const newNodes = [newNode];
if (templateNode.flowNodeType === FlowNodeTypeEnum.loop) {
const startNode = nodeTemplate2FlowNode({
template: LoopStartNode,
position: { x: mouseX + 60, y: mouseY + 280 },
parentNodeId: newNode.id,
t
});
const endNode = nodeTemplate2FlowNode({
template: LoopEndNode,
position: { x: mouseX + 420, y: mouseY + 680 },
parentNodeId: newNode.id,
t
});
newNodes.push(startNode, endNode);
}
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNodes);
return newState;
});
}
);
const gridStyle = useMemo(() => {
if (type === TemplateTypeEnum.teamPlugin) {
return {
gridTemplateColumns: ['1fr', '1fr'],
py: 2,
avatarSize: '2rem',
authorInName: false,
authorInRight: true
};
}
return {
gridTemplateColumns: ['1fr', '1fr 1fr'],
py: 3,
avatarSize: '1.75rem',
authorInName: true,
authorInRight: false
};
}, [type]);
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'} 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) => {
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>
{type === TemplateTypeEnum.systemPlugin && (
<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={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
flexShrink={0}
/>
<Box
color={'myGray.900'}
fontWeight={'500'}
fontSize={'sm'}
flex={'1 0 0'}
ml={3}
className="textEllipsis"
>
{t(template.name as any)}
</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>
);
});

View File

@@ -1,83 +0,0 @@
import React, { useCallback, useState } from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type';
import { useTranslation } from 'next-i18next';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import {
GetResourceFolderListProps,
GetResourceListItemResponse
} from '@fastgpt/global/common/parentFolder/type';
import { getMyApps } from '@/web/core/app/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const SelectAppModal = ({
value,
filterAppIds = [],
onClose,
onSuccess
}: {
value?: SelectAppItemType;
filterAppIds?: string[];
onClose: () => void;
onSuccess: (e: SelectAppItemType) => void;
}) => {
const { t } = useTranslation();
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
const getAppList = useCallback(
async ({ parentId }: GetResourceFolderListProps) => {
return getMyApps({
parentId,
type: [AppTypeEnum.folder, AppTypeEnum.simple, AppTypeEnum.workflow]
}).then((res) =>
res
.filter((item) => !filterAppIds.includes(item._id))
.map<GetResourceListItemResponse>((item) => ({
id: item._id,
name: item.name,
avatar: item.avatar,
isFolder: item.type === AppTypeEnum.folder
}))
);
},
[filterAppIds]
);
return (
<MyModal
isOpen
title={t('common:core.module.Select app')}
iconSrc="/imgs/workflow/ai.svg"
onClose={onClose}
position={'relative'}
w={'600px'}
>
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
<SelectOneResource
value={selectedApp?.id}
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
server={getAppList}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button
ml={2}
isDisabled={!selectedApp}
onClick={() => {
if (!selectedApp) return;
onSuccess(selectedApp);
onClose();
}}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(SelectAppModal);

View File

@@ -1,266 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useThrottleEffect } from 'ahooks';
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const ButtonEdge = (props: EdgeProps) => {
const nodes = useContextSelector(WorkflowInitContext, (v) => v.nodes);
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowDebugData = useContextSelector(WorkflowContext, (v) => v.workflowDebugData);
const hoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.hoverEdgeId);
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
selected,
source,
sourceHandleId,
target,
targetHandleId,
style
} = props;
// If parentNode is folded, the edge will not be displayed
const parentNode = useMemo(() => {
for (const node of nodeList) {
if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) {
return nodeList.find((parent) => parent.nodeId === node.parentNodeId);
}
}
return undefined;
}, [nodeList, source, target]);
const defaultZIndex = useMemo(
() => (nodeList.find((node) => node.nodeId === source && node.parentNodeId) ? 2002 : 0),
[nodeList, source]
);
const onDelConnect = useCallback(
(id: string) => {
onEdgesChange([
{
type: 'remove',
id
}
]);
},
[onEdgesChange]
);
// Selected edge or source/target node selected
const [highlightEdge, setHighlightEdge] = useState(false);
useThrottleEffect(
() => {
const connectNode = nodes.find((node) => {
return node.selected && (node.id === props.source || node.id === props.target);
});
setHighlightEdge(!!connectNode || !!selected);
},
[nodes, selected, props.source, props.target],
{
wait: 100
}
);
const [, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
const isToolEdge = sourceHandleId === NodeOutputKeyEnum.selectedTools;
const isHover = hoverEdgeId === id;
const { newTargetX, newTargetY } = useMemo(() => {
if (targetPosition === 'left') {
return {
newTargetX: targetX - 3,
newTargetY: targetY
};
}
if (targetPosition === 'right') {
return {
newTargetX: targetX + 3,
newTargetY: targetY
};
}
if (targetPosition === 'bottom') {
return {
newTargetX: targetX,
newTargetY: targetY + 3
};
}
if (targetPosition === 'top') {
return {
newTargetX: targetX,
newTargetY: targetY - 3
};
}
return {
newTargetX: targetX,
newTargetY: targetY
};
}, [targetPosition, targetX, targetY]);
const edgeColor = useMemo(() => {
const targetEdge = workflowDebugData?.runtimeEdges.find(
(edge) => edge.sourceHandle === sourceHandleId && edge.targetHandle === targetHandleId
);
if (!targetEdge) {
if (highlightEdge) return '#487FFF';
return '#94B5FF';
}
// debug mode
const colorMap = {
[RuntimeEdgeStatusEnum.active]: '#487FFF',
[RuntimeEdgeStatusEnum.waiting]: '#5E8FFF',
[RuntimeEdgeStatusEnum.skipped]: '#8A95A7'
};
return colorMap[targetEdge.status];
}, [highlightEdge, sourceHandleId, targetHandleId, workflowDebugData?.runtimeEdges]);
const memoEdgeLabel = useMemo(() => {
const arrowTransform = (() => {
if (targetPosition === 'left') {
return `translate(-85%, -47%) translate(${newTargetX}px,${newTargetY}px) rotate(0deg)`;
}
if (targetPosition === 'right') {
return `translate(-10%, -50%) translate(${newTargetX}px,${newTargetY}px) rotate(-180deg)`;
}
if (targetPosition === 'bottom') {
return `translate(-50%, -20%) translate(${newTargetX}px,${newTargetY}px) rotate(-90deg)`;
}
if (targetPosition === 'top') {
return `translate(-50%, -90%) translate(${newTargetX}px,${newTargetY}px) rotate(90deg)`;
}
})();
return (
<EdgeLabelRenderer>
<Box hidden={parentNode?.isFolded}>
<Flex
display={isHover || highlightEdge ? 'flex' : 'none'}
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={`translate(-55%, -50%) translate(${labelX}px,${labelY}px)`}
pointerEvents={'all'}
w={'18px'}
h={'18px'}
bg={'white'}
borderRadius={'18px'}
cursor={'pointer'}
zIndex={defaultZIndex + 1000}
onClick={() => onDelConnect(id)}
>
<MyIcon name={'core/workflow/closeEdge'} w={'100%'}></MyIcon>
</Flex>
{!isToolEdge && (
<Flex
alignItems={'center'}
justifyContent={'center'}
position={'absolute'}
transform={arrowTransform}
pointerEvents={'all'}
w={highlightEdge ? '12px' : '10px'}
h={highlightEdge ? '12px' : '10px'}
zIndex={highlightEdge ? defaultZIndex + 1000 : defaultZIndex}
>
<MyIcon
name={highlightEdge ? 'core/workflow/edgeArrowBold' : 'core/workflow/edgeArrow'}
w={'100%'}
color={edgeColor}
/>
</Flex>
)}
</Box>
</EdgeLabelRenderer>
);
}, [
parentNode?.isFolded,
isHover,
highlightEdge,
labelX,
labelY,
isToolEdge,
defaultZIndex,
edgeColor,
targetPosition,
newTargetX,
newTargetY,
onDelConnect,
id
]);
const memoBezierEdge = useMemo(() => {
const targetEdge = workflowDebugData?.runtimeEdges.find(
(edge) => edge.source === source && edge.target === target
);
const edgeStyle: React.CSSProperties = (() => {
if (!targetEdge) {
return {
...style,
...(highlightEdge
? {
strokeWidth: 4
}
: { strokeWidth: 3, zIndex: 2 })
};
}
return {
...style,
strokeWidth: 3
};
})();
return (
<BezierEdge
{...props}
targetX={newTargetX}
targetY={newTargetY}
style={{
...edgeStyle,
stroke: edgeColor,
display: parentNode?.isFolded ? 'none' : 'block'
}}
/>
);
}, [
workflowDebugData?.runtimeEdges,
props,
newTargetX,
newTargetY,
edgeColor,
source,
target,
style,
highlightEdge,
parentNode?.isFolded
]);
return (
<>
{memoBezierEdge}
{memoEdgeLabel}
</>
);
};
export default React.memo(ButtonEdge);

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Flex } from '@chakra-ui/react';
import { BoxProps } from '@chakra-ui/react';
const Container = ({ children, ...props }: BoxProps) => {
return (
<Flex
flexDirection={'column'}
mx={3}
p={4}
position={'relative'}
bg={'myGray.50'}
border={'1px solid #F0F1F6'}
borderRadius={'md'}
{...props}
>
{children}
</Flex>
);
};
export default React.memo(Container);

View File

@@ -1,115 +0,0 @@
import { Box, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comment';
import { useContextSelector } from 'use-context-selector';
import { useReactFlow } from 'reactflow';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
import { WorkflowContext } from '../../context';
const ContextMenu = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({
template: CommentNode,
position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
t
});
const allUnFolded = useMemo(() => {
return !!menu ? nodeList.some((node) => node.isFolded) : false;
}, [nodeList, menu]);
return !!menu ? (
<Box position="relative">
<Box
position="absolute"
top={`${menu.top - 6}px`}
left={`${menu.left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Box
position={'absolute'}
top={menu.top}
left={menu.left}
bg={'white'}
w={'120px'}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
color={'myGray.600'}
p={1}
zIndex={10}
>
<Flex
alignItems={'center'}
px={2}
py={1}
cursor={'pointer'}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
>
<MyIcon name="comment" w={'1rem'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
<Flex
mt={1}
alignItems={'center'}
px={2}
py={1}
cursor={'pointer'}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
setMenu(null);
setNodes((state) => {
return state.map((node) => ({
...node,
data: {
...node.data,
isFolded: !allUnFolded
}
}));
});
}}
>
<MyIcon name="common/select" w={'1rem'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{allUnFolded ? t('workflow:unFoldAll') : t('workflow:foldAll')}
</Box>
</Flex>
</Box>
</Box>
) : null;
};
export default React.memo(ContextMenu);

View File

@@ -1,35 +0,0 @@
import React from 'react';
import { Box, useTheme } from '@chakra-ui/react';
const Divider = ({
text,
showBorderBottom = true,
icon
}: {
text?: 'Input' | 'Output' | string;
showBorderBottom?: boolean;
icon?: React.ReactNode;
}) => {
const theme = useTheme();
const isDivider = !text;
return (
<Box
alignItems={'center'}
display={'flex'}
justifyContent={'center'}
bg={'myGray.25'}
py={isDivider ? '0' : 2}
borderTop={theme.borders.base}
borderBottom={showBorderBottom ? theme.borders.base : 0}
fontWeight={'medium'}
>
{icon}
{icon && <Box w={1} />}
{text}
</Box>
);
};
export default React.memo(Divider);

View File

@@ -1,241 +0,0 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import {
Background,
ControlButton,
MiniMap,
MiniMapNodeProps,
Panel,
useReactFlow,
useViewport
} from 'reactflow';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import styles from './index.module.scss';
import { maxZoom, minZoom } from '../../constants';
import { useKeyPress } from 'ahooks';
import { WorkflowEventContext } from '../../context/workflowEventContext';
const buttonStyle = {
border: 'none',
borderRadius: '6px',
padding: '7px'
};
const FlowController = React.memo(function FlowController() {
const { fitView, zoomIn, zoomOut } = useReactFlow();
const { zoom } = useViewport();
const undo = useContextSelector(WorkflowContext, (v) => v.undo);
const redo = useContextSelector(WorkflowContext, (v) => v.redo);
const canRedo = useContextSelector(WorkflowContext, (v) => v.canRedo);
const canUndo = useContextSelector(WorkflowContext, (v) => v.canUndo);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const workflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.workflowControlMode
);
const setWorkflowControlMode = useContextSelector(
WorkflowEventContext,
(v) => v.setWorkflowControlMode
);
const mouseInCanvas = useContextSelector(WorkflowEventContext, (v) => v.mouseInCanvas);
const { t } = useTranslation();
const isMac = !window ? false : window.navigator.userAgent.toLocaleLowerCase().includes('mac');
useKeyPress(['ctrl.z', 'meta.z', 'ctrl.shift.z', 'meta.shift.z', 'ctrl.y', 'meta.y'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
const isRedo = (e.key.toLowerCase() === 'z' && e.shiftKey) || e.key.toLowerCase() === 'y';
if (isRedo) {
redo();
} else {
undo();
}
});
useKeyPress(['ctrl.add', 'meta.add', 'ctrl.equalsign', 'meta.equalsign'], (e) => {
e.preventDefault();
e.stopPropagation();
if (!mouseInCanvas) return;
zoomIn();
});
useKeyPress(['ctrl.dash', 'meta.dash'], (e) => {
e.preventDefault();
if (!mouseInCanvas) return;
zoomOut();
});
/*
id: Render node id
*/
const MiniMapNode = useCallback(
({ x, y, width, height, color, id }: MiniMapNodeProps) => {
// If the node parentNode is folded, the child node will not be displayed
const node = nodeList.find((node) => node.nodeId === id);
const parentNode = node?.parentNodeId
? nodeList.find((n) => n.nodeId === node?.parentNodeId)
: undefined;
if (parentNode?.isFolded) {
return null;
}
return <rect x={x} y={y} width={width} height={height} fill={color} />;
},
[nodeList]
);
const Render = useMemo(() => {
return (
<>
<MiniMap
style={{
height: 92,
width: 150,
marginBottom: 62,
borderRadius: '10px',
boxShadow: '0px 0px 1px rgba(19, 51, 107, 0.10), 0px 4px 10px rgba(19, 51, 107, 0.10)'
}}
pannable
nodeComponent={MiniMapNode}
/>
<Panel
position={'bottom-right'}
style={{
display: 'flex',
marginBottom: 16,
padding: '5px 8px',
background: 'white',
borderRadius: '6px',
overflow: 'hidden',
alignItems: 'center',
gap: '2px',
boxShadow:
'0px 0px 1px 0px rgba(19, 51, 107, 0.20), 0px 12px 16px -4px rgba(19, 51, 107, 0.20)'
}}
>
{/* Control Mode */}
<MyTooltip
label={
workflowControlMode === 'select'
? t('workflow:pan_priority')
: t('workflow:mouse_priority')
}
>
<ControlButton
onClick={() => {
setWorkflowControlMode(workflowControlMode === 'select' ? 'drag' : 'select');
}}
style={{
...buttonStyle
}}
className={`${styles.customControlButton}`}
>
<MyIcon
name={
workflowControlMode === 'select'
? 'core/workflow/touchTable'
: 'core/workflow/mouse'
}
/>
</ControlButton>
</MyTooltip>
<Box w="1px" h="20px" bg="gray.200" mx={1.5}></Box>
{/* undo */}
<MyTooltip label={isMac ? t('common:common.undo_tip_mac') : t('common:common.undo_tip')}>
<ControlButton
onClick={undo}
style={buttonStyle}
className={`${styles.customControlButton}`}
disabled={!canUndo}
>
<MyIcon name={'core/workflow/undo'} />
</ControlButton>
</MyTooltip>
{/* redo */}
<MyTooltip label={isMac ? t('common:common.redo_tip_mac') : t('common:common.redo_tip')}>
<ControlButton
onClick={redo}
style={buttonStyle}
className={`${styles.customControlButton}`}
disabled={!canRedo}
>
<MyIcon name={'core/workflow/redo'} />
</ControlButton>
</MyTooltip>
<Box w="1px" h="20px" bg="gray.200" mx={1.5}></Box>
{/* zoom out */}
<MyTooltip
label={isMac ? t('common:common.zoomin_tip_mac') : t('common:common.zoomin_tip')}
>
<ControlButton
onClick={() => zoomOut()}
style={buttonStyle}
className={`${styles.customControlButton}`}
disabled={zoom <= minZoom}
>
<MyIcon name={'common/subtract'} />
</ControlButton>
</MyTooltip>
{/* zoom in */}
<MyTooltip
label={isMac ? t('common:common.zoomout_tip_mac') : t('common:common.zoomout_tip')}
>
<ControlButton
onClick={() => zoomIn()}
style={buttonStyle}
className={`${styles.customControlButton}`}
disabled={zoom >= maxZoom}
>
<MyIcon name={'common/addLight'} />
</ControlButton>
</MyTooltip>
<Box w="1px" h="20px" bg="gray.200" mx={1.5}></Box>
{/* fit view */}
<MyTooltip label={t('common:common.page_center')}>
<ControlButton
onClick={() => fitView()}
style={buttonStyle}
className={`custom-workflow-fix_view ${styles.customControlButton}`}
>
<MyIcon name={'core/modules/fixview'} />
</ControlButton>
</MyTooltip>
</Panel>
<Background />
</>
);
}, [
MiniMapNode,
workflowControlMode,
t,
isMac,
undo,
canUndo,
redo,
canRedo,
zoom,
setWorkflowControlMode,
zoomOut,
zoomIn,
fitView
]);
return Render;
});
export default FlowController;

View File

@@ -1,97 +0,0 @@
import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { CSSProperties, useEffect, useRef } from 'react';
import { ReactFlowState, useStore, useViewport } from 'reactflow';
const canvasStyle: CSSProperties = {
width: '100%',
height: '100%',
position: 'absolute',
zIndex: 10,
pointerEvents: 'none'
};
const storeSelector = (state: ReactFlowState) => ({
width: state.width,
height: state.height,
transform: state.transform
});
export type HelperLinesProps = {
horizontal?: THelperLine;
vertical?: THelperLine;
};
function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
const { width, height, transform } = useStore(storeSelector);
const { zoom } = useViewport();
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) {
return;
}
const dpi = window.devicePixelRatio;
canvas.width = width * dpi;
canvas.height = height * dpi;
ctx.scale(dpi, dpi);
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#D92D20';
const drawCross = (x: number, y: number, size: number) => {
ctx.beginPath();
ctx.moveTo(x - size, y - size);
ctx.lineTo(x + size, y + size);
ctx.moveTo(x + size, y - size);
ctx.lineTo(x - size, y + size);
ctx.stroke();
};
if (vertical) {
const x = vertical.position * transform[2] + transform[0];
ctx.beginPath();
ctx.moveTo(
x,
Math.min(...vertical.nodes.map((node) => node.top)) * transform[2] + transform[1]
);
ctx.lineTo(
x,
Math.max(...vertical.nodes.map((node) => node.bottom)) * transform[2] + transform[1]
);
ctx.stroke();
vertical.nodes.forEach((node) => {
drawCross(x, node.top * transform[2] + transform[1], 5 * zoom);
drawCross(x, node.bottom * transform[2] + transform[1], 5 * zoom);
});
}
if (horizontal) {
const y = horizontal.position * transform[2] + transform[1];
ctx.beginPath();
ctx.moveTo(
Math.min(...horizontal.nodes.map((node) => node.left)) * transform[2] + transform[0],
y
);
ctx.lineTo(
Math.max(...horizontal.nodes.map((node) => node.right)) * transform[2] + transform[0],
y
);
ctx.stroke();
horizontal.nodes.forEach((node) => {
drawCross(node.left * transform[2] + transform[0], y, 5 * zoom);
drawCross(node.right * transform[2] + transform[0], y, 5 * zoom);
});
}
}, [width, height, transform, horizontal, vertical, zoom]);
return <canvas ref={canvasRef} style={canvasStyle} />;
}
export default HelperLinesRenderer;

View File

@@ -1,13 +0,0 @@
import React from 'react';
import { Box, StackProps, HStack } from '@chakra-ui/react';
const IOTitle = ({ text, ...props }: { text?: 'Input' | 'Output' | string } & StackProps) => {
return (
<HStack fontSize={'md'} alignItems={'center'} fontWeight={'medium'} mb={4} {...props}>
<Box w={'3px'} h={'14px'} borderRadius={'13px'} bg={'primary.600'} />
<Box color={'myGray.900'}>{text}</Box>
</HStack>
);
};
export default React.memo(IOTitle);

Some files were not shown because too many files have changed in this diff Show More