V4.8.17 feature (#3485)
* feat: add third party account config (#3443) * temp * editor workflow variable style * add team to dispatch * i18n * delete console * change openai account position * fix * fix * fix * fix * fix * 4.8.17 test (#3461) * perf: external provider config * perf: ui * feat: add template config (#3434) * change template position * template config * delete console * delete * fix * fix * perf: Mongo visutal field (#3464) * remve invalid code * perf: team member visutal code * perf: virtual search; perf: search test data * fix: ts * fix: image response headers * perf: template code * perf: auth layout;perf: auto save (#3472) * perf: auth layout * perf: auto save * perf: auto save * fix: template guide display & http input support external variables (#3475) * fix: template guide display * http editor support external workflow variables * perf: auto save;fix: ifelse checker line break; (#3478) * perf: auto save * perf: auto save * fix: ifelse checker line break * perf: doc * perf: doc * fix: update var type error * 4.8.17 test (#3479) * perf: auto save * perf: auto save * perf: template code * 4.8.17 test (#3480) * perf: auto save * perf: auto save * perf: model price model * feat: add react memo * perf: model provider filter * fix: ts (#3481) * perf: auto save * perf: auto save * fix: ts * simple app tool select (#3473) * workflow plugin userguide & simple tool ui * simple tool filter * reuse component * change component to hook * fix * perf: too selector modal (#3484) * perf: auto save * perf: auto save * perf: markdown render * perf: too selector * fix: app version require tmbId * perf: templates refresh * perf: templates refresh * hide auto save error tip * perf: toolkit guide --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -17,7 +17,7 @@ const unAuthPage: { [key: string]: boolean } = {
|
||||
'/price': true
|
||||
};
|
||||
|
||||
const Auth = ({ children }: { children: JSX.Element }) => {
|
||||
const Auth = ({ children }: { children: JSX.Element | React.ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -80,14 +80,14 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
{isHideNavbar ? (
|
||||
<Auth>{children}</Auth>
|
||||
) : (
|
||||
<>
|
||||
<Auth>
|
||||
<Box h={'100%'} position={'fixed'} left={0} top={0} w={navbarWidth}>
|
||||
<Navbar unread={unread} />
|
||||
</Box>
|
||||
<Box h={'100%'} ml={navbarWidth} overflow={'overlay'}>
|
||||
<Auth>{children}</Auth>
|
||||
{children}
|
||||
</Box>
|
||||
</>
|
||||
</Auth>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -96,14 +96,16 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (
|
||||
<Auth>{children}</Auth>
|
||||
) : (
|
||||
<Flex h={'100%'} flexDirection={'column'}>
|
||||
<Box flex={'1 0 0'} h={0}>
|
||||
<Auth>{children}</Auth>
|
||||
</Box>
|
||||
<Box h={'50px'} borderTop={'1px solid rgba(0,0,0,0.1)'}>
|
||||
<NavbarPhone unread={unread} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Auth>
|
||||
<Flex h={'100%'} flexDirection={'column'}>
|
||||
<Box flex={'1 0 0'} h={0}>
|
||||
{children}
|
||||
</Box>
|
||||
<Box h={'50px'} borderTop={'1px solid rgba(0,0,0,0.1)'}>
|
||||
<NavbarPhone unread={unread} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Auth>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -82,6 +82,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
'/account/info',
|
||||
'/account/team',
|
||||
'/account/usage',
|
||||
'/account/thirdParty',
|
||||
'/account/apikey',
|
||||
'/account/setting',
|
||||
'/account/inform',
|
||||
|
||||
@@ -28,17 +28,22 @@ const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { s
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
|
||||
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
|
||||
|
||||
const Markdown = ({
|
||||
source = '',
|
||||
showAnimation = false,
|
||||
isDisabled = false,
|
||||
forbidZhFormat = false
|
||||
}: {
|
||||
type Props = {
|
||||
source?: string;
|
||||
showAnimation?: boolean;
|
||||
isDisabled?: boolean;
|
||||
forbidZhFormat?: boolean;
|
||||
}) => {
|
||||
};
|
||||
const Markdown = (props: Props) => {
|
||||
const source = props.source || '';
|
||||
|
||||
if (source.length < 200000) {
|
||||
return <MarkdownRender {...props} />;
|
||||
}
|
||||
|
||||
return <Box whiteSpace={'pre-wrap'}>{source}</Box>;
|
||||
};
|
||||
const MarkdownRender = ({ source = '', showAnimation, isDisabled, forbidZhFormat }: Props) => {
|
||||
const components = useMemo<any>(
|
||||
() => ({
|
||||
img: Image,
|
||||
|
||||
@@ -24,12 +24,6 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
|
||||
|
||||
const {
|
||||
isOpen: isOpenAiPointsModal,
|
||||
onClose: onCloseAiPointsModal,
|
||||
onOpen: onOpenAiPointsModal
|
||||
} = useDisclosure();
|
||||
|
||||
const avatarSize = useMemo(() => {
|
||||
const size = {
|
||||
sm: '1rem',
|
||||
@@ -74,17 +68,6 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
|
||||
: avatarList;
|
||||
}, [feConfigs.show_pay, avatarList, avatarSize, t]);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(e: string) => {
|
||||
if (e === 'price') {
|
||||
onOpenAiPointsModal();
|
||||
return;
|
||||
}
|
||||
return onchange?.(e);
|
||||
},
|
||||
[onOpenAiPointsModal, onchange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
@@ -94,30 +77,32 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
|
||||
}}
|
||||
>
|
||||
<MyTooltip label={disableTip}>
|
||||
<MySelect
|
||||
className="nowheel"
|
||||
isDisabled={!!disableTip}
|
||||
list={expandList}
|
||||
{...props}
|
||||
onchange={onSelect}
|
||||
/>
|
||||
<ModelPriceModal>
|
||||
{({ onOpen }) => (
|
||||
<MySelect
|
||||
className="nowheel"
|
||||
isDisabled={!!disableTip}
|
||||
list={expandList}
|
||||
{...props}
|
||||
onchange={(e) => {
|
||||
if (e === 'price') {
|
||||
onOpen();
|
||||
return;
|
||||
}
|
||||
return onchange?.(e);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ModelPriceModal>
|
||||
</MyTooltip>
|
||||
|
||||
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
|
||||
const { llmModelList, vectorModelList } = useSystemStore();
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
|
||||
const {
|
||||
isOpen: isOpenAiPointsModal,
|
||||
onClose: onCloseAiPointsModal,
|
||||
onOpen: onOpenAiPointsModal
|
||||
} = useDisclosure();
|
||||
|
||||
const avatarSize = useMemo(() => {
|
||||
const size = {
|
||||
sm: '1rem',
|
||||
@@ -211,8 +196,6 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
|
||||
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
46
projects/app/src/components/common/Modal/UseGuideModal.tsx
Normal file
46
projects/app/src/components/common/Modal/UseGuideModal.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Box, ModalBody, useDisclosure } from '@chakra-ui/react';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import React from 'react';
|
||||
|
||||
const UseGuideModal = ({
|
||||
children,
|
||||
title,
|
||||
iconSrc,
|
||||
text,
|
||||
link
|
||||
}: {
|
||||
children: ({ onClick }: { onClick: () => void }) => React.ReactNode;
|
||||
title?: string;
|
||||
iconSrc?: string;
|
||||
text?: string;
|
||||
link?: string;
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const onClick = () => {
|
||||
if (link) {
|
||||
return window.open(getDocPath(link), '_blank');
|
||||
}
|
||||
if (text) {
|
||||
return onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children({ onClick })}
|
||||
{isOpen && (
|
||||
<MyModal isOpen iconSrc={iconSrc} title={title} onClose={onClose} minW={'600px'}>
|
||||
<ModalBody>
|
||||
<Box border={'base'} borderRadius={'10px'} p={4} minH={'500px'}>
|
||||
<Markdown source={text} />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UseGuideModal);
|
||||
@@ -100,12 +100,6 @@ const AIChatSettingsModal = ({
|
||||
setRefresh(!refresh);
|
||||
};
|
||||
|
||||
const {
|
||||
isOpen: isOpenAiPointsModal,
|
||||
onClose: onCloseAiPointsModal,
|
||||
onOpen: onOpenAiPointsModal
|
||||
} = useDisclosure();
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
@@ -160,10 +154,11 @@ const AIChatSettingsModal = ({
|
||||
<Th fontSize={'mini'} pb={2}>
|
||||
<HStack spacing={1}>
|
||||
<Box> {t('app:ai_point_price')}</Box>
|
||||
<QuestionTip
|
||||
label={t('app:look_ai_point_price')}
|
||||
onClick={onOpenAiPointsModal}
|
||||
/>
|
||||
<ModelPriceModal>
|
||||
{({ onOpen }) => (
|
||||
<QuestionTip label={t('app:look_ai_point_price')} onClick={onOpen} />
|
||||
)}
|
||||
</ModelPriceModal>
|
||||
</HStack>
|
||||
</Th>
|
||||
<Th fontSize={'mini'} pb={2}>
|
||||
@@ -327,8 +322,6 @@ const AIChatSettingsModal = ({
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
@@ -26,6 +26,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal'));
|
||||
|
||||
const ModelTable = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -156,6 +159,19 @@ const ModelTable = () => {
|
||||
search
|
||||
]);
|
||||
|
||||
const filterProviderList = useMemo(() => {
|
||||
const allProviderIds: string[] = [
|
||||
...llmModelList,
|
||||
...vectorModelList,
|
||||
...audioSpeechModelList,
|
||||
whisperModel
|
||||
].map((model) => model.provider);
|
||||
|
||||
return providerList.current.filter(
|
||||
(item) => allProviderIds.includes(item.value) || item.value === ''
|
||||
);
|
||||
}, [audioSpeechModelList, llmModelList, vectorModelList, whisperModel]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Flex>
|
||||
@@ -168,7 +184,7 @@ const ModelTable = () => {
|
||||
bg={'myGray.50'}
|
||||
value={provider}
|
||||
onchange={setProvider}
|
||||
list={providerList.current}
|
||||
list={filterProviderList}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack flexShrink={0} ml={6}>
|
||||
@@ -228,24 +244,34 @@ const ModelTable = () => {
|
||||
|
||||
export default ModelTable;
|
||||
|
||||
export const ModelPriceModal = ({ onClose }: { onClose: () => void }) => {
|
||||
export const ModelPriceModal = ({
|
||||
children
|
||||
}: {
|
||||
children: ({ onOpen }: { onOpen: () => void }) => React.ReactNode;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isCentered
|
||||
iconSrc="/imgs/modal/bill.svg"
|
||||
title={t('common:support.wallet.subscription.Ai points')}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
maxW={'90vw'}
|
||||
maxH={'90vh'}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'}>
|
||||
<ModelTable />
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
<>
|
||||
{children({ onOpen })}
|
||||
{isOpen && (
|
||||
<MyModal
|
||||
isCentered
|
||||
iconSrc="/imgs/modal/bill.svg"
|
||||
title={t('common:support.wallet.subscription.Ai points')}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
w={'100%'}
|
||||
h={'100%'}
|
||||
maxW={'90vw'}
|
||||
maxH={'90vh'}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'}>
|
||||
<ModelTable />
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -226,8 +226,8 @@ const DatasetParamsModal = ({
|
||||
{limit !== undefined && (
|
||||
<Box display={['block', 'flex']}>
|
||||
<Flex flex={'0 0 120px'} alignItems={'center'} mb={[5, 0]}>
|
||||
<FormLabel>{t('app:max_quote_tokens')}</FormLabel>
|
||||
<QuestionTip label={t('app:max_quote_tokens_tips')} />
|
||||
<FormLabel>{t('common:max_quote_tokens')}</FormLabel>
|
||||
<QuestionTip label={t('common:max_quote_tokens_tips')} />
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'}>
|
||||
<InputSlider
|
||||
@@ -245,8 +245,8 @@ const DatasetParamsModal = ({
|
||||
)}
|
||||
<Box display={['block', 'flex']} mt={[6, 10]} mb={4}>
|
||||
<Flex flex={'0 0 120px'} alignItems={'center'} mb={[5, 0]}>
|
||||
<FormLabel>{t('app:min_similarity')}</FormLabel>
|
||||
<QuestionTip label={t('app:min_similarity_tip')} />
|
||||
<FormLabel>{t('common:min_similarity')}</FormLabel>
|
||||
<QuestionTip label={t('common:min_similarity_tip')} />
|
||||
</Flex>
|
||||
<Box flex={'1 0 0'}>
|
||||
{showSimilarity ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
|
||||
import { Box, Button, Flex, Switch, Textarea, useDisclosure } from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
@@ -20,6 +20,8 @@ import { isEqual } from 'lodash';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
|
||||
import { PluginRunContext } from '../context';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import AIModelSelector from '@/components/Select/AIModelSelector';
|
||||
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
|
||||
@@ -152,6 +154,7 @@ const RenderPluginInput = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const inputType = input.renderTypeList[0];
|
||||
const { llmModelList } = useSystemStore();
|
||||
|
||||
const render = (() => {
|
||||
if (inputType === FlowNodeInputTypeEnum.customVariable) {
|
||||
@@ -167,7 +170,19 @@ const RenderPluginInput = ({
|
||||
<FileSelector onChange={onChange} input={input} setUploading={setUploading} value={value} />
|
||||
);
|
||||
}
|
||||
|
||||
if (inputType === FlowNodeInputTypeEnum.selectLLMModel) {
|
||||
return (
|
||||
<AIModelSelector
|
||||
w={'100%'}
|
||||
value={value}
|
||||
list={llmModelList.map((item) => ({
|
||||
value: item.model,
|
||||
label: item.name
|
||||
}))}
|
||||
onchange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (input.valueType === WorkflowIOValueTypeEnum.string) {
|
||||
return (
|
||||
<Textarea
|
||||
|
||||
@@ -62,8 +62,8 @@ const SearchParamsTip = ({
|
||||
<Thead>
|
||||
<Tr bg={'transparent !important'}>
|
||||
<Th fontSize={'mini'}>{t('common:core.dataset.search.search mode')}</Th>
|
||||
<Th fontSize={'mini'}>{t('app:max_quote_tokens')}</Th>
|
||||
<Th fontSize={'mini'}>{t('app:min_similarity')}</Th>
|
||||
<Th fontSize={'mini'}>{t('common:max_quote_tokens')}</Th>
|
||||
<Th fontSize={'mini'}>{t('common:min_similarity')}</Th>
|
||||
{hasReRankModel && <Th fontSize={'mini'}>{t('common:core.dataset.search.ReRank')}</Th>}
|
||||
<Th fontSize={'mini'}>{t('common:core.module.template.Query extension')}</Th>
|
||||
{hasEmptyResponseMode && (
|
||||
|
||||
@@ -96,7 +96,7 @@ const LafAccountModal = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen iconSrc="/imgs/workflow/laf.png" title={t('common:user.Laf Account Setting')}>
|
||||
<MyModal isOpen iconSrc="support/account/laf" title={t('common:user.Laf Account Setting')}>
|
||||
<ModalBody>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
<Box>{t('common:support.user.Laf account intro')}</Box>
|
||||
@@ -107,7 +107,7 @@ const LafAccountModal = ({
|
||||
</Box>
|
||||
<Box>
|
||||
<Link textDecoration={'underline'} href={`${feConfigs.lafEnv}/`} isExternal>
|
||||
{t('support.user.Go laf env', {
|
||||
{t('common:support.user.Go laf env', {
|
||||
env: feConfigs.lafEnv?.split('//')[1]
|
||||
})}
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/wallet/sub/constants';
|
||||
import React, { useMemo } from 'react';
|
||||
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
|
||||
import { Box, Flex, Grid, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Flex, Grid } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
@@ -22,12 +22,6 @@ const StandardPlanContentList = ({
|
||||
const { t } = useTranslation();
|
||||
const { subPlans } = useSystemStore();
|
||||
|
||||
const {
|
||||
isOpen: isOpenAiPointsModal,
|
||||
onClose: onCloseAiPointsModal,
|
||||
onOpen: onOpenAiPointsModal
|
||||
} = useDisclosure();
|
||||
|
||||
const planContent = useMemo(() => {
|
||||
const plan = subPlans?.standard?.[level];
|
||||
|
||||
@@ -100,11 +94,15 @@ const StandardPlanContentList = ({
|
||||
amount: planContent.totalPoints
|
||||
})}
|
||||
</Box>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.wallet.subscription.AI points click to read tip')}
|
||||
onClick={onOpenAiPointsModal}
|
||||
></QuestionTip>
|
||||
<ModelPriceModal>
|
||||
{({ onOpen }) => (
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.wallet.subscription.AI points click to read tip')}
|
||||
onClick={onOpen}
|
||||
/>
|
||||
)}
|
||||
</ModelPriceModal>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
@@ -127,7 +125,6 @@ const StandardPlanContentList = ({
|
||||
<Box color={'myGray.600'}>{t('common:support.wallet.subscription.web_site_sync')}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
|
||||
</Grid>
|
||||
) : null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user