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:
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
77
projects/app/src/pages/api/admin/initv4820.ts
Normal file
77
projects/app/src/pages/api/admin/initv4820.ts
Normal 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);
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
42
projects/app/src/pages/api/core/ai/model/delete.ts
Normal file
42
projects/app/src/pages/api/core/ai/model/delete.ts
Normal 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);
|
||||
29
projects/app/src/pages/api/core/ai/model/detail.ts
Normal file
29
projects/app/src/pages/api/core/ai/model/detail.ts
Normal 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);
|
||||
29
projects/app/src/pages/api/core/ai/model/getConfigJson.ts
Normal file
29
projects/app/src/pages/api/core/ai/model/getConfigJson.ts
Normal 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);
|
||||
22
projects/app/src/pages/api/core/ai/model/getDefaultConfig.ts
Normal file
22
projects/app/src/pages/api/core/ai/model/getDefaultConfig.ts
Normal 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);
|
||||
57
projects/app/src/pages/api/core/ai/model/list.ts
Normal file
57
projects/app/src/pages/api/core/ai/model/list.ts
Normal 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);
|
||||
127
projects/app/src/pages/api/core/ai/model/test.ts
Normal file
127
projects/app/src/pages/api/core/ai/model/test.ts
Normal 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' }]
|
||||
});
|
||||
};
|
||||
64
projects/app/src/pages/api/core/ai/model/update.ts
Normal file
64
projects/app/src/pages/api/core/ai/model/update.ts
Normal 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);
|
||||
75
projects/app/src/pages/api/core/ai/model/updateDefault.ts
Normal file
75
projects/app/src/pages/api/core/ai/model/updateDefault.ts
Normal 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);
|
||||
62
projects/app/src/pages/api/core/ai/model/updateWithJson.ts
Normal file
62
projects/app/src/pages/api/core/ai/model/updateWithJson.ts
Normal 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);
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
34
projects/app/src/pages/api/core/dataset/data/getQuoteData.ts
Normal file
34
projects/app/src/pages/api/core/dataset/data/getQuoteData.ts
Normal 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);
|
||||
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <></>;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user