V4.8.15 feature (#3331)

* feat: add customize toolkit (#3205)

* chaoyang

* fix-auth

* add toolkit

* add order

* plugin usage

* fix

* delete console:

* Fix: Fix fullscreen preview top positioning and improve Markdown rendering logic (#3247)

* 完成任务:修复全屏预览顶部固定问题,优化 Markdown 渲染逻辑

* 有问题修改

* 问题再修改

* 修正问题

* fix: plugin standalone display issue (#3254)

* 4.8.15 test (#3246)

* o1 config

* perf: system plugin code

* 调整系统插件代码。增加html 渲染安全配置。 (#3258)

* perf: base64 picker

* perf: list app or dataset

* perf: plugin config code

* 小窗适配等问题 (#3257)

* 小窗适配等问题

* git问题

* 小窗剩余问题

* feat: system plugin auth and lock version (#3265)

* feat: system plugin auth and lock version

* update comment

* 4.8.15 test (#3267)

* tmp log

* perf: login direct

* perf: iframe html code

* remove log

* fix: plugin standalone display (#3277)

* refactor: 页面拆分&i18n拆分 (#3281)

* refactor: account组件拆成独立页面

* script: 新增i18n json文件创建脚本

* refactor: 页面i18n拆分

* i18n: add en&hant

* 4.8.15 test (#3285)

* tmp log

* remove log

* fix: watch avatar refresh

* perf: i18n code

* fix(plugin): use intro instead of userguide (#3290)

* Universal SSO (#3292)

* tmp log

* remove log

* feat: common oauth

* readme

* perf: sso provider

* remove sso code

* perf: refresh plugins

* feat: add api dataset (#3272)

* add api-dataset

* fix api-dataset

* fix api dataset

* fix ts

* perf: create collection code (#3301)

* tmp log

* remove log

* perf: i18n change

* update version doc

* feat: question guide from chatId

* perf: create collection code

* fix: request api

* fix: request api

* fix: tts auth and response type (#3303)

* perf: md splitter

* fix: tts auth and response type

* fix: api file dataset (#3307)

* perf: api dataset init (#3310)

* perf: collection schema

* perf: api dataset init

* refactor: 团队管理独立页面 (#3302)

* ui: 团队管理独立页面

* 代码优化

* fix

* perf: sync collection and ui check (#3314)

* perf: sync collection

* remove script

* perf: update api server

* perf: api dataset parent

* perf: team ui

* perf: team 18n

* update team ui

* perf: ui check

* perf: i18n

* fix: debug variables & cronjob & system plugin callback load (#3315)

* fix: debug variables & cronjob & system plugin callback load

* fix type

* fix

* fix

* fix: plugin dataset quote;perf: system variables init (#3316)

* fix: plugin dataset quote

* perf: system variables init

* perf: node templates ui;fix: dataset import ui (#3318)

* fix: dataset import ui

* perf: node templates ui

* perf: ui refresh

* feat:套餐改名和套餐跳转配置 (#3309)

* fixing:except Sidebar

* 去除了多余的代码

* 修正了套餐说明的代码

* 修正了误删除的show_git代码

* 修正了名字部分等代码

* 修正了问题,遗留了其他和ui讨论不一致的部分

* 4.8.15 test (#3319)

* remove log

* pref: bill ui

* pref: bill ui

* perf: log

* html渲染文档 (#3270)

* html渲染文档

* 文档有点小问题

* feat: doc (#3322)

* 集合重训练 (#3282)

* rebaser

* 一点补充

* 小问题

* 其他问题修正,删除集合保留文件的参数还没找到...

* reTraining

* delete uesless

* 删除了一行错误代码

* 集合重训练部分

* fixing

* 删除console代码

* feat: navbar item config (#3326)

* perf: custom navbar code;perf: retraining code;feat: api dataset and dataset api doc (#3329)

* feat: api dataset and dataset api doc

* perf: retraining code

* perf: custom navbar code

* fix: ts (#3330)

* fix: ts

* fix: ts

* retraining ui

* perf: api collection filter

* perf: retrining button

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
This commit is contained in:
Archer
2024-12-06 10:56:53 +08:00
committed by GitHub
parent b188544386
commit 1aebe5f185
307 changed files with 7383 additions and 3981 deletions

View File

@@ -43,6 +43,8 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/price': true
};
export const navbarWidth = '64px';
const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const { Loading } = useLoading();
@@ -79,10 +81,10 @@ const Layout = ({ children }: { children: JSX.Element }) => {
<Auth>{children}</Auth>
) : (
<>
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'64px'}>
<Box h={'100%'} position={'fixed'} left={0} top={0} w={navbarWidth}>
<Navbar unread={unread} />
</Box>
<Box h={'100%'} ml={'70px'} overflow={'overlay'}>
<Box h={'100%'} ml={navbarWidth} overflow={'overlay'}>
<Auth>{children}</Auth>
</Box>
</>

View File

@@ -11,19 +11,37 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { getDocPath } from '@/web/common/system/doc';
export enum NavbarTypeEnum {
normal = 'normal',
small = 'small'
}
const itemStyles: BoxProps & LinkProps = {
my: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
w: '48px',
h: '58px',
borderRadius: 'md'
};
const hoverStyle: LinkProps = {
_hover: {
bg: 'myGray.05',
color: 'primary.600'
}
};
const Navbar = ({ unread }: { unread: number }) => {
const { t } = useTranslation();
const router = useRouter();
const { userInfo } = useUserStore();
const { gitStar, feConfigs } = useSystemStore();
const { lastChatAppId } = useChatStore();
const navbarList = useMemo(
() => [
{
@@ -47,34 +65,36 @@ const Navbar = ({ unread }: { unread: number }) => {
link: `/dataset/list`,
activeLink: ['/dataset/list', '/dataset/detail']
},
{
label: t('common:navbar.Toolkit'),
icon: 'phoneTabbar/tool',
activeIcon: 'phoneTabbar/toolFill',
link: `/toolkit`,
activeLink: ['/toolkit']
},
{
label: t('common:navbar.Account'),
icon: 'support/user/userLight',
activeIcon: 'support/user/userFill',
link: '/account',
activeLink: ['/account']
link: '/account/info',
activeLink: [
'/account/bill',
'/account/info',
'/account/team',
'/account/usage',
'/account/apikey',
'/account/individuation',
'/account/inform',
'/account/promotion'
]
}
],
[lastChatAppId, t]
);
const itemStyles: BoxProps & LinkProps = {
my: 3,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
w: '48px',
h: '58px',
borderRadius: 'md'
};
const hoverStyle: LinkProps = {
_hover: {
bg: 'myGray.05',
color: 'primary.600'
}
};
const isSecondNavbarPage = useMemo(() => {
return ['/toolkit'].includes(router.pathname);
}, [router.pathname]);
return (
<Flex
@@ -85,6 +105,7 @@ const Navbar = ({ unread }: { unread: number }) => {
w={'100%'}
userSelect={'none'}
pb={2}
bg={isSecondNavbarPage ? 'myGray.50' : 'transparent'}
>
{/* logo */}
<Box
@@ -96,13 +117,7 @@ const Navbar = ({ unread }: { unread: number }) => {
cursor={'pointer'}
onClick={() => router.push('/account')}
>
<Avatar
w={'36px'}
h={'36px'}
src={userInfo?.avatar}
fallbackSrc={HUMAN_ICON}
borderRadius={'50%'}
/>
<Avatar w={'2rem'} h={'2rem'} src={userInfo?.avatar} borderRadius={'50%'} />
</Box>
{/* 导航列表 */}
<Box flex={1}>
@@ -121,7 +136,7 @@ const Navbar = ({ unread }: { unread: number }) => {
color: 'myGray.500',
bg: 'transparent',
_hover: {
bg: 'rgba(255,255,255,0.9)'
bg: isSecondNavbarPage ? 'white' : 'rgba(255,255,255,0.9)'
}
})}
{...(item.link !== router.asPath
@@ -153,7 +168,7 @@ const Navbar = ({ unread }: { unread: number }) => {
{...itemStyles}
{...hoverStyle}
prefetch
href={`/account?currentTab=inform`}
href={`/account/inform`}
mb={0}
color={'myGray.500'}
height={'48px'}
@@ -164,21 +179,26 @@ const Navbar = ({ unread }: { unread: number }) => {
</Link>
</Box>
)}
{(feConfigs?.docUrl || feConfigs?.chatbotUrl) && (
<MyTooltip label={t('common:common.system.Use Helper')} placement={'right-end'}>
<Link
{...itemStyles}
{...hoverStyle}
href={feConfigs?.chatbotUrl || getDocPath('/docs/intro')}
target="_blank"
mb={0}
color={'myGray.500'}
height={'48px'}
>
<MyIcon name={'common/courseLight'} width={'24px'} height={'24px'} />
</Link>
</MyTooltip>
)}
{feConfigs?.navbarItems
?.filter((item) => item.isActive)
.map((item) => (
<MyTooltip key={item.id} label={item.name} placement={'right-end'}>
<Link
as={NextLink}
href={item.url}
target={'_blank'}
{...itemStyles}
{...hoverStyle}
mt={0}
color={'myGray.500'}
height={'48px'}
>
<Avatar src={item.avatar} borderRadius={'md'} />
</Link>
</MyTooltip>
))}
{feConfigs?.show_git && (
<MyTooltip label={`Git Star: ${gitStar}`} placement={'right-end'}>
<Link

View File

@@ -29,11 +29,19 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
unread: 0
},
{
label: t('common:navbar.Tools'),
label: t('common:navbar.Datasets'),
icon: 'core/dataset/datasetLight',
activeIcon: 'core/dataset/datasetFill',
link: `/dataset/list`,
activeLink: ['/dataset/list', '/dataset/detail'],
unread: 0
},
{
label: t('common:navbar.Toolkit'),
icon: 'phoneTabbar/tool',
activeIcon: 'phoneTabbar/toolFill',
link: '/tools',
activeLink: ['/tools'],
link: `/toolkit`,
activeLink: ['/toolkit'],
unread: 0
},
{
@@ -41,7 +49,16 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
icon: 'support/user/userLight',
activeIcon: 'support/user/userFill',
link: '/account',
activeLink: ['/account'],
activeLink: [
'/account/bill',
'/account/info',
'/account/team',
'/account/usage',
'/account/apikey',
'/account/individuation',
'/account/inform',
'/account/promotion'
],
unread
}
],
@@ -56,7 +73,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
justifyContent={'space-between'}
backgroundColor={'white'}
position={'relative'}
px={10}
px={4}
>
{navbarList.map((item) => (
<Flex

View File

@@ -5,7 +5,7 @@ import Icon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
const codeLight: { [key: string]: React.CSSProperties } = {
export const codeLight: { [key: string]: React.CSSProperties } = {
'code[class*=language-]': {
color: '#d4d4d4',
textShadow: 'none',

View File

@@ -1,25 +1,242 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { Box } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import React, { useMemo, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import {
Box,
Flex,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalHeader,
useDisclosure,
ModalCloseButton
} from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { useTranslation } from 'next-i18next';
import { useMarkdownWidth } from '../hooks';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type.d';
import { codeLight } from '../CodeLight';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const StyledButton = ({
label,
iconName,
onClick,
isActive,
viewMode,
isMobile
}: {
label: string;
iconName: IconNameType;
onClick: () => void;
isActive?: boolean;
viewMode: 'source' | 'iframe';
isMobile?: boolean;
}) => {
const isPreview = viewMode === 'iframe';
const textColor = isPreview
? isActive
? 'myGray.900'
: 'myGray.500'
: isActive
? '#FFF'
: 'rgba(255, 255, 255, 0.8)';
const bg = isPreview ? (isActive ? 'myGray.150' : '') : isActive ? '#333A47' : '';
const hoverBg = isPreview ? 'myGray.150' : '#333A47';
const MermaidBlock = ({ code }: { code: string }) => {
const { width, Ref } = useMarkdownWidth();
return (
<Box w={width} ref={Ref}>
<Flex
bg={bg}
color={textColor}
borderRadius="5px"
boxShadow="none"
fontWeight={isActive ? 500 : 400}
_hover={{
bg: hoverBg
}}
alignItems="center"
justifyContent="center"
onClick={onClick}
cursor="pointer"
px={isMobile ? '6px' : '8px'}
h={isMobile ? '24px' : '28px'}
>
{isMobile ? (
<MyTooltip label={label} placement="bottom" hasArrow>
<Flex alignItems="center" justifyContent="center">
<Icon name={iconName} width="14px" height="14px" />
</Flex>
</MyTooltip>
) : (
<Flex alignItems="center" justifyContent="flex-start">
<Icon name={iconName} width="14px" height="14px" />
<Box ml={2} fontSize="sm">
{label}
</Box>
</Flex>
)}
</Flex>
);
};
const IframeHtmlCodeBlock = ({
children,
className,
codeBlock,
match
}: {
children: React.ReactNode & React.ReactNode[];
className?: string;
codeBlock?: boolean;
match: RegExpExecArray | null;
}) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const [viewMode, setViewMode] = useState<'source' | 'iframe'>('source');
const isPreview = viewMode === 'iframe';
const { isOpen, onOpen, onClose } = useDisclosure();
const { width, Ref } = useMarkdownWidth();
const isMobile = width <= 420;
const codeBoxName = useMemo(() => {
const input = match?.['input'] || '';
if (!input) return match?.[1]?.toUpperCase();
const splitInput = input.split('#');
return splitInput[1] || match?.[1]?.toUpperCase();
}, [match]);
const Iframe = useMemo(
() => (
<iframe
src={code}
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
srcDoc={String(children)}
sandbox=""
referrerPolicy="no-referrer"
style={{
width: '100%',
height: '100%',
minHeight: '70vh',
border: 'none'
border: 'none',
background: 'white'
}}
/>
</Box>
),
[children]
);
if (codeBlock) {
return (
<Box
ref={Ref}
my={3}
borderRadius={'md'}
overflow={'hidden'}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
>
<Flex
py={2}
px={4}
color={'white'}
userSelect={'none'}
alignItems="center"
fontSize={'sm'}
gap={1.5}
{...(isPreview
? {
borderBottom: '1px solid',
borderColor: 'gray.150',
bg: 'myGray.25'
}
: {
bg: 'myGray.800'
})}
>
<Box
flex={1}
display="flex"
alignItems="center"
color={isPreview ? 'myGray.800' : 'rgba(255, 255, 255, 0.9)'}
>
{codeBoxName}
<Flex
cursor="pointer"
onClick={() => copyData(String(children))}
alignItems="center"
ml={2}
>
<Icon name="copy" width="14px" />
</Flex>
</Box>
<StyledButton
label={t('common:common.Code')}
iconName="code"
onClick={() => setViewMode('source')}
isActive={viewMode === 'source'}
viewMode={viewMode}
isMobile={isMobile}
/>
<StyledButton
label={t('common:common.Preview')}
iconName="preview"
onClick={() => setViewMode('iframe')}
isActive={viewMode === 'iframe'}
viewMode={viewMode}
isMobile={isMobile}
/>
<StyledButton
label={t('common:common.FullScreen')}
iconName="fullScreen"
onClick={onOpen}
viewMode={viewMode}
isMobile={isMobile}
/>
</Flex>
{isPreview ? (
<Box w={width} h="60vh">
{Iframe}
</Box>
) : (
<SyntaxHighlighter style={codeLight as any} language={match?.[1]} PreTag="pre">
{String(children).replace(/&nbsp;/g, ' ')}
</SyntaxHighlighter>
)}
{isOpen && (
<Modal onClose={onClose} isOpen size={'full'}>
<ModalOverlay />
<ModalContent h={'100vh'} display={'flex'} flexDirection={'column'}>
<ModalHeader
display="flex"
justifyContent="space-between"
alignItems="center"
p={4}
bg="white"
borderBottom="1px solid"
borderColor="gray.300"
height="60px"
>
<Box fontSize="lg" color="myGray.900">
{t('common:common.FullScreenLight')}
</Box>
<ModalCloseButton zIndex={1} position={'relative'} top={0} right={0} />
</ModalHeader>
<ModalBody p={0} flex="1">
{Iframe}
</ModalBody>
</ModalContent>
</Modal>
)}
</Box>
);
}
return <code className={className}>{children}</code>;
};
export default MermaidBlock;
export default React.memo(IframeHtmlCodeBlock);

View File

@@ -23,6 +23,7 @@ const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr:
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false });
const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false });
const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { ssr: false });
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
@@ -127,6 +128,13 @@ function Code(e: any) {
if (codeType === CodeClassNameEnum.iframe) {
return <IframeCodeBlock code={strChildren} />;
}
if (codeType && codeType.toLowerCase() === CodeClassNameEnum.html) {
return (
<IframeHtmlCodeBlock className={className} codeBlock={codeBlock} match={match}>
{children}
</IframeHtmlCodeBlock>
);
}
return (
<CodeLight className={className} codeBlock={codeBlock} match={match}>

View File

@@ -6,7 +6,8 @@ export enum CodeClassNameEnum {
quote = 'quote',
files = 'files',
latex = 'latex',
iframe = 'iframe'
iframe = 'iframe',
html = 'html'
}
function htmlTableToLatex(html: string) {

View File

@@ -3,12 +3,16 @@ import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router';
import { AI_POINT_USAGE_CARD_ROUTE } from '@/web/support/wallet/sub/constants';
import MySelect, { SelectProps } from '@fastgpt/web/components/common/MySelect';
import { HUGGING_FACE_ICON, LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { Box, Flex } from '@chakra-ui/react';
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
const AiPointsModal = dynamic(() =>
import('@/pages/price/components/Points').then((mod) => mod.AiPointsModal)
);
type Props = SelectProps & {
disableTip?: string;
@@ -19,6 +23,12 @@ const AIModelSelector = ({ list, onchange, disableTip, ...props }: Props) => {
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
const router = useRouter();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const avatarList = list.map((item) => {
const modelData =
llmModelList.find((model) => model.model === item.value) ||
@@ -58,7 +68,7 @@ const AIModelSelector = ({ list, onchange, disableTip, ...props }: Props) => {
const onSelect = useCallback(
(e: string) => {
if (e === 'price') {
router.push(AI_POINT_USAGE_CARD_ROUTE);
onOpenAiPointsModal();
return;
}
return onchange?.(e);
@@ -67,15 +77,25 @@ const AIModelSelector = ({ list, onchange, disableTip, ...props }: Props) => {
);
return (
<MyTooltip label={disableTip}>
<MySelect
className="nowheel"
isDisabled={!!disableTip}
list={expandList}
{...props}
onchange={onSelect}
/>
</MyTooltip>
<Box
css={{
span: {
display: 'block'
}
}}
>
<MyTooltip label={disableTip}>
<MySelect
className="nowheel"
isDisabled={!!disableTip}
list={expandList}
{...props}
onchange={onSelect}
/>
</MyTooltip>
{isOpenAiPointsModal && <AiPointsModal onClose={onCloseAiPointsModal} />}
</Box>
);
};

View File

@@ -29,6 +29,7 @@ import SelectAiModel from '@/components/Select/AIModelSelector';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import { defaultDatasetMaxTokens } from '@fastgpt/global/core/app/constants';
export type DatasetParamsProps = {
searchMode: `${DatasetSearchModeEnum}`;
@@ -52,7 +53,7 @@ const DatasetParamsModal = ({
limit,
similarity,
usingReRank,
maxTokens = 3000,
maxTokens = defaultDatasetMaxTokens,
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel,
datasetSearchExtensionBg,
@@ -117,6 +118,12 @@ const DatasetParamsModal = ({
}
}, [chatModelSelectList, datasetSearchUsingCfrForm, queryExtensionModel, setValue]);
// 保证只有 80 左右个刻度。
const maxTokenStep = useMemo(() => {
if (maxTokens < 8000) return 80;
return Math.ceil(maxTokens / 80 / 100) * 100;
}, [maxTokens]);
return (
<MyModal
isOpen={true}
@@ -232,7 +239,7 @@ const DatasetParamsModal = ({
]}
min={100}
max={maxTokens}
step={50}
step={maxTokenStep}
value={getValues(NodeInputKeyEnum.datasetMaxTokens) ?? 1000}
onChange={(val) => {
setValue(NodeInputKeyEnum.datasetMaxTokens, val);

View File

@@ -8,35 +8,19 @@ import {
Textarea,
HStack
} from '@chakra-ui/react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo } 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 MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { AppScheduledTriggerConfigType } from '@fastgpt/global/core/app/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import dynamic from 'next/dynamic';
import type { MultipleSelectProps } from '@fastgpt/web/components/common/MySelect/type.d';
import { cronParser2Fields } from '@fastgpt/global/common/string/time';
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
import ScheduleTimeSelect, {
cronString2Label,
defaultCronString
} from '@fastgpt/web/components/common/MySelect/CronSelector';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
);
// options type:
enum CronJobTypeEnum {
month = 'month',
week = 'week',
day = 'day',
interval = 'interval'
}
type CronType = 'month' | 'week' | 'day' | 'interval';
const defaultValue = ['day', 0, 0];
const defaultCronString = '0 0 * * *';
type CronFieldType = [CronType, number, number];
const ScheduledTriggerConfig = ({
value,
@@ -49,106 +33,8 @@ const ScheduledTriggerConfig = ({
const { isOpen, onOpen, onClose } = useDisclosure();
const timezone = value?.timezone;
const cronString = value?.cronString;
const defaultPrompt = value?.defaultPrompt;
const get24HoursOptions = () => {
return Array.from({ length: 24 }, (_, i) => ({
label: `${i < 10 ? '0' : ''}${i}:00`,
value: i
}));
};
const getRoute = (i: number) => {
const { t } = useTranslation();
switch (i) {
case 0:
return t('app:week.Sunday');
case 1:
return t('app:week.Monday');
case 2:
return t('app:week.Tuesday');
case 3:
return t('app:week.Wednesday');
case 4:
return t('app:week.Thursday');
case 5:
return t('app:week.Friday');
case 6:
return t('app:week.Saturday');
default:
return t('app:week.Sunday');
}
};
const getWeekOptions = () => {
return Array.from({ length: 7 }, (_, i) => {
return {
label: getRoute(i),
value: i,
children: get24HoursOptions()
};
});
};
const getMonthOptions = () => {
return Array.from({ length: 28 }, (_, i) => ({
label: `${i + 1}` + t('app:month.unit'),
value: i,
children: get24HoursOptions()
}));
};
const getInterValOptions = () => {
// 每n小时
return [
{
label: t('app:interval.per_hour'),
value: 1
},
{
label: t('app:interval.2_hours'),
value: 2
},
{
label: t('app:interval.3_hours'),
value: 3
},
{
label: t('app:interval.4_hours'),
value: 4
},
{
label: t('app:interval.6_hours'),
value: 6
},
{
label: t('app:interval.12_hours'),
value: 12
}
];
};
const cronSelectList = useRef<MultipleSelectProps['list']>([
{
label: t('app:cron.every_day'),
value: CronJobTypeEnum.day,
children: get24HoursOptions()
},
{
label: t('app:cron.every_week'),
value: CronJobTypeEnum.week,
children: getWeekOptions()
},
{
label: t('app:cron.every_month'),
value: CronJobTypeEnum.month,
children: getMonthOptions()
},
{
label: t('app:cron.interval'),
value: CronJobTypeEnum.interval,
children: getInterValOptions()
}
]);
const isOpenSchedule = value?.cronString !== '';
const onUpdate = useCallback(
({
@@ -169,95 +55,6 @@ const ScheduledTriggerConfig = ({
[onChange, value]
);
/* cron string to config field */
const cronConfig = useMemo(() => {
if (!cronString) {
return;
}
const cronField = cronParser2Fields(cronString);
if (!cronField) {
return;
}
if (cronField.dayOfMonth.length !== 31) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.month, cronField.dayOfMonth[0], cronField.hour[0]]
};
}
if (cronField.dayOfWeek.length !== 8) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.week, cronField.dayOfWeek[0], cronField.hour[0]]
};
}
if (cronField.hour.length === 1) {
return {
isOpen: true,
cronField: [CronJobTypeEnum.day, cronField.hour[0], 0]
};
}
return {
isOpen: true,
cronField: [CronJobTypeEnum.interval, 24 / cronField.hour.length, 0]
};
}, [cronString]);
const isOpenSchedule = cronConfig?.isOpen ?? false;
const cronField = (cronConfig?.cronField || defaultValue) as CronFieldType;
const cronConfig2cronString = useCallback(
(e: CronFieldType) => {
const str = (() => {
if (e[0] === CronJobTypeEnum.month) {
return `0 ${e[2]} ${e[1]} * *`;
} else if (e[0] === CronJobTypeEnum.week) {
return `0 ${e[2]} * * ${e[1]}`;
} else if (e[0] === CronJobTypeEnum.day) {
return `0 ${e[1]} * * *`;
} else if (e[0] === CronJobTypeEnum.interval) {
return `0 */${e[1]} * * *`;
} else {
return '';
}
})();
onUpdate({ cronString: str });
},
[onUpdate]
);
// cron config to show label
const formatLabel = useMemo(() => {
if (!isOpenSchedule) {
return t('common:common.Not open');
}
if (cronField[0] === 'month') {
return t('common:core.app.schedule.Every month', {
day: cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'week') {
return t('common:core.app.schedule.Every week', {
day: cronField[1] === 0 ? t('app:day') : cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'day') {
return t('common:core.app.schedule.Every day', {
hour: cronField[1]
});
}
if (cronField[0] === 'interval') {
return t('common:core.app.schedule.Interval', {
interval: cronField[1]
});
}
return t('common:common.Not open');
}, [cronField, isOpenSchedule, t]);
useEffect(() => {
if (!value?.timezone) {
onUpdate({ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone });
@@ -282,7 +79,7 @@ const ScheduledTriggerConfig = ({
color={'myGray.600'}
onClick={onOpen}
>
{formatLabel}
{cronString2Label(value?.cronString ?? '', t)}
</Button>
</MyTooltip>
</Flex>
@@ -313,12 +110,10 @@ const ScheduledTriggerConfig = ({
<Flex alignItems={'center'} mt={5}>
<FormLabel flex={'0 0 80px'}>{t('app:execute_time')}</FormLabel>
<Box flex={'1 0 0'}>
<MultipleRowSelect
label={formatLabel}
value={cronField}
list={cronSelectList.current}
onSelect={(e) => {
cronConfig2cronString(e as CronFieldType);
<ScheduleTimeSelect
cronString={value?.cronString}
onChange={(e) => {
onUpdate({ cronString: e });
}}
/>
</Box>
@@ -353,17 +148,15 @@ const ScheduledTriggerConfig = ({
</>
);
}, [
cronConfig2cronString,
cronField,
defaultPrompt,
formatLabel,
isOpen,
isOpenSchedule,
onClose,
onOpen,
onUpdate,
t,
timezone
timezone,
value?.cronString
]);
return Render;

View File

@@ -14,6 +14,8 @@ import { defaultTTSConfig } from '@fastgpt/global/core/app/constants';
import ChatFunctionTip from './Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
const TTSSelect = ({
value = defaultTTSConfig,
@@ -26,6 +28,8 @@ const TTSSelect = ({
const { audioSpeechModelList } = useSystemStore();
const { isOpen, onOpen, onClose } = useDisclosure();
const appId = useContextSelector(AppContext, (v) => v.appId);
const list = useMemo(
() => [
{ label: t('common:core.app.tts.Close'), value: TTSTypeEnum.none },
@@ -50,6 +54,7 @@ const TTSSelect = ({
);
const { playAudioByText, cancelAudio, audioLoading, audioPlaying } = useAudioPlay({
appId,
ttsConfig: value
});

View File

@@ -2,20 +2,32 @@ import { Box, Flex, Divider } from '@chakra-ui/react';
import React from 'react';
import { useTranslation } from 'next-i18next';
const CostTooltip = ({ cost }: { cost?: number }) => {
const CostTooltip = ({ cost, hasTokenFee }: { cost?: number; hasTokenFee?: boolean }) => {
const { t } = useTranslation();
const getCostText = () => {
if (hasTokenFee && cost && cost > 0) {
return `${t('app:plugin_cost_per_times', {
cost: cost
})} + ${t('app:plugin_cost_by_token')}`;
}
if (hasTokenFee) {
return t('app:plugin_cost_by_token');
}
if (cost && cost > 0) {
return t('app:plugin_cost_per_times', {
cost: cost
});
}
return t('common:core.plugin.Free');
};
return (
<>
<Divider mt={4} mb={2} />
<Flex>
<Box>{t('common:core.plugin.cost')}</Box>
<Box color={'myGray.600'}>
{cost && cost > 0
? t('app:plugin_cost_per_times', {
cost: cost
})
: t('common:core.plugin.Free')}
</Box>
<Box color={'myGray.600'}>{getCostText()}</Box>
</Flex>
</>
);

View File

@@ -179,6 +179,7 @@ const Provider = ({
finishSegmentedAudio,
splitText2Audio
} = useAudioPlay({
appId,
ttsConfig,
...outLinkAuthData
});

View File

@@ -64,7 +64,6 @@ export const VariableInputItem = ({
maxLength={item.maxLength || 4000}
/>
)}
{item.type === VariableInputEnum.select && (
<Controller
key={`variables.${item.key}`}

View File

@@ -334,31 +334,28 @@ const ChatBox = ({
});
// create question guide
const createQuestionGuide = useCallback(
async ({ histories }: { histories: ChatSiteItemType[] }) => {
if (!questionGuide || chatController.current?.signal?.aborted) return;
try {
const abortSignal = new AbortController();
questionGuideController.current = abortSignal;
const createQuestionGuide = useCallback(async () => {
if (!questionGuide || chatController.current?.signal?.aborted) return;
try {
const abortSignal = new AbortController();
questionGuideController.current = abortSignal;
const result = await postQuestionGuide(
{
appId,
messages: chats2GPTMessages({ messages: histories, reserveId: false }).slice(-6),
...outLinkAuthData
},
abortSignal
);
if (Array.isArray(result)) {
setQuestionGuide(result);
setTimeout(() => {
scrollToBottom();
}, 100);
}
} catch (error) {}
},
[questionGuide, appId, outLinkAuthData, scrollToBottom]
);
const result = await postQuestionGuide(
{
appId,
chatId,
...outLinkAuthData
},
abortSignal
);
if (Array.isArray(result)) {
setQuestionGuide(result);
setTimeout(() => {
scrollToBottom();
}, 100);
}
} catch (error) {}
}, [questionGuide, appId, outLinkAuthData, scrollToBottom]);
/* Abort chat completions, questionGuide */
const abortRequest = useMemoizedFn((signal: string = 'stop') => {
@@ -407,7 +404,12 @@ const ChatBox = ({
// Only declared variables are kept
const requestVariables: Record<string, any> = {};
allVariableList?.forEach((item) => {
requestVariables[item.key] = variables[item.key];
requestVariables[item.key] =
variables[item.key] === '' ||
variables[item.key] === undefined ||
variables[item.key] === null
? item.defaultValue
: variables[item.key];
});
const responseChatId = getNanoid(24);
@@ -525,9 +527,7 @@ const ChatBox = ({
setTimeout(() => {
if (!checkIsInteractiveByHistories(newChatHistories)) {
createQuestionGuide({
histories: newChatHistories
});
createQuestionGuide();
}
generatingScroll(true);

View File

@@ -36,7 +36,6 @@ import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getDocPath } from '@/web/common/system/doc';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyBox from '@fastgpt/web/components/common/MyBox';
@@ -303,7 +302,6 @@ function EditKeyModal({
onEdit: () => void;
}) {
const { t } = useTranslation();
const { publishT } = useI18n();
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
const { feConfigs } = useSystemStore();
@@ -333,13 +331,13 @@ function EditKeyModal({
<MyModal
isOpen={true}
iconSrc="/imgs/modal/key.svg"
title={isEdit ? publishT('edit_api_key') : publishT('create_api_key')}
title={isEdit ? t('publish:edit_api_key') : t('publish:create_api_key')}
>
<ModalBody>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
<Input
placeholder={publishT('key_alias') || 'key_alias'}
placeholder={t('publish:key_alias') || 'key_alias'}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'

View File

@@ -1,295 +0,0 @@
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MemberTable from './components/MemberTable';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { TeamModalContext } from './context';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
enum TabListEnum {
member = 'member',
permission = 'permission',
group = 'group'
}
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
function TeamCard() {
const { toast } = useToast();
const { t } = useTranslation();
const {
myTeams,
refetchTeams,
members,
refetchMembers,
setEditTeamData,
onSwitchTeam,
searchKey,
setSearchKey
} = useContextSelector(TeamModalContext, (v) => v);
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('common:user.team.member.Confirm Leave')
});
const { runAsync: onLeaveTeam, loading: isLoadingLeaveTeam } = useRequest2(
async (teamId?: string) => {
if (!teamId) return;
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('common:user.team.Leave Team Failed')
}
);
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const [editGroupId, setEditGroupId] = useState<string>();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const Tablist = useMemo(
() => [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('common:user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
value: TabListEnum.member
},
{
icon: 'support/team/group',
label: t('user:team.group.group'),
value: TabListEnum.group
},
{
icon: 'support/team/key',
label: t('common:common.Role'),
value: TabListEnum.permission
}
],
[members.length, t]
);
const [tab, setTab] = useState(Tablist[0].value);
return (
<Flex
flexDirection={'column'}
bg={'white'}
minH={['50vh', 'auto']}
h={'100%'}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
alignItems={'center'}
px={5}
py={4}
borderBottom={'1.5px solid'}
borderBottomColor={'myGray.100'}
mb={2}
>
<Box fontSize={['sm', 'md']} fontWeight={'bold'} alignItems={'center'} color={'myGray.900'}>
{userInfo?.team.teamName}
</Box>
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
<MyIcon
name="edit"
w="14px"
ml={2}
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
)}
</Flex>
<Flex px={5} alignItems={'center'} justifyContent={'space-between'}>
<LightRowTabs<TabListEnum> overflow={'auto'} list={Tablist} value={tab} onChange={setTab} />
{/* ctrl buttons */}
<Flex alignItems={'center'}>
{tab === TabListEnum.member &&
userInfo?.team.permission.hasManagePer &&
feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('common:user.team.Team Tags Async')}
</Button>
)}
{tab === TabListEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('common:user.team.Invite Member')}
</Button>
)}
{tab === TabListEnum.member && !userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
isLoading={isLoadingLeaveTeam}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('common:user.team.Leave Team')}
</Button>
)}
{tab === TabListEnum.group && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
{tab === TabListEnum.permission && (
<Box ml="auto">
<SearchInput
placeholder={t('user:team.group.search_placeholder')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
)}
</Flex>
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{tab === TabListEnum.member && <MemberTable />}
{tab === TabListEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{tab === TabListEnum.permission && <PermissionManage />}
</Box>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
<ConfirmLeaveTeamModal />
</Flex>
);
}
export default TeamCard;

View File

@@ -1,103 +0,0 @@
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { defaultForm } from './components/EditInfoModal';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from './context';
function TeamList() {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { myTeams, onSwitchTeam, setEditTeamData } = useContextSelector(TeamModalContext, (v) => v);
return (
<Flex
flexDirection={'column'}
w={['auto', '270px']}
h={['auto', '100%']}
pt={3}
px={5}
mb={[2, 0]}
>
<Flex
alignItems={'center'}
py={2}
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontSize={['sm', 'md']} fontWeight={'bold'}>
{t('common:common.Team')}
</Box>
{/* if there is no team */}
{myTeams.length < 1 && (
<IconButton
variant={'ghost'}
border={'none'}
icon={
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'primary.500'}
cursor={'pointer'}
/>
}
aria-label={''}
onClick={() => setEditTeamData(defaultForm)}
/>
)}
</Flex>
<Box flex={['auto', '1 0 0']} overflow={'auto'}>
{myTeams.map((team) => (
<Flex
key={team.teamId}
alignItems={'center'}
mt={3}
borderRadius={'md'}
p={3}
cursor={'default'}
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'primary.200'
}
: {
_hover: {
bg: 'myGray.100'
}
})}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
flex={'1 0 0'}
w={0}
fontSize={'sm'}
{...(team.role === TeamMemberRoleEnum.owner
? {
fontWeight: 'bold'
}
: {})}
>
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
) : (
<Button
size={'xs'}
variant={'whitePrimary'}
onClick={() => onSwitchTeam(team.teamId)}
>
{t('common:user.team.Check Team')}
</Button>
)}
</Flex>
))}
</Box>
</Flex>
);
}
export default TeamList;

View File

@@ -1,50 +0,0 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { createContext, useContextSelector } from 'use-context-selector';
import TeamList from './TeamList';
import TeamCard from './TeamCard';
import { TeamModalContext, TeamModalContextProvider } from './context';
export const TeamContext = createContext<{}>({} as any);
type Props = { onClose: () => void };
const TeamManageModal = ({ onClose }: Props) => {
const { isLoading } = useContextSelector(TeamModalContext, (v) => v);
return (
<>
<MyModal
isOpen
onClose={onClose}
maxW={['90vw', '1000px']}
w={'100%'}
h={'550px'}
isCentered
bg={'myGray.50'}
overflow={'hidden'}
isLoading={isLoading}
>
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
<TeamList />
<Box h={'100%'} flex={'1 0 0'}>
<TeamCard />
</Box>
</Box>
</MyModal>
</>
);
};
const Render = (props: Props) => {
const { userInfo } = useUserStore();
return !!userInfo?.team ? (
<TeamModalContextProvider>
<TeamManageModal {...props} />
</TeamModalContextProvider>
) : null;
};
export default React.memo(Render);

View File

@@ -1,70 +0,0 @@
import React from 'react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
const TeamMenu = () => {
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { toast } = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button
variant={'whitePrimary'}
userSelect={'none'}
w={'100%'}
h={'34px'}
justifyContent={'space-between'}
px={3}
css={{
'& span': {
display: 'block'
}
}}
transform={'none !important'}
rightIcon={<MyIcon w={'1rem'} name={'common/select'} />}
onClick={() => {
if (feConfigs.isPlus) {
onOpen();
} else {
toast({
status: 'warning',
title: t('common:common.system.Commercial version function')
});
}
}}
>
<MyTooltip label={t('common:user.team.Select Team')}>
<Flex w={'100%'} alignItems={'center'}>
{userInfo?.team ? (
<>
<Avatar src={userInfo.team.avatar} w={'1rem'} />
<Box ml={2}>{userInfo.team.teamName}</Box>
</>
) : (
<>
<Box w={'8px'} h={'8px'} mr={3} borderRadius={'50%'} bg={'#67c13b'} />
{t('common:user.team.Personal Team')}
</>
)}
</Flex>
</MyTooltip>
</Button>
{isOpen && <TeamManageModal onClose={onClose} />}
</>
);
};
export default TeamMenu;

View File

@@ -5,9 +5,8 @@ import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constant
import { Box, Flex, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRouter } from 'next/router';
import { AI_POINT_USAGE_CARD_ROUTE } from '@/web/support/wallet/sub/constants';
import { getAiPointUsageCardRoute } from '@/web/support/wallet/sub/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const StandardPlanContentList = ({
@@ -23,6 +22,7 @@ const StandardPlanContentList = ({
const planContent = useMemo(() => {
const plan = subPlans?.standard?.[level];
if (!plan) return;
return {
price: plan.price * (mode === SubModeEnum.month ? 1 : 10),
@@ -96,7 +96,7 @@ const StandardPlanContentList = ({
ml={1}
label={t('common:support.wallet.subscription.AI points click to read tip')}
onClick={() => {
router.push(AI_POINT_USAGE_CARD_ROUTE);
router.push(getAiPointUsageCardRoute());
}}
></QuestionTip>
</Flex>

View File

@@ -1,7 +0,0 @@
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
export type CreateQuestionGuideParams = OutLinkChatAuthProps & {
appId: string;
messages: ChatCompletionMessageParam[];
};

View File

@@ -2,6 +2,7 @@ import {
PushDatasetDataChunkProps,
PushDatasetDataResponse
} from '@fastgpt/global/core/dataset/api';
import { APIFileServer } from '@fastgpt/global/core/dataset/apiDataset';
import {
DatasetSearchModeEnum,
DatasetSourceReadTypeEnum,
@@ -25,6 +26,7 @@ export type CreateDatasetParams = {
avatar: string;
vectorModel?: string;
agentModel?: string;
apiServer?: APIFileServer;
};
export type RebuildEmbeddingProps = {

View File

@@ -10,7 +10,7 @@ export async function register() {
const [
{ connectMongo },
{ systemStartCb },
{ initGlobalVariables, getInitConfig },
{ initGlobalVariables, getInitConfig, initSystemPlugins },
{ initVectorStore },
{ initRootUser },
{ getSystemPluginCb },
@@ -37,7 +37,7 @@ export async function register() {
await connectMongo();
//init system configinit vector databaseinit root user
await Promise.all([getInitConfig(), initVectorStore(), initRootUser()]);
await Promise.all([getInitConfig(), initVectorStore(), initRootUser(), initSystemPlugins()]);
getSystemPluginCb();
startMongoWatch();

View File

@@ -0,0 +1,27 @@
import React from 'react';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useTranslation } from 'next-i18next';
import { Box } from '@chakra-ui/react';
import AccountContainer, { TabEnum } from './components/AccountContainer';
import { serviceSideProps } from '../../web/common/utils/i18n';
const ApiKey = () => {
const { t } = useTranslation();
return (
<AccountContainer>
<Box px={[4, 8]} py={[4, 6]}>
<ApiKeyTable tips={t('account_apikey:key_tips')}></ApiKeyTable>
</Box>
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account_apikey', 'account', 'publish']))
}
};
}
export default ApiKey;

View File

@@ -79,8 +79,8 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
}),
{
manual: true,
successToast: t('common:common.submit_success'),
errorToast: t('common:common.Submit failed'),
successToast: t('account_bill:submit_success'),
errorToast: t('account_bill:submit_failed'),
onSuccess: () => {
onClose();
router.reload();
@@ -100,6 +100,7 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
emailAddress: ''
}
});
const { loading: isLoadingHeader } = useRequest2(() => getTeamInvoiceHeader(), {
manual: false,
onSuccess: (res) => inputForm.reset(res)
@@ -120,12 +121,12 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
w={'43rem'}
onClose={onClose}
isLoading={isLoading}
title={t('common:support.wallet.apply_invoice')}
title={t('account_bill:support_wallet_apply_invoice')}
>
{!isOpenSettleModal ? (
<Box px={['1.6rem', '3.25rem']} py={['1rem', '2rem']}>
<Box fontWeight={500} fontSize={'1rem'} pb={'0.75rem'}>
{t('common:support.wallet.billable_invoice')}
{t('account_bill:support_wallet_apply_invoice')}
</Box>
<Box h={'27.9rem'} overflow={'auto'}>
<TableContainer>
@@ -149,9 +150,9 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
}}
/>
</Th>
<Th>{t('common:user.type')}</Th>
<Th>{t('common:user.Time')}</Th>
<Th>{t('common:support.wallet.Amount')}</Th>
<Th>{t('account_bill:type')}</Th>
<Th>{t('account_bill:time')}</Th>
<Th>{t('account_bill:support_wallet_amount')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'0.875rem'}>
@@ -179,7 +180,9 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss')
: '-'}
</Td>
<Td>{t('common:pay.yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
<Td>
{t('account_bill:yuan', { amount: formatStorePrice2Read(item.price) })}
</Td>
</Tr>
))}
</Tbody>
@@ -193,7 +196,7 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('common:support.wallet.noBill')}
{t('account_bill:no_invoice_record')}
</Box>
</Flex>
)}
@@ -213,7 +216,7 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:common.Confirm')}
{t('account_bill:confirm')}
</Box>
</Flex>
</Button>
@@ -223,8 +226,8 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
<Box px={['1.6rem', '3.25rem']} py={['1rem', '2rem']}>
<Box w={'100%'} fontSize={'0.875rem'}>
<Flex w={'100%'} justifyContent={'space-between'}>
<Box>{t('common:support.wallet.invoice_amount')}</Box>
<Box>{t('common:pay.yuan', { amount: formatStorePrice2Read(totalPrice) })}</Box>
<Box>{t('account_bill:support_wallet_amount')}</Box>
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(totalPrice) })}</Box>
</Flex>
<Box w={'100%'} py={4}>
<Divider showBorderBottom={false} />
@@ -248,21 +251,21 @@ const ApplyInvoiceModal = ({ onClose }: { onClose: () => void }) => {
>
<MyIcon name="infoRounded" w={'14px'} h={'14px'} />
<Box ml={2} fontSize={'0.6875rem'}>
{t('common:support.wallet.invoice_info')}
{t('account_bill:invoice_sending_info')}
</Box>
</Flex>
<Flex justify={'flex-end'} w={'100%'} pt={[3, 7]}>
<Button variant={'outline'} mr={'0.75rem'} px="0" onClick={handleBack}>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:back')}
{t('account_bill:back')}
</Box>
</Flex>
</Button>
<Button isLoading={isSubmitting} px="0" onClick={inputForm.handleSubmit(onSubmitApply)}>
<Flex alignItems={'center'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:common.Confirm')}
{t('account_bill:confirm')}
</Box>
</Flex>
</Button>

View File

@@ -44,7 +44,7 @@ const BillTable = () => {
const billTypeList = useMemo(
() =>
[
{ label: t('common:common.All'), value: '' },
{ label: t('account_bill:all'), value: '' },
...Object.entries(billTypeMap).map(([key, value]) => ({
label: t(value.label as any),
value: key
@@ -120,9 +120,9 @@ const BillTable = () => {
w={'130px'}
></MySelect>
</Th>
<Th>{t('common:user.Time')}</Th>
<Th>{t('common:support.wallet.Amount')}</Th>
<Th>{t('common:support.wallet.bill.Status')}</Th>
<Th>{t('account_bill:time')}</Th>
<Th>{t('account_bill:support_wallet_amount')}</Th>
<Th>{t('account_bill:status')}</Th>
<Th></Th>
</Tr>
</Thead>
@@ -134,16 +134,16 @@ const BillTable = () => {
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{commonT('common:pay.yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
<Td>{t('account_bill:yuan', { amount: formatStorePrice2Read(item.price) })}</Td>
<Td>{t(billStatusMap[item.status]?.label as any)}</Td>
<Td>
{item.status === 'NOTPAY' && (
<Button mr={4} onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
{t('common:common.Update')}
{t('account_bill:update')}
</Button>
)}
<Button variant={'whiteBase'} size={'sm'} onClick={() => setBillDetail(item)}>
{t('common:common.Detail')}
{t('account_bill:detail')}
</Button>
</Td>
</Tr>
@@ -164,7 +164,7 @@ const BillTable = () => {
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('common:support.wallet.noBill')}
{t('account_bill:no_invoice_record')}
</Box>
</Flex>
)}
@@ -187,85 +187,79 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('common:support.wallet.bill_detail')}
title={t('account_bill:bill_detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Number')}:</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:order_number')}:</FormLabel>
<Box>{bill.orderId}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.usage.Time')}:</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:generation_time')}:</FormLabel>
<Box>{dayjs(bill.createTime).format('YYYY/MM/DD HH:mm:ss')}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Type')}:</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:order_type')}:</FormLabel>
<Box>{t(billTypeMap[bill.type]?.label as any)}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.Status')}:</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:status')}:</FormLabel>
<Box>{t(billStatusMap[bill.status]?.label as any)}</Box>
</Flex>
{!!bill.metadata?.payWay && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.bill.payWay.Way')}:</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:payment_method')}:</FormLabel>
<Box>{t(billPayWayMap[bill.metadata.payWay]?.label as any)}</Box>
</Flex>
)}
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.Amount')}:</FormLabel>
<Box>{commonT('common:pay.yuan', { amount: formatStorePrice2Read(bill.price) })}</Box>
<FormLabel flex={'0 0 120px'}>{t('account_bill:support_wallet_amount')}:</FormLabel>
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(bill.price) })}</Box>
</Flex>
{bill.metadata && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('common:support.wallet.has_invoice')}:</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:has_invoice')}:</FormLabel>
{bill.metadata.payWay === 'balance' ? (
t('user:bill.not_need_invoice')
) : (
<Box>
{(bill.metadata.payWay = bill.hasInvoice ? t('common:yes') : t('common:no'))}
{
(bill.metadata.payWay = bill.hasInvoice
? t('account_bill:yes')
: t('account_bill:no'))
}
</Box>
)}
</Flex>
)}
{!!bill.metadata?.subMode && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>
{t('common:support.wallet.subscription.mode.Period')}:
</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_period')}:</FormLabel>
<Box>{t(subModeMap[bill.metadata.subMode]?.label as any)}</Box>
</Flex>
)}
{!!bill.metadata?.standSubLevel && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>
{t('common:support.wallet.subscription.Stand plan level')}:
</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_package')}:</FormLabel>
<Box>{t(standardSubLevelMap[bill.metadata.standSubLevel]?.label as any)}</Box>
</Flex>
)}
{bill.metadata?.month !== undefined && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>
{t('common:support.wallet.subscription.Month amount')}:
</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_mode_month')}:</FormLabel>
<Box>{bill.metadata?.month}</Box>
</Flex>
)}
{bill.metadata?.datasetSize !== undefined && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>
{t('common:support.wallet.subscription.Extra dataset size')}:
</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:extra_dataset_size')}:</FormLabel>
<Box>{bill.metadata?.datasetSize}</Box>
</Flex>
)}
{bill.metadata?.extraPoints !== undefined && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>
{t('common:support.wallet.subscription.Extra ai points')}:
</FormLabel>
<FormLabel flex={'0 0 120px'}>{t('account_bill:extra_ai_points')}:</FormLabel>
<Box>{bill.metadata.extraPoints}</Box>
</Flex>
)}

View File

@@ -38,12 +38,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required>
{t('common:support.wallet.invoice_data.organization_name')}
</FormLabel>
<FormLabel required>{t('account_bill:organization_name')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.organization_name')}
placeholder={t('account_bill:organization_name')}
{...register('teamName', { required: true })}
/>
</Flex>
@@ -52,10 +50,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required>{t('common:support.wallet.invoice_data.unit_code')}</FormLabel>
<FormLabel required>{t('account_bill:unit_code')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.unit_code')}
placeholder={t('account_bill:unit_code')}
{...register('unifiedCreditCode', { required: true })}
/>
</Flex>
@@ -64,12 +62,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice}>
{t('common:support.wallet.invoice_data.company_address')}
</FormLabel>
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:company_address')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.company_address')}
placeholder={t('account_bill:company_address')}
{...register('companyAddress', { required: !!needSpecialInvoice })}
/>
</Flex>
@@ -78,12 +74,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice}>
{t('common:support.wallet.invoice_data.company_phone')}
</FormLabel>
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:company_phone')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.company_phone')}
placeholder={t('account_bill:company_phone')}
{...register('companyPhone', { required: !!needSpecialInvoice })}
/>
</Flex>
@@ -92,12 +86,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice}>
{t('common:support.wallet.invoice_data.bank')}
</FormLabel>
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:bank_name')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.bank')}
placeholder={t('account_bill:bank_name')}
{...register('bankName', { required: !!needSpecialInvoice })}
/>
</Flex>
@@ -106,12 +98,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required={!!needSpecialInvoice}>
{t('common:support.wallet.invoice_data.bank_account')}
</FormLabel>
<FormLabel required={!!needSpecialInvoice}>{t('account_bill:bank_account')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.bank_account')}
placeholder={t('account_bill:bank_account')}
{...register('bankAccount', { required: !!needSpecialInvoice })}
/>
</Flex>
@@ -120,9 +110,7 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required>
{t('common:support.wallet.invoice_data.need_special_invoice')}
</FormLabel>
<FormLabel required>{t('account_bill:need_special_invoice')}</FormLabel>
{/* @ts-ignore */}
<RadioGroup
value={`${needSpecialInvoice}`}
@@ -133,10 +121,10 @@ export const InvoiceHeaderSingleForm = ({
>
<HStack h={'2rem'}>
<Radio value="true" pr={'1rem'}>
<Box fontSize={'14px'}>{t('common:yes')}</Box>
<Box fontSize={'14px'}>{t('account_bill:yes')}</Box>
</Radio>
<Radio value="false">
<Box fontSize={'14px'}>{t('common:no')}</Box>
<Box fontSize={'14px'}>{t('account_bill:no')}</Box>
</Radio>
</HStack>
</RadioGroup>
@@ -149,10 +137,10 @@ export const InvoiceHeaderSingleForm = ({
alignItems={['flex-start', 'center']}
flexDir={['column', 'row']}
>
<FormLabel required>{t('common:support.wallet.invoice_data.email')}</FormLabel>
<FormLabel required>{t('account_bill:email_address')}</FormLabel>
<Input
{...styles}
placeholder={t('common:support.wallet.invoice_data.email')}
placeholder={t('account_bill:email_address')}
{...register('emailAddress', {
required: true,
pattern: {
@@ -195,8 +183,8 @@ const InvoiceHeaderForm = () => {
(data: TeamInvoiceHeaderType) => updateTeamInvoiceHeader(data),
{
manual: true,
successToast: t('common:common.Save Success'),
errorToast: t('common:common.Save Failed')
successToast: t('account_bill:save_success'),
errorToast: t('account_bill:save_failed')
}
);
@@ -214,7 +202,7 @@ const InvoiceHeaderForm = () => {
>
<Flex alignItems={'center'} px={'20px'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:common.Save')}
{t('account_bill:save')}
</Box>
</Flex>
</Button>

View File

@@ -42,9 +42,9 @@ const InvoiceTable = () => {
<Thead h="3rem">
<Tr>
<Th w={'20%'}>#</Th>
<Th w={'20%'}>{t('common:user.Time')}</Th>
<Th w={'20%'}>{t('common:support.wallet.Amount')}</Th>
<Th w={'20%'}>{t('common:support.wallet.bill.Status')}</Th>
<Th w={'20%'}>{t('account_bill:time')}</Th>
<Th w={'20%'}>{t('account_bill:support_wallet_amount')}</Th>
<Th w={'20%'}>{t('account_bill:status')}</Th>
<Th w={'20%'}></Th>
</Tr>
</Thead>
@@ -55,7 +55,7 @@ const InvoiceTable = () => {
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{t('common:pay.yuan', { amount: formatStorePrice2Read(item.amount) })}</Td>
<Td>{t('account_bill:yuan', { amount: formatStorePrice2Read(item.amount) })}</Td>
<Td>
<Flex
px={'0.75rem'}
@@ -71,8 +71,8 @@ const InvoiceTable = () => {
<MyIcon name="point" w={'6px'} h={'6px'} />
<Box ml={'0.25rem'}>
{item.status === 1
? t('common:common.submitted')
: t('common:common.have_done')}
? t('account_bill:submitted')
: t('account_bill:completed')}
</Box>
</Flex>
</Td>
@@ -91,7 +91,7 @@ const InvoiceTable = () => {
>
<Flex>
<MyIcon name="paragraph" w={'16px'} h={'16px'} />
<Box ml={'0.38rem'}>{t('common:common.Detail')}</Box>
<Box ml={'0.38rem'}>{t('account_bill:detail')}</Box>
</Flex>
</Button>
</Td>
@@ -113,7 +113,7 @@ const InvoiceTable = () => {
>
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
<Box mt={2} color={'myGray.500'}>
{t('common:support.wallet.no_invoice')}
{t('account_bill:no_invoice_record_tip')}
</Box>
</Flex>
)}
@@ -143,48 +143,27 @@ function InvoiceDetailModal({
title={
<Flex align={'center'}>
<MyIcon name="paragraph" w={'20px'} h={'20px'} color={'blue.600'} />
<Box ml={'0.62rem'}>{t('common:support.wallet.invoice_detail')}</Box>
<Box ml={'0.62rem'}>{t('account_bill:invoice_detail')}</Box>
</Flex>
}
>
<ModalBody px={'3.25rem'} py={'2rem'}>
<Flex w={'100%'} h={'100%'} flexDir={'column'} gap={'1rem'}>
<LabelItem
label={t('common:support.wallet.invoice_amount')}
value={t('common:pay.yuan', { amount: formatStorePrice2Read(invoice.amount) })}
label={t('account_bill:invoice_amount')}
value={t('account_bill:yuan', { amount: formatStorePrice2Read(invoice.amount) })}
/>
<LabelItem label={t('account_bill:organization_name')} value={invoice.teamName} />
<LabelItem label={t('account_bill:unit_code')} value={invoice.unifiedCreditCode} />
<LabelItem label={t('account_bill:company_address')} value={invoice.companyAddress} />
<LabelItem label={t('account_bill:company_phone')} value={invoice.companyPhone} />
<LabelItem label={t('account_bill:bank_name')} value={invoice.bankName} />
<LabelItem label={t('account_bill:bank_account')} value={invoice.bankAccount} />
<LabelItem
label={t('common:support.wallet.invoice_data.organization_name')}
value={invoice.teamName}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.unit_code')}
value={invoice.unifiedCreditCode}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.company_address')}
value={invoice.companyAddress}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.company_phone')}
value={invoice.companyPhone}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.bank')}
value={invoice.bankName}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.bank_account')}
value={invoice.bankAccount}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.need_special_invoice')}
value={invoice.needSpecialInvoice ? t('common:yes') : t('common:no')}
/>
<LabelItem
label={t('common:support.wallet.invoice_data.email')}
value={invoice.emailAddress}
label={t('account_bill:need_special_invoice')}
value={invoice.needSpecialInvoice ? t('account_bill:yes') : t('account_bill:no')}
/>
<LabelItem label={t('account_bill:email_address')} value={invoice.emailAddress} />
</Flex>
</ModalBody>
</MyModal>

View File

@@ -3,8 +3,10 @@ import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import ApplyInvoiceModal from './ApplyInvoiceModal';
import ApplyInvoiceModal from './components/ApplyInvoiceModal';
import { useRouter } from 'next/router';
import AccountContainer, { TabEnum } from '../components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
export enum InvoiceTabEnum {
bill = 'bill',
@@ -12,9 +14,9 @@ export enum InvoiceTabEnum {
invoiceHeader = 'invoiceHeader'
}
const BillTable = dynamic(() => import('./BillTable'));
const InvoiceHeaderForm = dynamic(() => import('./InvoiceHeaderForm'));
const InvoiceTable = dynamic(() => import('./InvoiceTable'));
const BillTable = dynamic(() => import('./components/BillTable'));
const InvoiceHeaderForm = dynamic(() => import('./components/InvoiceHeaderForm'));
const InvoiceTable = dynamic(() => import('./components/InvoiceTable'));
const BillAndInvoice = () => {
const { t } = useTranslation();
const router = useRouter();
@@ -23,15 +25,18 @@ const BillAndInvoice = () => {
const [isOpenInvoiceModal, setIsOpenInvoiceModal] = useState(false);
return (
<>
<AccountContainer>
<Box p={['1rem', '2rem']}>
<Flex justifyContent={'space-between'} alignItems={'center'} pb={'0.75rem'}>
<FillRowTabs
list={[
{ label: t('common:support.wallet.bill_tag.bill'), value: InvoiceTabEnum.bill },
{ label: t('common:support.wallet.bill_tag.invoice'), value: InvoiceTabEnum.invoice },
{ label: t('account_bill:bill_record'), value: InvoiceTabEnum.bill },
{
label: t('common:support.wallet.bill_tag.default_header'),
label: t('account_bill:support_wallet_bill_tag_invoice'),
value: InvoiceTabEnum.invoice
},
{
label: t('account_bill:default_header'),
value: InvoiceTabEnum.invoiceHeader
}
]}
@@ -49,7 +54,7 @@ const BillAndInvoice = () => {
<Button variant={'primary'} px="0" onClick={() => setIsOpenInvoiceModal(true)}>
<Flex alignItems={'center'} px={'20px'}>
<Box px={'1.25rem'} py={'0.5rem'}>
{t('common:support.wallet.invoicing')}
{t('account_bill:support_wallet_invoicing')}
</Box>
</Flex>
</Button>
@@ -68,8 +73,16 @@ const BillAndInvoice = () => {
/>
)}
</Box>
</>
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account_bill', 'account']))
}
};
}
export default BillAndInvoice;

View File

@@ -1,27 +1,18 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import PageContainer from '@/components/PageContainer';
import SideTabs from '@/components/SideTabs';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import UserInfo from './components/Info/index';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useTranslation } from 'next-i18next';
import Script from 'next/script';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const Promotion = dynamic(() => import('./components/Promotion'));
const UsageTable = dynamic(() => import('./components/UsageTable'));
const BillAndInvoice = dynamic(() => import('./components/bill/BillAndInvoice'));
const InformTable = dynamic(() => import('./components/InformTable'));
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
const Individuation = dynamic(() => import('./components/Individuation'));
enum TabEnum {
export enum TabEnum {
'info' = 'info',
'promotion' = 'promotion',
'usage' = 'usage',
@@ -29,26 +20,44 @@ enum TabEnum {
'inform' = 'inform',
'individuation' = 'individuation',
'apikey' = 'apikey',
'loginout' = 'loginout'
'loginout' = 'loginout',
'team' = 'team'
}
const Account = ({ currentTab }: { currentTab: TabEnum }) => {
const AccountContainer = ({
children,
isLoading
}: {
children: React.ReactNode;
isLoading?: boolean;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { userInfo, setUserInfo } = useUserStore();
const { feConfigs, systemVersion } = useSystemStore();
const router = useRouter();
const { isPc } = useSystem();
const currentTab = useMemo(() => {
return router.pathname.split('/').pop() as TabEnum;
}, [router.pathname]);
const tabList = [
{
icon: 'support/user/userLight',
label: t('user:personal_information'),
label: t('account:personal_information'),
value: TabEnum.info
},
...(feConfigs?.isPlus
? [
{
icon: 'support/user/usersLight',
label: t('account:team'),
value: TabEnum.team
},
{
icon: 'support/usage/usageRecordLight',
label: t('user:usage_record'),
label: t('account:usage_records'),
value: TabEnum.usage
}
]
@@ -57,7 +66,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
? [
{
icon: 'support/bill/payRecordLight',
label: t('user:bill_and_invoices'),
label: t('account:bills_and_invoices'),
value: TabEnum.bill
}
]
@@ -66,7 +75,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
? [
{
icon: 'support/account/promotionLight',
label: t('user:promotion_records'),
label: t('account:promotion_records'),
value: TabEnum.promotion
}
]
@@ -75,40 +84,36 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
? [
{
icon: 'support/outlink/apikeyLight',
label: t('common:user.apikey.key'),
label: t('account:api_key'),
value: TabEnum.apikey
}
]
: []),
{
icon: 'support/user/individuation',
label: t('user:personalization'),
label: t('account:personalization'),
value: TabEnum.individuation
},
...(feConfigs.isPlus
? [
{
icon: 'support/user/informLight',
label: t('user:notice'),
label: t('account:notifications'),
value: TabEnum.inform
}
]
: []),
{
icon: 'support/account/loginoutLight',
label: t('user:sign_out'),
label: t('account:logout'),
value: TabEnum.loginout
}
];
const { openConfirm, ConfirmModal } = useConfirm({
content: t('common:support.user.logout.confirm')
content: t('account:confirm_logout')
});
const router = useRouter();
const theme = useTheme();
const setCurrentTab = useCallback(
(tab: string) => {
if (tab === TabEnum.loginout) {
@@ -117,11 +122,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
router.replace('/login');
})();
} else {
router.replace({
query: {
currentTab: tab
}
});
router.replace('/account/' + tab);
}
},
[openConfirm, router, setUserInfo]
@@ -130,7 +131,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
return (
<>
<Script src={getWebReqUrl('/js/qrcode.min.js')} strategy="lazyOnload"></Script>
<PageContainer>
<PageContainer isLoading={isLoading}>
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
{isPc ? (
<Flex
@@ -172,13 +173,7 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
)}
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]} overflow={'auto'}>
{currentTab === TabEnum.info && <UserInfo />}
{currentTab === TabEnum.promotion && <Promotion />}
{currentTab === TabEnum.usage && <UsageTable />}
{currentTab === TabEnum.bill && <BillAndInvoice />}
{currentTab === TabEnum.individuation && <Individuation />}
{currentTab === TabEnum.inform && <InformTable />}
{currentTab === TabEnum.apikey && <ApiKeyTable />}
{children}
</Box>
</Flex>
<ConfirmModal />
@@ -187,13 +182,4 @@ const Account = ({ currentTab }: { currentTab: TabEnum }) => {
);
};
export async function getServerSideProps(content: any) {
return {
props: {
currentTab: content?.query?.currentTab || TabEnum.info,
...(await serviceSideProps(content, ['publish', 'user']))
}
};
}
export default Account;
export default AccountContainer;

View File

@@ -1,15 +0,0 @@
import React from 'react';
import ApiKeyTable from '@/components/support/apikey/Table';
import { useI18n } from '@/web/context/I18n';
import { Box } from '@chakra-ui/react';
const ApiKey = () => {
const { publishT } = useI18n();
return (
<Box px={[4, 8]} py={[4, 6]}>
<ApiKeyTable tips={publishT('key_tips')}></ApiKeyTable>
</Box>
);
};
export default ApiKey;

View File

@@ -1,65 +0,0 @@
import { Box, Card, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { UserType } from '@fastgpt/global/support/user/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
import I18nLngSelector from '@/components/Select/I18nLngSelector';
const Individuation = () => {
const { t } = useTranslation();
const { userInfo, updateUserInfo } = useUserStore();
const { toast } = useToast();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const onclickSave = useCallback(
async (data: UserType) => {
await updateUserInfo({
timezone: data.timezone
});
reset(data);
toast({
title: t('common:dataset.data.Update Success Tip'),
status: 'success'
});
},
[reset, t, toast, updateUserInfo]
);
return (
<Box py={[3, '28px']} px={['5vw', '64px']}>
<Flex alignItems={'center'} fontSize={'lg'} h={'30px'}>
<MyIcon mr={2} name={'support/user/individuation'} w={'20px'} />
{t('common:support.account.Individuation')}
</Flex>
<Card mt={6} px={[3, 10]} py={[3, 7]} fontSize={'sm'}>
<Flex alignItems={'center'} w={['85%', '350px']}>
<Box flex={'0 0 80px'}>{t('common:user.Language')}:&nbsp;</Box>
<Box flex={'1 0 0'}>
<I18nLngSelector />
</Box>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '350px']}>
<Box flex={'0 0 80px'}>{t('common:user.Timezone')}:&nbsp;</Box>
<TimezoneSelect
value={userInfo?.timezone}
onChange={(e) => {
if (!userInfo) return;
onclickSave({ ...userInfo, timezone: e });
}}
/>
</Flex>
</Card>
</Box>
);
};
export default Individuation;

View File

@@ -1,96 +0,0 @@
import React from 'react';
import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/web/support/user/inform/api';
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const InformTable = () => {
const { t } = useTranslation();
const theme = useTheme();
const { Loading } = useLoading();
const {
data: informs,
isLoading,
total,
pageSize,
Pagination,
getData,
pageNum
} = usePagination<UserInformSchema>({
api: getInforms,
pageSize: 20
});
return (
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Box px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
{informs.map((item) => (
<Box
key={item._id}
border={theme.borders.md}
py={2}
px={4}
borderRadius={'md'}
position={'relative'}
_notLast={{ mb: 3 }}
>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{item.title}</Box>
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
</Box>
{!item.read && (
<Button
variant={'whitePrimary'}
size={'xs'}
onClick={async () => {
if (!item.read) {
await readInform(item._id);
getData(pageNum);
}
}}
>
{t('common:support.inform.Read')}
</Button>
)}
</Flex>
<Box mt={2} fontSize={'sm'} color={'myGray.600'} whiteSpace={'pre-wrap'}>
{item.content}
</Box>
{!item.read && (
<>
<Box
w={'5px'}
h={'5px'}
borderRadius={'10px'}
bg={'red.600'}
position={'absolute'}
top={'8px'}
left={'8px'}
/>
</>
)}
</Box>
))}
{!isLoading && informs.length === 0 && (
<EmptyTip text={t('common:user.no_notice')}></EmptyTip>
)}
</Box>
{total > pageSize && (
<Flex w={'100%'} mt={4} px={[3, 8]} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
<Loading loading={isLoading && informs.length === 0} fixed={false} />
</Flex>
);
};
export default InformTable;

View File

@@ -1,138 +0,0 @@
import React from 'react';
import {
Grid,
Box,
Flex,
BoxProps,
useTheme,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { getPromotionInitData, getPromotionRecords } from '@/web/support/activity/promotion/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import dayjs from 'dayjs';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
const Promotion = () => {
const { t } = useTranslation();
const theme = useTheme();
const { copyData } = useCopyData();
const { userInfo } = useUserStore();
const { Loading } = useLoading();
const {
data: promotionRecords,
isLoading,
total,
pageSize,
Pagination
} = usePagination({
api: getPromotionRecords,
pageSize: 20
});
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
['getPromotionInitData'],
getPromotionInitData
);
const statisticsStyles: BoxProps = {
p: [4, 5],
border: theme.borders.base,
textAlign: 'center',
fontSize: ['md', 'lg'],
borderRadius: 'md'
};
const titleStyles: BoxProps = {
mt: 2,
fontSize: ['lg', '28px'],
fontWeight: 'bold'
};
return (
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
<Box {...statisticsStyles}>
<Box>{t('common:user.Amount of inviter')}</Box>
<Box {...titleStyles}>{invitedAmount}</Box>
</Box>
<Box {...statisticsStyles}>
<Box>{t('common:user.Amount of earnings')}</Box>
<Box {...titleStyles}>{earningsAmount}</Box>
</Box>
<Box {...statisticsStyles}>
<Flex alignItems={'center'} justifyContent={'center'}>
<Box>{t('common:user.Promotion Rate')}</Box>
<QuestionTip ml={1} label={t('common:user.Promotion rate tip')}></QuestionTip>
</Flex>
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
</Box>
<Box {...statisticsStyles}>
<Flex alignItems={'center'} justifyContent={'center'}>
<Box>{t('common:user.Invite Url')}</Box>
<QuestionTip ml={1} label={t('common:user.Invite url tip')}></QuestionTip>
</Flex>
<Button
mt={4}
variant={'whitePrimary'}
fontSize={'sm'}
onClick={() => {
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
}}
>
{t('common:user.Copy invite url')}
</Button>
</Box>
</Grid>
<Box mt={5}>
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
<Table>
<Thead>
<Tr>
<Th>{t('common:user.Time')}</Th>
<Th>{t('common:user.type')}</Th>
<Th>{t('common:pay.amount')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{promotionRecords.map((item) => (
<Tr key={item._id}>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{t(`user:promotion.${item.type}` as any)}</Td>
<Td>{item.amount}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{!isLoading && promotionRecords.length === 0 && (
<EmptyTip text={t('common:user.no_invite_records')}></EmptyTip>
)}
{total > pageSize && (
<Flex mt={4} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
<Loading loading={isLoading} fixed={false} />
</Box>
</Flex>
);
};
export default Promotion;

View File

@@ -0,0 +1,104 @@
import React, { useMemo } from 'react';
import { Box, ButtonProps, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
const TeamSelector = ({
showManage,
...props
}: ButtonProps & {
showManage?: boolean;
}) => {
const { t } = useTranslation();
const router = useRouter();
const { userInfo, initUserInfo } = useUserStore();
const { setLoading } = useSystemStore();
const { data: myTeams = [] } = useRequest2(() => getTeamList(TeamMemberStatusEnum.active), {
manual: false,
refreshDeps: [userInfo]
});
const { runAsync: onSwitchTeam } = useRequest2(
async (teamId: string) => {
setLoading(true);
await putSwitchTeam(teamId);
return initUserInfo();
},
{
onFinally: () => {
setLoading(false);
},
errorToast: t('common:user.team.Switch Team Failed')
}
);
const teamList = useMemo(() => {
return myTeams.map((team) => ({
label: (
<Flex
key={team.teamId}
alignItems={'center'}
borderRadius={'md'}
cursor={'default'}
gap={3}
onClick={() => onSwitchTeam(team.teamId)}
_hover={{
cursor: 'pointer'
}}
>
<Avatar src={team.avatar} w={['1.25rem', '1.375rem']} />
<Box flex={'1 0 0'} w={0} className="textEllipsis" fontSize={'sm'}>
{team.teamName}
</Box>
</Flex>
),
value: team.teamId
}));
}, [myTeams, onSwitchTeam]);
const formatTeamList = useMemo(() => {
return [
...(showManage
? [
{
label: (
<Flex
key={'manage'}
alignItems={'center'}
borderRadius={'md'}
cursor={'default'}
gap={3}
onClick={() => router.push('/account/team')}
>
<MyIcon name="common/setting" w={['1.25rem', '1.375rem']} />
<Box flex={'1 0 0'} w={0} className="textEllipsis" fontSize={'sm'}>
{t('user:manage_team')}
</Box>
</Flex>
),
value: 'manage',
showBorder: true
}
]
: []),
...teamList
];
}, [showManage, teamList, router]);
return (
<Box w={'100%'}>
<MySelect {...props} value={userInfo?.team?.teamId} list={formatTeamList} />
</Box>
);
};
export default TeamSelector;

View File

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

View File

@@ -0,0 +1,77 @@
import { Box, Card, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { UserType } from '@fastgpt/global/support/user/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
import TimezoneSelect from '@fastgpt/web/components/common/MySelect/TimezoneSelect';
import I18nLngSelector from '@/components/Select/I18nLngSelector';
import AccountContainer from './components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
const Individuation = () => {
const { t } = useTranslation();
const { userInfo, updateUserInfo } = useUserStore();
const { toast } = useToast();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const onclickSave = useCallback(
async (data: UserType) => {
await updateUserInfo({
timezone: data.timezone
});
reset(data);
toast({
title: t('account_individuation:update_data_success'),
status: 'success'
});
},
[reset, t, toast, updateUserInfo]
);
return (
<AccountContainer>
<Box py={[3, '28px']} px={['5vw', '64px']}>
<Flex alignItems={'center'} fontSize={'lg'} h={'30px'}>
<MyIcon mr={2} name={'support/user/individuation'} w={'20px'} />
{t('account_individuation:personalization')}
</Flex>
<Card mt={6} px={[3, 10]} py={[3, 7]} fontSize={'sm'}>
<Flex alignItems={'center'} w={['85%', '350px']}>
<Box flex={'0 0 80px'}>{t('account_individuation:language')}:&nbsp;</Box>
<Box flex={'1 0 0'}>
<I18nLngSelector />
</Box>
</Flex>
<Flex mt={6} alignItems={'center'} w={['85%', '350px']}>
<Box flex={'0 0 80px'}>{t('account_individuation:timezone')}:&nbsp;</Box>
<TimezoneSelect
value={userInfo?.timezone}
onChange={(e) => {
if (!userInfo) return;
onclickSave({ ...userInfo, timezone: e });
}}
/>
</Flex>
</Card>
</Box>
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account', 'account_individuation']))
}
};
}
export default Individuation;

View File

@@ -33,8 +33,8 @@ const ConversionModal = ({
onSuccess() {
router.reload();
},
successToast: t('user:bill.convert_success'),
errorToast: t('user:bill.convert_error')
successToast: t('account_info:exchange_success'),
errorToast: t('account_info:exchange_failure')
});
return (
@@ -43,27 +43,27 @@ const ConversionModal = ({
onClose={onClose}
iconSrc="support/bill/wallet"
iconColor="primary.600"
title={t('user:bill.use_balance')}
title={t('account_info:usage_balance')}
>
<ModalBody maxW={'450px'}>
<VStack px="2.25" gap={2} pb="6">
<HStack px="4" py="2" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" mr="1" />
<Box fontSize={'mini'} fontWeight={'500'}>
{t('user:bill.use_balance_hint')}
{t('account_info:usage_balance_notice')}
</Box>
</HStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('user:bill.current_token_price')}
{t('account_info:current_token_price')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
15/1000 {t('user:bill.tokens')}/{t('common:common.month')}
15/1000 {t('account_info:tokens')}/{t('account_info:month')}
</Box>
</VStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('user:bill.balance')}
{t('account_info:balance')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
{formatStorePrice2Read(userInfo?.team?.balance)?.toFixed(2)}
@@ -71,13 +71,13 @@ const ConversionModal = ({
</VStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('user:bill.you_can_convert')}
{t('account_info:you_can_convert')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
{points} {t('user:bill.tokens')}
{points} {t('account_info:tokens')}
</Box>
<Tag fontSize={'xs'} fontWeight={'500'}>
{t('user:bill.token_expire_1year')}
{t('account_info:token_validity_period')}
</Tag>
</VStack>
@@ -90,10 +90,10 @@ const ConversionModal = ({
onClick={onConvert}
isLoading={loading}
>
{t('user:bill.conversion')}
{t('account_info:exchange')}
</Button>
<Link fontSize={'sm'} color="primary" mt="2" onClick={onOpenContact}>
{t('user:bill.contact_customer_service')}
{t('account_info:contact_customer_service')}
</Link>
</VStack>
</VStack>

View File

@@ -25,7 +25,7 @@ const OpenAIAccountModal = ({
onSuccess(res) {
onClose();
},
errorToast: t('common:user.Set OpenAI Account Failed')
errorToast: t('account_info:openai_account_setting_exception')
});
return (
@@ -33,11 +33,11 @@ const OpenAIAccountModal = ({
isOpen
onClose={onClose}
iconSrc="common/openai"
title={t('common:user.OpenAI Account Setting')}
title={t('account_info:openai_account_configuration')}
>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('common:info.open_api_notice')}
{t('account_info:open_api_notice')}
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>
@@ -48,16 +48,16 @@ const OpenAIAccountModal = ({
<Input
flex={1}
{...register('baseUrl')}
placeholder={t('common:info.open_api_placeholder')}
placeholder={t('account_info:request_address_notice')}
></Input>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
{t('common:common.Confirm')}
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -47,8 +47,8 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
initUserInfo();
onClose();
},
successToast: t('user:bind_inform_account_success'),
errorToast: t('user:bind_inform_account_error')
successToast: t('account_info:bind_notification_success'),
errorToast: t('account_info:bind_notification_error')
}
);
@@ -58,9 +58,9 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
?.map((item) => {
switch (item) {
case 'email':
return t('common:support.user.login.Email');
return t('account_info:email_label');
case 'phone':
return t('common:support.user.login.Phone number');
return t('account_info:phone_label');
}
})
.join('/');
@@ -71,16 +71,16 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('common:user.Notification Receive')}
title={t('account_info:notification_receiving_hint')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('user:notification.Bind Notification Pipe Hint')}</Box>
<Box fontSize={'sm'}>{t('account_info:bind_notification_hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
<Box flex={'0 0 70px'}>{t('account_info:user_account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
@@ -89,12 +89,12 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('user:password.verification_code')}</Box>
<Box flex={'0 0 70px'}>{t('account_info:verification_code_required')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('user:password.code_required')}
placeholder={t('account_info:code_required')}
></Input>
<SendCodeBox username={account} />
</Flex>
@@ -102,14 +102,14 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
{t('account_info:cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('common:common.Confirm')}
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -25,15 +25,15 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: (data: FormType) => {
if (data.newPsw !== data.confirmPsw) {
return Promise.reject(t('common:common.Password inconsistency'));
return Promise.reject(t('account_info:password_mismatch'));
}
return updatePasswordByOld(data);
},
onSuccess() {
onClose();
},
successToast: t('common:user.Update password successful'),
errorToast: t('common:user.Update password failed')
successToast: t('account_info:password_update_success'),
errorToast: t('account_info:password_update_error')
});
return (
@@ -41,15 +41,15 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
isOpen
onClose={onClose}
iconSrc="/imgs/modal/password.svg"
title={t('common:user.Update Password')}
title={t('account_info:update_password')}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('common:user.old_password') + ':'}</Box>
<Box flex={'0 0 70px'}>{t('account_info:old_password') + ':'}</Box>
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('common:user.new_password') + ':'}</Box>
<Box flex={'0 0 70px'}>{t('account_info:new_password') + ':'}</Box>
<Input
flex={1}
type={'password'}
@@ -57,13 +57,13 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
required: true,
maxLength: {
value: 60,
message: t('common:user.password_message')
message: t('account_info:password_length_error')
}
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('common:user.confirm_password') + ':'}</Box>
<Box flex={'0 0 70px'}>{t('account_info:confirm_password') + ':'}</Box>
<Input
flex={1}
type={'password'}
@@ -71,7 +71,7 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
required: true,
maxLength: {
value: 60,
message: t('common:user.password_message')
message: t('account_info:password_length_error')
}
})}
></Input>
@@ -79,10 +79,10 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
{t('common:common.Confirm')}
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -65,7 +65,7 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
isOpen
maxW={['90vw', '1200px']}
iconSrc="modal/teamPlans"
title={t('common:support.wallet.Standard Plan Detail')}
title={t('account_info:package_details')}
isCentered
>
<ModalCloseButton onClick={onClose} />
@@ -74,11 +74,11 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
<Table>
<Thead>
<Tr>
<Th>{t('common:support.standard.type')}</Th>
<Th>{t('common:support.standard.storage')}</Th>
<Th>{t('common:support.standard.AI Bonus Points')}</Th>
<Th>{t('user:bill.valid_time')}</Th>
<Th>{t('common:support.standard.due_date')}</Th>
<Th>{t('account_info:type')}</Th>
<Th>{t('account_info:storage_capacity')}</Th>
<Th>{t('account_info:ai_points')}</Th>
<Th>{t('account_info:effective_time')}</Th>
<Th>{t('account_info:expiration_time')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
@@ -121,12 +121,10 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
<StatusTag status={status as packageStatus} />
</Flex>
</Td>
<Td>
{datasetSize ? `${datasetSize + t('common:core.dataset.data.group')}` : '-'}
</Td>
<Td>{datasetSize ? `${datasetSize + t('account_info:group')}` : '-'}</Td>
<Td>
{totalPoints
? `${Math.round(totalPoints - surplusPoints)} / ${totalPoints} ${t('common:support.wallet.subscription.point')}`
? `${Math.round(totalPoints - surplusPoints)} / ${totalPoints} ${t('account_info:ai_points_calculation_standard')}`
: '-'}
</Td>
<Td color={'myGray.600'}>{formatTime2YMDHM(startTime)}</Td>
@@ -143,7 +141,7 @@ const StandDetailModal = ({ onClose }: { onClose: () => void }) => {
<HStack mt={4} color={'primary.700'}>
<MyIcon name={'infoRounded'} w={'1rem'} />
<Box fontSize={'mini'} fontWeight={'500'}>
{t('user:bill.standard_valid_tip')}
{t('account_info:package_usage_rules')}
</Box>
</HStack>
</ModalBody>
@@ -155,9 +153,9 @@ function StatusTag({ status }: { status: packageStatus }) {
const { t } = useTranslation();
const statusText = useMemo(() => {
return {
inactive: t('common:support.wallet.subscription.status.inactive'),
active: t('common:support.wallet.subscription.status.active'),
expired: t('common:support.wallet.subscription.status.expired')
inactive: t('account_info:pending_usage'),
active: t('account_info:active'),
expired: t('account_info:expired')
};
}, [t]);
const styleMap = useMemo(() => {

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import {
Box,
Flex,
@@ -9,7 +9,8 @@ import {
Link,
Progress,
Grid,
BoxProps
BoxProps,
FlexProps
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
@@ -25,7 +26,6 @@ import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRouter } from 'next/router';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { putUpdateMemberName } from '@/web/support/user/team/api';
import { getDocPath } from '@/web/common/system/doc';
@@ -35,10 +35,7 @@ import {
standardSubLevelMap
} from '@fastgpt/global/support/wallet/sub/constants';
import { formatTime2YMD } from '@fastgpt/global/common/string/time';
import {
AI_POINT_USAGE_CARD_ROUTE,
EXTRA_PLAN_CARD_ROUTE
} from '@/web/support/wallet/sub/constants';
import { getExtraPlanCardRoute } from '@/web/support/wallet/sub/constants';
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
@@ -46,28 +43,33 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import AccountContainer from '../components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
import { useRouter } from 'next/router';
import TeamSelector from '../components/TeamSelector';
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
const ConversionModal = dynamic(() => import('./ConversionModal'));
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./UpdateNotificationModal'));
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'));
const StandDetailModal = dynamic(() => import('./components/standardDetailModal'));
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./components/UpdateNotificationModal'));
const OpenAIAccountModal = dynamic(() => import('./components/OpenAIAccountModal'));
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const AiPointsModal = dynamic(() =>
import('@/pages/price/components/Points').then((mod) => mod.AiPointsModal)
);
const Account = () => {
const Info = () => {
const { isPc } = useSystem();
const { teamPlanStatus } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants;
const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure();
const { initUserInfo } = useUserStore();
useQuery(['init'], initUserInfo);
return (
<>
<AccountContainer>
<Box py={[3, '28px']} px={[5, 10]} mx={'auto'}>
{isPc ? (
<Flex justifyContent={'center'} maxW={'1080px'}>
@@ -92,11 +94,19 @@ const Account = () => {
)}
</Box>
{isOpenContact && <CommunityModal onClose={onCloseContact} />}
</>
</AccountContainer>
);
};
export default React.memo(Account);
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account', 'account_info', 'user']))
}
};
}
export default React.memo(Info);
const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
@@ -110,6 +120,8 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
const standardPlan = teamPlanStatus?.standardConstants;
const { isPc } = useSystem();
const { toast } = useToast();
const router = useRouter();
const {
isOpen: isOpenConversionModal,
onClose: onCloseConversionModal,
@@ -139,7 +151,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
});
reset(data);
toast({
title: t('common:dataset.data.Update Success Tip'),
title: t('account_info:update_success_tip'),
status: 'success'
});
},
@@ -164,7 +176,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
});
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : t('common:common.error.Select avatar failed'),
title: typeof err === 'string' ? err : t('account_info:avatar_selection_exception'),
status: 'warning'
});
}
@@ -184,16 +196,16 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
{isPc && (
<Flex alignItems={'center'} fontSize={'md'} h={'30px'}>
<MyIcon mr={2} name={'support/user/userLight'} w={'1.25rem'} />
{t('common:support.user.User self info')}
{t('account_info:personal_information')}
</Flex>
)}
<Box mt={[0, 6]} fontSize={'sm'}>
{isPc ? (
<Flex alignItems={'center'} cursor={'pointer'}>
<Box {...labelStyles}>{t('common:support.user.Avatar')}:&nbsp;</Box>
<Box {...labelStyles}>{t('account_info:avatar')}:&nbsp;</Box>
<MyTooltip label={t('common:common.avatar.Select Avatar')}>
<MyTooltip label={t('account_info:select_avatar')}>
<Box
w={['44px', '56px']}
h={['44px', '56px']}
@@ -216,7 +228,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
cursor={'pointer'}
onClick={onOpenSelectFile}
>
<MyTooltip label={t('common:common.avatar.Select Avatar')}>
<MyTooltip label={t('account_info:choose_avatar')}>
<Box
w={['44px', '54px']}
h={['44px', '54px']}
@@ -233,17 +245,17 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
<MyIcon mr={1} name={'edit'} w={'14px'} />
{t('common:user.Replace')}
{t('account_info:change')}
</Flex>
</Flex>
)}
{feConfigs?.isPlus && (
<Flex mt={[0, 4]} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Member Name')}:&nbsp;</Box>
<Box {...labelStyles}>{t('account_info:member_name')}:&nbsp;</Box>
<Input
flex={'1 0 0'}
defaultValue={userInfo?.team?.memberName || 'Member'}
title={t('common:user.Edit name')}
title={t('account_info:click_modify_nickname')}
borderColor={'transparent'}
transform={'translateX(-11px)'}
maxLength={20}
@@ -258,21 +270,21 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
</Flex>
)}
<Flex alignItems={'center'} mt={6}>
<Box {...labelStyles}>{t('common:user.Account')}:&nbsp;</Box>
<Box {...labelStyles}>{t('account_info:user_account')}:&nbsp;</Box>
<Box flex={1}>{userInfo?.username}</Box>
</Flex>
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Password')}:&nbsp;</Box>
<Box {...labelStyles}>{t('account_info:password')}:&nbsp;</Box>
<Box flex={1}>*****</Box>
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdatePsw}>
{t('common:user.Change')}
{t('account_info:change')}
</Button>
</Flex>
)}
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Notification Receive')}:&nbsp;</Box>
<Box {...labelStyles}>{t('account_info:notification_receiving')}:&nbsp;</Box>
<Box
flex={1}
{...(!userInfo?.team.notificationAccount && userInfo?.permission.isOwner
@@ -282,35 +294,35 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
{userInfo?.team.notificationAccount
? userInfo?.team.notificationAccount
: userInfo?.permission.isOwner
? t('common:user.Notification Receive Bind')
: t('user:notification.remind_owner_bind')}
? t('account_info:please_bind_notification_receiving_path')
: t('account_info:reminder_create_bound_notification_account')}
</Box>
{userInfo?.permission.isOwner && (
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdateNotification}>
{t('common:user.Change')}
{t('account_info:change')}
</Button>
)}
</Flex>
)}
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.Team')}:&nbsp;</Box>
<Box flex={1}>
<TeamMenu />
</Box>
<Box {...labelStyles}>{t('account_info:user_team_team_name')}:&nbsp;</Box>
<Flex flex={'1 0 0'} w={0} align={'center'}>
<TeamSelector height={'28px'} w={'100%'} showManage />
</Flex>
</Flex>
{feConfigs?.isPlus && (userInfo?.team?.balance ?? 0) > 0 && (
<Box mt={6} whiteSpace={'nowrap'}>
<Flex alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.team.Balance')}:&nbsp;</Box>
<Box {...labelStyles}>{t('account_info:team_balance')}:&nbsp;</Box>
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>{' '}
{t('user:bill.yuan')}
{t('account_info:yuan')}
</Box>
{userInfo?.permission.hasManagePer && !!standardPlan && (
<Button variant={'primary'} size={'sm'} ml={5} onClick={onOpenConversionModal}>
{t('user:bill.conversion')}
{t('account_info:exchange')}
</Button>
)}
</Flex>
@@ -331,6 +343,7 @@ const PlanUsage = () => {
const router = useRouter();
const { t } = useTranslation();
const { userInfo, initUserInfo, teamPlanStatus } = useUserStore();
const { subPlans } = useSystemStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
@@ -340,6 +353,11 @@ const PlanUsage = () => {
onClose: onCloseStandardModal,
onOpen: onOpenStandardModal
} = useDisclosure();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const planName = useMemo(() => {
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
@@ -373,7 +391,7 @@ const PlanUsage = () => {
return {
colorScheme: 'green',
value: 0,
maxSize: t('common:common.Unlimited'),
maxSize: t('account_info:unlimited'),
usedSize: 0
};
}
@@ -388,7 +406,7 @@ const PlanUsage = () => {
return {
colorScheme,
value: rate * 100,
maxSize: teamPlanStatus.datasetMaxSize || t('common:common.Unlimited'),
maxSize: teamPlanStatus.datasetMaxSize || t('account_info:unlimited'),
usedSize: teamPlanStatus.usedDatasetSize
};
}, [teamPlanStatus, t]);
@@ -397,7 +415,7 @@ const PlanUsage = () => {
return {
colorScheme: 'green',
value: 0,
maxSize: t('common:common.Unlimited'),
maxSize: t('account_info:unlimited'),
usedSize: 0
};
}
@@ -413,7 +431,7 @@ const PlanUsage = () => {
return {
colorScheme,
value: rate * 100,
max: teamPlanStatus.totalPoints ? teamPlanStatus.totalPoints : t('common:common.Unlimited'),
max: teamPlanStatus.totalPoints ? teamPlanStatus.totalPoints : t('account_info:unlimited'),
used: teamPlanStatus.usedPoints ? Math.round(teamPlanStatus.usedPoints) : 0
};
}, [teamPlanStatus, t]);
@@ -423,13 +441,13 @@ const PlanUsage = () => {
<Flex fontSize={['md', 'lg']} h={'30px'}>
<Flex alignItems={'center'}>
<MyIcon mr={2} name={'support/account/plans'} w={'20px'} />
{t('common:support.wallet.subscription.Team plan and usage')}
{t('account_info:package_and_usage')}
</Flex>
<Button ml={4} size={'sm'} onClick={() => router.push(AI_POINT_USAGE_CARD_ROUTE)}>
{t('common:support.user.Price')}
<Button ml={4} size={'sm'} onClick={onOpenAiPointsModal}>
{t('account_info:billing_standard')}
</Button>
<Button ml={4} variant={'whitePrimary'} size={'sm'} onClick={onOpenStandardModal}>
{t('common:support.wallet.Standard Plan Detail')}
{t('account_info:package_details')}
</Button>
</Flex>
<Box
@@ -442,25 +460,33 @@ const PlanUsage = () => {
<Flex px={[5, 7]} pt={[3, 6]}>
<Box flex={'1 0 0'}>
<Box color={'myGray.600'} fontSize="sm">
{t('common:support.wallet.subscription.Current plan')}
{t('account_info:current_package')}
</Box>
<Box fontWeight={'bold'} fontSize="lg">
{t(planName as any)}
</Box>
</Box>
<Button onClick={() => router.push('/price')} w={'8rem'} size="sm">
{t('common:support.wallet.subscription.Upgrade plan')}
<Button
onClick={() => {
router.push(
subPlans?.planDescriptionUrl ? getDocPath(subPlans.planDescriptionUrl) : '/price'
);
}}
w={'8rem'}
size="sm"
>
{t('account_info:upgrade_package')}
</Button>
</Flex>
<Box px={[5, 7]} pb={[3, 6]}>
{isFreeTeam && (
<Box mt="2" color={'#485264'} fontSize="sm">
{t('common:info.free_plan')}
{t('account_info:account_knowledge_base_cleanup_warning')}
</Box>
)}
{standardPlan.currentSubLevel !== StandardSubLevelEnum.free && (
<Flex mt="2" color={'#485264'} fontSize="xs">
<Box>{t('common:support.wallet.Plan expired time')}:</Box>
<Box>{t('account_info:package_expiry_time')}:</Box>
<Box ml={2}>{formatTime2YMD(standardPlan?.expiredTime)}</Box>
</Flex>
)}
@@ -488,14 +514,14 @@ const PlanUsage = () => {
<Flex>
<Flex flex={'1 0 0'} alignItems={'flex-end'}>
<Box fontSize={'md'} fontWeight={'bold'} color={'myGray.900'}>
{t('common:info.resource')}
{t('account_info:resource_usage')}
</Box>
<Box ml={1} display={['none', 'block']} fontSize={'xs'} color={'myGray.500'}>
{t('common:info.include')}
{t('account_info:standard_package_and_extra_resource_package')}
</Box>
</Flex>
<Link
href={getWebReqUrl(EXTRA_PLAN_CARD_ROUTE)}
href={getWebReqUrl(getExtraPlanCardRoute())}
transform={'translateX(15px)'}
display={'flex'}
alignItems={'center'}
@@ -503,7 +529,7 @@ const PlanUsage = () => {
cursor={'pointer'}
fontSize={'sm'}
>
{t('common:info.buy_extra')}
{t('account_info:purchase_extra_package')}
<MyIcon ml={1} name={'common/rightArrowLight'} w={'12px'} />
</Link>
</Flex>
@@ -511,7 +537,7 @@ const PlanUsage = () => {
<Flex alignItems={'center'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'} color={'myGray.900'}>
{t('common:support.user.team.Dataset usage')}
{t('account_info:knowledge_base_capacity')}
</Box>
<Box color={'myGray.600'} ml={2}>
{datasetUsageMap.usedSize}/{datasetUsageMap.maxSize}
@@ -535,12 +561,9 @@ const PlanUsage = () => {
<Flex alignItems={'center'}>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'} color={'myGray.900'}>
{t('common:support.wallet.subscription.AI points usage')}
{t('account_info:ai_points_usage')}
</Box>
<QuestionTip
ml={1}
label={t('common:support.wallet.subscription.AI points usage tip')}
></QuestionTip>
<QuestionTip ml={1} label={t('account_info:ai_points_usage_tip')}></QuestionTip>
<Box color={'myGray.600'} ml={2}>
{aiPointsUsageMap.used}/{aiPointsUsageMap.max}
</Box>
@@ -561,6 +584,7 @@ const PlanUsage = () => {
</Box>
</Box>
{isOpenStandardModal && <StandDetailModal onClose={onCloseStandardModal} />}
{isOpenAiPointsModal && <AiPointsModal onClose={onCloseAiPointsModal} />}
</Box>
) : null;
};
@@ -570,7 +594,9 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { isPc } = useSystem();
const { userInfo, updateUserInfo } = useUserStore();
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
@@ -586,12 +612,26 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
});
reset(data);
toast({
title: t('common:dataset.data.Update Success Tip'),
title: t('account_info:update_success_tip'),
status: 'success'
});
},
[reset, toast, updateUserInfo]
[reset, t, toast, updateUserInfo]
);
const buttonStyles = useRef<FlexProps>({
bg: 'white',
py: 3,
px: 6,
border: theme.borders.sm,
borderWidth: '1.5px',
borderRadius: 'md',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none',
fontSize: 'sm'
});
return (
<Box>
<Grid gridGap={4} mt={3}>
@@ -613,50 +653,32 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
>
<MyIcon name={'common/courseLight'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{t('common:system.Help Document')}
</Box>
</Link>
)}
{feConfigs?.chatbotUrl && (
<Link
href={feConfigs?.chatbotUrl}
target="_blank"
display={'flex'}
py={3}
px={6}
bg={'white'}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
userSelect={'none'}
textDecoration={'none !important'}
fontSize={'sm'}
>
<MyIcon name={'core/app/aiLight'} w={'18px'} />
<Box ml={2} flex={1}>
{t('common:common.system.Help Chatbot')}
{t('account_info:help_document')}
</Box>
</Link>
)}
{!isPc &&
feConfigs?.navbarItems
?.filter((item) => item.isActive)
.map((item) => (
<Flex
key={item.id}
{...buttonStyles.current}
onClick={() => window.open(item.url, '_blank')}
>
<Avatar src={item.avatar} w={'18px'} />
<Box ml={2} flex={1}>
{item.name}
</Box>
</Flex>
))}
{feConfigs?.lafEnv && userInfo?.team.role === TeamMemberRoleEnum.owner && (
<Flex
bg={'white'}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
cursor={'pointer'}
userSelect={'none'}
onClick={onOpenLaf}
fontSize={'sm'}
>
<Flex {...buttonStyles.current} onClick={onOpenLaf}>
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<Box ml={2} flex={1}>
{'laf' + t('common:navbar.Account')}
{'laf' + t('account_info:account_duplicate')}
</Box>
<Box
w={'9px'}
@@ -668,22 +690,10 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
)}
{feConfigs?.show_openai_account && (
<Flex
bg={'white'}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
cursor={'pointer'}
userSelect={'none'}
onClick={onOpenOpenai}
fontSize={'sm'}
>
<Flex {...buttonStyles.current} onClick={onOpenOpenai}>
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{'OpenAI / OneAPI' + t('common:navbar.Account')}
{'OpenAI / OneAPI' + t('account_info:account_duplicate')}
</Box>
<Box
w={'9px'}
@@ -702,7 +712,7 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
h={'48px'}
fontSize={'sm'}
>
{t('common:system.Concat us')}
{t('account_info:contact_us')}
</Button>
)}
</Grid>

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { Box, Button, Flex, useTheme } from '@chakra-ui/react';
import { getInforms, readInform } from '@/web/support/user/inform/api';
import type { UserInformSchema } from '@fastgpt/global/support/user/inform/type';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import AccountContainer, { TabEnum } from './components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
const InformTable = () => {
const { t } = useTranslation();
const theme = useTheme();
const { Loading } = useLoading();
const {
data: informs,
isLoading,
total,
pageSize,
Pagination,
getData,
pageNum
} = usePagination<UserInformSchema>({
api: getInforms,
pageSize: 20
});
return (
<AccountContainer>
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Box px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
{informs.map((item) => (
<Box
key={item._id}
border={theme.borders.md}
py={2}
px={4}
borderRadius={'md'}
position={'relative'}
_notLast={{ mb: 3 }}
>
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{item.title}</Box>
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
</Box>
{!item.read && (
<Button
variant={'whitePrimary'}
size={'xs'}
onClick={async () => {
if (!item.read) {
await readInform(item._id);
getData(pageNum);
}
}}
>
{t('account_inform:read')}
</Button>
)}
</Flex>
<Box mt={2} fontSize={'sm'} color={'myGray.600'} whiteSpace={'pre-wrap'}>
{item.content}
</Box>
{!item.read && (
<>
<Box
w={'5px'}
h={'5px'}
borderRadius={'10px'}
bg={'red.600'}
position={'absolute'}
top={'8px'}
left={'8px'}
/>
</>
)}
</Box>
))}
{!isLoading && informs.length === 0 && (
<EmptyTip text={t('account_inform:no_notifications')}></EmptyTip>
)}
</Box>
{total > pageSize && (
<Flex w={'100%'} mt={4} px={[3, 8]} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
<Loading loading={isLoading && informs.length === 0} fixed={false} />
</Flex>
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account_inform', 'account']))
}
};
}
export default InformTable;

View File

@@ -0,0 +1,153 @@
import React from 'react';
import {
Grid,
Box,
Flex,
BoxProps,
useTheme,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { getPromotionInitData, getPromotionRecords } from '@/web/support/activity/promotion/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import dayjs from 'dayjs';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import AccountContainer, { TabEnum } from './components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
const Promotion = () => {
const { t } = useTranslation();
const theme = useTheme();
const { copyData } = useCopyData();
const { userInfo } = useUserStore();
const { Loading } = useLoading();
const {
data: promotionRecords,
isLoading,
total,
pageSize,
Pagination
} = usePagination({
api: getPromotionRecords,
pageSize: 20
});
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
['getPromotionInitData'],
getPromotionInitData
);
const statisticsStyles: BoxProps = {
p: [4, 5],
border: theme.borders.base,
textAlign: 'center',
fontSize: ['md', 'lg'],
borderRadius: 'md'
};
const titleStyles: BoxProps = {
mt: 2,
fontSize: ['lg', '28px'],
fontWeight: 'bold'
};
return (
<AccountContainer>
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
<Box {...statisticsStyles}>
<Box>{t('account_promotion:total_invited')}</Box>
<Box {...titleStyles}>{invitedAmount}</Box>
</Box>
<Box {...statisticsStyles}>
<Box>{t('account_promotion:earnings')}</Box>
<Box {...titleStyles}>{earningsAmount}</Box>
</Box>
<Box {...statisticsStyles}>
<Flex alignItems={'center'} justifyContent={'center'}>
<Box>{t('account_promotion:cashback_ratio')}</Box>
<QuestionTip
ml={1}
label={t('account_promotion:cashback_ratio_description')}
></QuestionTip>
</Flex>
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
</Box>
<Box {...statisticsStyles}>
<Flex alignItems={'center'} justifyContent={'center'}>
<Box>{t('account_promotion:invite_url')}</Box>
<QuestionTip ml={1} label={t('account_promotion:invite_url_tip')}></QuestionTip>
</Flex>
<Button
mt={4}
variant={'whitePrimary'}
fontSize={'sm'}
onClick={() => {
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
}}
>
{t('account_promotion:copy_invite_link')}
</Button>
</Box>
</Grid>
<Box mt={5}>
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
<Table>
<Thead>
<Tr>
<Th>{t('account_promotion:time')}</Th>
<Th>{t('account_promotion:type')}</Th>
<Th>{t('account_promotion:amount')}</Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{promotionRecords.map((item) => (
<Tr key={item._id}>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{t(`user:promotion.${item.type}` as any)}</Td>
<Td>{item.amount}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{!isLoading && promotionRecords.length === 0 && (
<EmptyTip text={t('account_promotion:no_invite_records')}></EmptyTip>
)}
{total > pageSize && (
<Flex mt={4} justifyContent={'flex-end'}>
<Pagination />
</Flex>
)}
<Loading loading={isLoading} fixed={false} />
</Box>
</Flex>
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account', 'account_promotion']))
}
};
}
export default Promotion;

View File

@@ -101,11 +101,11 @@ function EditModal({
onClose={onClose}
iconSrc="support/team/group"
iconColor="primary.600"
title={defaultData.id ? t('common:user.team.Update Team') : t('common:user.team.Create Team')}
title={defaultData.id ? t('user:team.Update Team') : t('user:team.Create Team')}
>
<ModalBody>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('common:user.team.Set Name')}
{t('user:team.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common:common.Set Avatar')}>
@@ -125,7 +125,7 @@ function EditModal({
autoFocus
bg={'myWhite.600'}
maxLength={20}
placeholder={t('common:user.team.Team Name')}
placeholder={t('user:team.Team Name')}
{...register('name', {
required: t('common:common.Please Input Name')
})}

View File

@@ -11,7 +11,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../../context';
import { TeamContext } from '../context';
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
@@ -21,7 +21,7 @@ export type GroupFormType = {
};
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { t } = useTranslation();
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
fileType: '.jpg, .jpeg, .png',

View File

@@ -17,7 +17,7 @@ import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../../context';
import { TeamContext } from '../context';
import { putUpdateGroup } from '@/web/support/user/team/group/api';
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
@@ -45,7 +45,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
refetchGroups,
groups,
refetchMembers
} = useContextSelector(TeamModalContext, (v) => v);
} = useContextSelector(TeamContext, (v) => v);
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
@@ -134,14 +134,17 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
title={t('user:team.group.manage_member')}
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
iconColor="primary.600"
minW={['70vw', '800px']}
minW="800px"
h={'100%'}
isCentered
>
<ModalBody flex={1} display={'flex'} flexDirection={'column'} gap={4}>
<ModalBody flex={1}>
<Grid
templateColumns="1fr 1fr"
borderRadius="8px"
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" p="4">
<SearchInput

View File

@@ -16,7 +16,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react';
import { TeamModalContext } from '../../context';
import { TeamContext } from '../context';
import { useContextSelector } from 'use-context-selector';
export type ChangeOwnerModalProps = {
@@ -29,11 +29,7 @@ export function ChangeOwnerModal({
}: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState('');
const {
members: allMembers,
groups,
refetchGroups
} = useContextSelector(TeamModalContext, (v) => v);
const { members: allMembers, groups, refetchGroups } = useContextSelector(TeamContext, (v) => v);
const group = useMemo(() => {
return groups.find((item) => item._id === groupId);
}, [groupId, groups]);
@@ -180,7 +176,7 @@ export function ChangeOwnerModal({
setKeepAdmin(e.target.checked);
}}
>
{t('user:team.group.keep_admin')}
{t('account_team:retain_admin_permissions')}
</Checkbox>
</Box>
</Flex>

View File

@@ -15,14 +15,14 @@ import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../../context';
import { TeamContext } from '../context';
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup } from '@/web/support/user/team/group/api';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../Info/MemberTag';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useState } from 'react';
@@ -42,11 +42,11 @@ function MemberTable({
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete',
content: t('user:team.group.delete_confirm')
content: t('account_team:confirm_delete_group')
});
const { groups, refetchGroups, members, refetchMembers } = useContextSelector(
TeamModalContext,
TeamContext,
(v) => v
);
@@ -80,15 +80,15 @@ function MemberTable({
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('user:team.group.name')}
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('user:owner')}</Th>
<Th bg="myGray.100">{t('user:team.group.members')}</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
@@ -135,7 +135,7 @@ function MemberTable({
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('user:team.group.manage_member')}>
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<AvatarGroup
avatars={group.members.map(
@@ -162,14 +162,14 @@ function MemberTable({
{
children: [
{
label: t('user:team.group.edit_info'),
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('user:team.group.manage_member'),
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
@@ -178,7 +178,7 @@ function MemberTable({
...(isGroupOwner(group)
? [
{
label: t('user:team.group.transfer_owner'),
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);

View File

@@ -19,7 +19,7 @@ const InviteModal = ({
}) => {
const { t } = useTranslation();
const { ConfirmModal, openConfirm } = useConfirm({
title: t('common:user.team.Invite Member Result Tip'),
title: t('user:team.Invite Member Result Tip'),
showCancel: false
});
@@ -38,7 +38,7 @@ const InviteModal = ({
() => onClose(),
undefined,
<Box whiteSpace={'pre-wrap'}>
{t('user.team.Invite Member Success Tip', {
{t('user:team.Invite Member Success Tip', {
success: res.invite.length,
inValid: res.inValid.map((item) => item.username).join(', '),
inTeam: res.inTeam.map((item) => item.username).join(', ')
@@ -46,7 +46,7 @@ const InviteModal = ({
</Box>
)();
},
errorToast: t('common:user.team.Invite Member Failed Tip')
errorToast: t('user:team.Invite Member Failed Tip')
}
);
@@ -79,7 +79,7 @@ const InviteModal = ({
isLoading={isLoading}
onClick={onInvite}
>
{t('common:user.team.Confirm Invite')}
{t('user:team.Confirm Invite')}
</Button>
</ModalFooter>
<ConfirmModal />

View File

@@ -10,7 +10,7 @@ import Tag from '@fastgpt/web/components/common/Tag';
import Icon from '@fastgpt/web/components/common/Icon';
import GroupTags from '@/components/support/permission/Group/GroupTags';
import { useContextSelector } from 'use-context-selector';
import { TeamModalContext } from '../context';
import { TeamContext } from './context';
function MemberTable() {
const { userInfo } = useUserStore();
@@ -21,20 +21,20 @@ function MemberTable() {
});
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
TeamModalContext,
TeamContext,
(v) => v
);
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'} mx="6">
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('common:common.Username')}
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('user:team.belong_to_group')}</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
@@ -50,7 +50,7 @@ function MemberTable() {
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('common:user.team.member.waiting')}
{t('account_team:waiting')}
</Tag>
)}
</Box>
@@ -85,7 +85,7 @@ function MemberTable() {
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('common:user.team.Remove Member Confirm Tip', {
t('account_team:remove_tip', {
username: item.memberName
})
)();

View File

@@ -17,10 +17,10 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getTeamClbs, updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamModalContext } from '../../context';
import { TeamContext } from '../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../Info/MemberTag';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamManagePermissionVal,
@@ -33,7 +33,7 @@ function PermissionManage() {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
TeamModalContext,
TeamContext,
(v) => v
);
@@ -179,7 +179,7 @@ function PermissionManage() {
const userManage = userInfo?.permission.hasManagePer;
return (
<TableContainer fontSize={'sm'} mx="6">
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr bg={'white !important'}>

View File

@@ -1,6 +1,6 @@
import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './components/EditInfoModal';
import type { EditTeamFormDataType } from './EditInfoModal';
import dynamic from 'next/dynamic';
import { getTeamList, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
@@ -11,7 +11,7 @@ import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
const EditInfoModal = dynamic(() => import('./components/EditInfoModal'));
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
type TeamModalContextType = {
myTeams: TeamTmbItemType[];
@@ -26,9 +26,10 @@ type TeamModalContextType = {
refetchGroups: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
teamSize: number;
};
export const TeamModalContext = createContext<TeamModalContextType>({
export const TeamContext = createContext<TeamModalContextType>({
myTeams: [],
groups: [],
members: [],
@@ -52,7 +53,8 @@ export const TeamModalContext = createContext<TeamModalContextType>({
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
throw new Error('Function not implemented.');
}
},
teamSize: 0
});
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
@@ -120,11 +122,12 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
members,
refetchMembers,
groups,
refetchGroups
refetchGroups,
teamSize: members.length
};
return (
<TeamModalContext.Provider value={contextValue}>
<TeamContext.Provider value={contextValue}>
{userInfo?.team?.permission && (
<>
{children}
@@ -140,6 +143,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
)}
</>
)}
</TeamModalContext.Provider>
</TeamContext.Provider>
);
};
export default TeamModalContextProvider;

View File

@@ -0,0 +1,329 @@
import { serviceSideProps } from '@/web/common/utils/i18n';
import AccountContainer from '../components/AccountContainer';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector';
import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { useRouter } from 'next/router';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { delLeaveTeam } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamContext, TeamModalContextProvider } from './components/context';
import dynamic from 'next/dynamic';
import TeamTagModal from '@/components/support/user/team/TeamTagModal';
import MemberTable from './components/MemberTable';
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
export enum TeamTabEnum {
member = 'member',
group = 'group',
permission = 'permission'
}
const Team = () => {
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const {
myTeams,
refetchTeams,
members,
refetchMembers,
setEditTeamData,
onSwitchTeam,
searchKey,
setSearchKey,
teamSize,
isLoading
} = useContextSelector(TeamContext, (v) => v);
const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const { runAsync: onLeaveTeam, loading: isLoadingLeaveTeam } = useRequest2(
async (teamId?: string) => {
if (!teamId) return;
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
const [editGroupId, setEditGroupId] = useState<string>();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
return (
<AccountContainer isLoading={isLoading}>
{/* header */}
<Flex
w={'100%'}
h={'3.5rem'}
px={'1.56rem'}
py={'0.56rem'}
borderBottom={'1px solid'}
borderColor={'myGray.200'}
bg={'myGray.25'}
align={'center'}
gap={6}
justify={'space-between'}
>
<Flex align={'center'}>
<Flex gap={2} color={'myGray.900'}>
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
<Box fontWeight={'500'} fontSize={'1rem'}>
{t('account:team')}
</Box>
</Flex>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
</Flex>
)}
</Flex>
<Box
float={'right'}
color={'myGray.900'}
h={'1.25rem'}
px={'0.5rem'}
py={'0.125rem'}
fontSize={'0.75rem'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
>
{t('account_team:total_team_members', { amount: teamSize })}
</Box>
</Flex>
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
<Flex alignItems={'center'}>
{teamTab === TeamTabEnum.member &&
userInfo?.team.permission.hasManagePer &&
feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('account_team:label_sync')}
</Button>
)}
{teamTab === TeamTabEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('account_team:user_team_invite_member')}
</Button>
)}
{teamTab === TeamTabEnum.member && !userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
isLoading={isLoadingLeaveTeam}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('account_team:user_team_leave_team')}
</Button>
)}
{teamTab === TeamTabEnum.group && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
{teamTab === TeamTabEnum.permission && (
<Box ml="auto">
<SearchInput
placeholder={t('user:team.group.search_placeholder')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
)}
</Flex>
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{teamTab === TeamTabEnum.member && <MemberTable />}
{teamTab === TeamTabEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{teamTab === TeamTabEnum.permission && <PermissionManage />}
</Box>
</Box>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
<ConfirmLeaveTeamModal />
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account', 'account_team', 'user']))
}
};
}
const Render = () => {
const { userInfo } = useUserStore();
return !!userInfo?.team ? (
<TeamModalContextProvider>
<Team />
</TeamModalContextProvider>
) : null;
};
export default React.memo(Render);

View File

@@ -63,44 +63,44 @@ const UsageDetail = ({ usage, onClose }: { usage: UsageItemType; onClose: () =>
isOpen={true}
onClose={onClose}
iconSrc="/imgs/modal/bill.svg"
title={t('common:support.wallet.usage.Usage Detail')}
title={t('account_usage:usage_detail')}
maxW={['90vw', '700px']}
>
<ModalBody>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.bill.Number')}:</FormLabel>
<FormLabel flex={'0 0 80px'}>{t('account_usage:order_number')}:</FormLabel>
<Box>{usage.id}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.Time')}:</FormLabel>
<FormLabel flex={'0 0 80px'}>{t('account_usage:generation_time')}:</FormLabel>
<Box>{dayjs(usage.time).format('YYYY/MM/DD HH:mm:ss')}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.App name')}:</FormLabel>
<FormLabel flex={'0 0 80px'}>{t('account_usage:app_name')}:</FormLabel>
<Box>{t(usage.appName as any) || '-'}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.Source')}:</FormLabel>
<FormLabel flex={'0 0 80px'}>{t('account_usage:source')}:</FormLabel>
<Box>{t(UsageSourceMap[usage.source]?.label as any)}</Box>
</Flex>
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 80px'}>{t('common:support.wallet.usage.Total points')}:</FormLabel>
<FormLabel flex={'0 0 80px'}>{t('account_usage:total_points_consumed')}:</FormLabel>
<Box fontWeight={'bold'}>{formatNumber(usage.totalPoints)}</Box>
</Flex>
<Box pb={4}>
<FormLabel flex={'0 0 80px'} mb={1}>
{t('common:support.wallet.usage.Bill Module')}
{t('account_usage:billing_module')}
</FormLabel>
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr>
<Th>{t('common:support.wallet.usage.Module name')}</Th>
{hasModel && <Th>{t('common:support.wallet.usage.Ai model')}</Th>}
{hasToken && <Th>{t('common:support.wallet.usage.Token Length')}</Th>}
{hasCharsLen && <Th>{t('common:support.wallet.usage.Text Length')}</Th>}
{hasDuration && <Th>{t('common:support.wallet.usage.Duration')}</Th>}
<Th>{t('common:support.wallet.usage.Total points')}</Th>
<Th>{t('account_usage:module_name')}</Th>
{hasModel && <Th>{t('account_usage:ai_model')}</Th>}
{hasToken && <Th>{t('account_usage:token_length')}</Th>}
{hasCharsLen && <Th>{t('account_usage:text_length')}</Th>}
{hasDuration && <Th>{t('account_usage:duration_seconds')}</Th>}
<Th>{t('account_usage:total_points_consumed')}</Th>
</Tr>
</Thead>
<Tbody>

View File

@@ -0,0 +1,212 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Flex,
Box,
Button
} from '@chakra-ui/react';
import { UsageSourceEnum, UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import { getUserUsages } from '@/web/support/wallet/usage/api';
import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
import { usePagination } from '@fastgpt/web/hooks/usePagination';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import dayjs from 'dayjs';
import DateRangePicker, {
type DateRangeType
} from '@fastgpt/web/components/common/DateRangePicker';
import { addDays } from 'date-fns';
import dynamic from 'next/dynamic';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import AccountContainer, { TabEnum } from '../components/AccountContainer';
import { serviceSideProps } from '@/web/common/utils/i18n';
const UsageDetail = dynamic(() => import('./UsageDetail'));
const UsageTable = () => {
const { t } = useTranslation();
const { Loading } = useLoading();
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
to: new Date()
});
const [usageSource, setUsageSource] = useState<UsageSourceEnum | ''>('');
const { isPc } = useSystem();
const { userInfo, loadAndGetTeamMembers } = useUserStore();
const [usageDetail, setUsageDetail] = useState<UsageItemType>();
const sourceList = useMemo(
() =>
[
{ label: t('account_usage:all'), value: '' },
...Object.entries(UsageSourceMap).map(([key, value]) => ({
label: t(value.label as any),
value: key
}))
] as {
label: never;
value: UsageSourceEnum | '';
}[],
[t]
);
const [selectTmbId, setSelectTmbId] = useState(userInfo?.team?.tmbId);
const { data: members = [] } = useQuery(['getMembers', userInfo?.team?.teamId], () => {
if (!userInfo?.team?.teamId) return [];
return loadAndGetTeamMembers();
});
const tmbList = useMemo(
() =>
members.map((item) => ({
label: (
<Flex alignItems={'center'}>
<Avatar src={item.avatar} w={'16px'} mr={1} />
{item.memberName}
</Flex>
),
value: item.tmbId
})),
[members]
);
const {
data: usages,
isLoading,
Pagination,
getData
} = usePagination<UsageItemType>({
api: getUserUsages,
pageSize: isPc ? 20 : 10,
params: {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
source: usageSource,
teamMemberId: selectTmbId
},
defaultRequest: false
});
useEffect(() => {
getData(1);
}, [usageSource, selectTmbId]);
return (
<AccountContainer>
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
<Flex
flexDir={['column', 'row']}
gap={2}
w={'100%'}
px={[3, 8]}
alignItems={['flex-end', 'center']}
>
{tmbList.length > 1 && userInfo?.team?.permission.hasManagePer && (
<Flex alignItems={'center'}>
<Box mr={2} flexShrink={0}>
{t('account_usage:member')}
</Box>
<MySelect
size={'sm'}
minW={'100px'}
list={tmbList}
value={selectTmbId}
onchange={setSelectTmbId}
/>
</Flex>
)}
<Box flex={'1'} />
<Flex alignItems={'center'} gap={3}>
<DateRangePicker
defaultDate={dateRange}
position="bottom"
onChange={setDateRange}
onSuccess={() => getData(1)}
/>
<Pagination />
</Flex>
</Flex>
<TableContainer
mt={2}
px={[3, 8]}
position={'relative'}
flex={'1 0 0'}
h={0}
overflowY={'auto'}
>
<Table>
<Thead>
<Tr>
{/* <Th>{t('account_usage:user.team.Member Name')}</Th> */}
<Th>{t('account_usage:user_type')}</Th>
<Th>
<MySelect<UsageSourceEnum | ''>
list={sourceList}
value={usageSource}
size={'sm'}
onchange={(e) => {
setUsageSource(e);
}}
w={'130px'}
></MySelect>
</Th>
<Th>{t('account_usage:project_name')}</Th>
<Th>{t('account_usage:total_points')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{usages.map((item) => (
<Tr key={item.id}>
{/* <Td>{item.memberName}</Td> */}
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
<Td>{t(UsageSourceMap[item.source]?.label as any) || '-'}</Td>
<Td>{t(item.appName as any) || '-'}</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button
size={'sm'}
variant={'whitePrimary'}
onClick={() => setUsageDetail(item)}
>
{t('account_usage:details')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
{!isLoading && usages.length === 0 && (
<EmptyTip text={t('account_usage:no_usage_records')}></EmptyTip>
)}
</TableContainer>
<Loading loading={isLoading} fixed={false} />
{!!usageDetail && (
<UsageDetail usage={usageDetail} onClose={() => setUsageDetail(undefined)} />
)}
</Flex>
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account_usage', 'account']))
}
};
}
export default React.memo(UsageTable);

View File

@@ -7,42 +7,62 @@ import { NextAPI } from '@/service/middleware/entry';
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import {
OwnerPermissionVal,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
export type PreviewContextProps = {
datasetId: string;
type: DatasetSourceReadTypeEnum;
sourceId: string;
isQAImport?: boolean;
selector?: string;
externalFileId?: string;
};
async function handler(req: ApiRequestProps<PreviewContextProps>, res: NextApiResponse<any>) {
const { type, sourceId, isQAImport, selector } = req.body;
const { type, sourceId, isQAImport, selector, datasetId, externalFileId } = req.body;
if (!sourceId) {
throw new Error('fileId is empty');
}
const { teamId } = await (async () => {
const { teamId, apiServer } = await (async () => {
if (type === DatasetSourceReadTypeEnum.fileLocal) {
return authCollectionFile({
const res = await authCollectionFile({
req,
authToken: true,
authApiKey: true,
fileId: sourceId,
per: OwnerPermissionVal
});
return {
teamId: res.teamId
};
}
return authCert({ req, authApiKey: true, authToken: true });
const { dataset } = await authDataset({
req,
authApiKey: true,
authToken: true,
datasetId,
per: WritePermissionVal
});
return {
teamId: dataset.teamId,
apiServer: dataset.apiServer
};
})();
const rawText = await readDatasetSourceRawText({
teamId,
type,
sourceId: sourceId,
sourceId,
isQAImport,
selector
selector,
apiServer,
externalFileId
});
return {

View File

@@ -3,11 +3,19 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { pushQuestionGuideUsage } from '@/service/support/wallet/usage/push';
import { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { AuthModeType } from '@fastgpt/service/support/permission/type';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/authLink';
import { authOutLinkInit } from '@/service/support/permission/auth/outLink';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
async function handler(
req: ApiRequestProps<
@@ -52,3 +60,62 @@ async function handler(
}
export default NextAPI(handler);
/*
Abandoned
Different chat source
1. token (header)
2. apikey (header)
3. share page (body: shareId outLinkUid)
4. team chat page (body: teamId teamToken)
*/
async function authChatCert(props: AuthModeType): Promise<{
teamId: string;
tmbId: string;
authType: AuthUserTypeEnum;
apikey: string;
isOwner: boolean;
canWrite: boolean;
outLinkUid?: string;
}> {
const { teamId, teamToken, shareId, outLinkUid } = props.req.body as OutLinkChatAuthProps;
if (shareId && outLinkUid) {
const { outLinkConfig } = await authOutLinkValid({ shareId });
const { uid } = await authOutLinkInit({
outLinkUid,
tokenUrl: outLinkConfig.limit?.hookUrl
});
return {
teamId: String(outLinkConfig.teamId),
tmbId: String(outLinkConfig.tmbId),
authType: AuthUserTypeEnum.outLink,
apikey: '',
isOwner: false,
canWrite: false,
outLinkUid: uid
};
}
if (teamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
const tmb = await MongoTeamMember.findOne(
{ teamId, role: TeamMemberRoleEnum.owner },
'tmbId'
).lean();
if (!tmb) return Promise.reject(ChatErrEnum.unAuthChat);
return {
teamId,
tmbId: String(tmb._id),
authType: AuthUserTypeEnum.teamDomain,
apikey: '',
isOwner: false,
canWrite: false,
outLinkUid: uid
};
}
return authCert(props);
}

View File

@@ -1,47 +1,59 @@
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateQuestionGuideParams } from '@/global/core/ai/api.d';
import { pushQuestionGuideUsage } from '@/service/support/wallet/usage/push';
import { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
export type CreateQuestionGuideParams = OutLinkChatAuthProps & {
appId: string;
chatId: string;
};
async function handler(req: ApiRequestProps<CreateQuestionGuideParams>, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { messages } = req.body;
const { appId, chatId } = req.body;
const { tmbId, teamId } = await authChatCrud({
const [{ tmbId, teamId }] = await Promise.all([
authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.body
});
})
]);
const qgModel = global.llmModels[0];
// Auth app and get questionGuide config
const { result, tokens } = await createQuestionGuide({
messages,
model: qgModel.model
});
// Get histories
const { histories } = await getChatItems({
appId,
chatId,
offset: 0,
limit: 6,
field: 'obj value time'
});
const messages = chats2GPTMessages({ messages: histories, reserveId: false });
jsonRes(res, {
data: result
});
const qgModel = global.llmModels[0];
pushQuestionGuideUsage({
tokens,
teamId,
tmbId
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
const { result, tokens } = await createQuestionGuide({
messages,
model: qgModel.model
});
jsonRes(res, {
data: result
});
pushQuestionGuideUsage({
tokens,
teamId,
tmbId
});
}
export default NextAPI(handler);

View File

@@ -28,33 +28,51 @@ export type ListAppBody = {
async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemType[]> {
const { parentId, type, getRecentlyChat, searchKey } = req.body;
// 凭证校验
const {
app: ParentApp,
tmbId,
teamId,
permission: myPer
} = await (async () => {
if (parentId) {
return await authApp({
req,
authToken: true,
authApiKey: true,
appId: parentId,
per: ReadPermissionVal
// Auth user permission
const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([
authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
}),
...(parentId
? [
authApp({
req,
authToken: true,
authApiKey: true,
appId: parentId,
per: ReadPermissionVal
})
]
: [])
]);
// Get team all app permissions
const [perList, myGroupMap] = await Promise.all([
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: {
$exists: true
}
}).lean(),
getGroupsByTmbId({
tmbId,
teamId
}).then((item) => {
const map = new Map<string, 1>();
item.forEach((item) => {
map.set(String(item._id), 1);
});
} else {
return {
...(await authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
})),
app: undefined
};
}
})();
return map;
})
]);
// Get my permissions
const myPerList = perList.filter(
(item) => String(item.tmbId) === String(tmbId) || myGroupMap.has(String(item.groupId))
);
const findAppsQuery = (() => {
const searchMatch = searchKey
@@ -65,10 +83,15 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
]
}
: {};
// Filter apps by permission, if not owner, only get apps that I have permission to access
const appIdQuery = teamPer.isOwner
? {}
: { _id: { $in: myPerList.map((item) => item.resourceId) } };
if (getRecentlyChat) {
return {
// get all chat app
...appIdQuery,
teamId,
type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.plugin] },
...searchMatch
@@ -77,63 +100,46 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
if (searchKey) {
return {
...appIdQuery,
teamId,
...searchMatch
};
}
return {
...appIdQuery,
teamId,
...(type && (Array.isArray(type) ? { type: { $in: type } } : { type })),
...parseParentIdInMongo(parentId)
};
})();
const limit = (() => {
if (getRecentlyChat) return 15;
if (searchKey) return 20;
return 1000;
})();
/* temp: get all apps and per */
const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
const myApps = await MongoApp.find(
findAppsQuery,
'_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission'
)
.sort({
updateTime: -1
})
).map((item) => String(item._id));
.limit(limit)
.lean();
const [myApps, perList] = await Promise.all([
MongoApp.find(
findAppsQuery,
'_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission'
)
.sort({
updateTime: -1
})
.limit(searchKey ? 20 : 1000)
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
teamId,
resourceId: {
$exists: true
}
}).lean()
]);
// Filter apps by permission
const filterApps = myApps
// Add app permission and filter apps by read permission
const formatApps = myApps
.map((app) => {
const { Per, privateApp } = (() => {
const myPerList = perList.filter(
(item) =>
String(item.tmbId) === String(tmbId) || myGroupIds.includes(String(item.groupId))
);
const getPer = (appId: string) => {
const tmbPer = myPerList.find(
(item) => String(item.resourceId) === appId && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
myPerList
.filter(
(item) =>
String(item.resourceId) === appId && myGroupIds.includes(String(item.groupId))
)
.filter((item) => String(item.resourceId) === appId && !!item.groupId)
.map((item) => item.permission)
);
@@ -143,15 +149,15 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
return {
Per: new AppPermission({
per: tmbPer ?? groupPer ?? AppDefaultPermissionVal,
isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner
isOwner: String(app.tmbId) === String(tmbId) || teamPer.isOwner
}),
privateApp: AppFolderTypeList.includes(app.type) ? clbCount <= 1 : clbCount === 0
};
};
// Inherit app
if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) {
return getPer(String(ParentApp._id));
if (app.inheritPermission && parentId && !AppFolderTypeList.includes(app.type)) {
return getPer(String(parentId));
} else {
return getPer(String(app._id));
}
@@ -165,9 +171,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
})
.filter((app) => app.permission.hasReadPer);
const sliceApps = getRecentlyChat ? filterApps.slice(0, 15) : filterApps;
return sliceApps.map((app) => ({
return formatApps.map((app) => ({
_id: app._id,
tmbId: app.tmbId,
avatar: app.avatar,

View File

@@ -2,12 +2,12 @@ import type { NextApiResponse } from 'next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { NextAPI } from '@/service/middleware/entry';
import { getSystemPlugins } from '@/service/core/app/plugin';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { getSystemPluginCb, getSystemPlugins } from '@/service/core/app/plugin';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
export type GetSystemPluginTemplatesBody = {
searchKey?: string;
@@ -24,6 +24,10 @@ async function handler(
const formatParentId = parentId || null;
// Make sure system plugin callbacks are loaded
if (!global.systemPluginCb || Object.keys(global.systemPluginCb).length === 0)
await getSystemPluginCb();
return getSystemPlugins().then((res) =>
res
// Just show the active plugins
@@ -39,7 +43,9 @@ async function handler(
intro: plugin.intro,
isTool: plugin.isTool,
currentCost: plugin.currentCost,
author: plugin.author
hasTokenFee: plugin.hasTokenFee,
author: plugin.author,
instructions: plugin.userGuide
}))
.filter((item) => {
if (searchKey) {

View File

@@ -6,8 +6,7 @@ import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
PerResourceTypeEnum,
ReadPermissionVal,
WritePermissionVal
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
@@ -16,7 +15,7 @@ import {
syncChildrenPermission,
syncCollaborators
} from '@fastgpt/service/support/permission/inheritPermission';
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ClientSession } from 'mongoose';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller';
@@ -91,7 +90,10 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
const onUpdate = async (session?: ClientSession) => {
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
const { nodes: formatNodes } = beforeUpdateAppFormat({
nodes,
isPlugin: app.type === AppTypeEnum.plugin
});
return MongoApp.findByIdAndUpdate(
appId,

View File

@@ -9,6 +9,7 @@ import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/strin
import { PostPublishAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
async function handler(
req: ApiRequestProps<PostPublishAppProps>,
@@ -17,9 +18,12 @@ async function handler(
const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], chatConfig, isPublish, versionName } = req.body;
const { tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { app, tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });
const { nodes: formatNodes } = beforeUpdateAppFormat({
nodes,
isPlugin: app.type === AppTypeEnum.plugin
});
await mongoSessionRun(async (session) => {
// create version histories

View File

@@ -8,7 +8,6 @@ import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authType2UsageSource } from '@/service/support/wallet/usage/utils';
import { getAudioSpeechModel } from '@fastgpt/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
/*
@@ -93,4 +92,5 @@ async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiRes
}
}
export default NextAPI(handler);
// 不能使用 NextApiResponse
export default handler;

View File

@@ -0,0 +1,36 @@
import { NextAPI } from '@/service/middleware/entry';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { APIFileItem } from '@fastgpt/global/core/dataset/apiDataset';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { NextApiRequest } from 'next';
export type GetApiDatasetFileListProps = {
searchKey?: string;
parentId?: ParentIdType;
datasetId: string;
};
export type GetApiDatasetFileListResponse = APIFileItem[];
async function handler(req: NextApiRequest) {
let { searchKey = '', parentId = null, datasetId } = req.body;
const { dataset } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId,
per: ReadPermissionVal
});
const apiServer = dataset.apiServer;
if (!apiServer) {
return Promise.reject('apiServer is required');
}
return useApiDatasetRequest({ apiServer }).listFiles({ searchKey, parentId });
}
export default NextAPI(handler);

View File

@@ -0,0 +1,40 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
export type listExistIdQuery = {
datasetId: string;
};
export type listExistIdBody = {};
export type listExistIdResponse = string[];
async function handler(
req: ApiRequestProps<listExistIdBody, listExistIdQuery>,
res: ApiResponseType<any>
): Promise<listExistIdResponse> {
const { datasetId } = req.query;
const { dataset } = await authDataset({
req,
datasetId,
per: ReadPermissionVal,
authToken: true,
authApiKey: true
});
const collections = await MongoDatasetCollection.find(
{
teamId: dataset.teamId,
datasetId: dataset._id
},
'_id apiFileId'
).lean();
return collections.map((col) => col.apiFileId).filter(Boolean) as string[];
}
export default NextAPI(handler);

View File

@@ -0,0 +1,88 @@
import type { NextApiRequest } from 'next';
import type { ApiDatasetCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import {
TrainingModeEnum,
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CreateCollectionResponse } from '@/global/core/dataset/api';
import { readApiServerFileContent } from '@fastgpt/service/core/dataset/read';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
async function handler(req: NextApiRequest): CreateCollectionResponse {
const {
name,
apiFileId,
trainingType = TrainingModeEnum.chunk,
chunkSize = 512,
chunkSplitter,
qaPrompt,
...body
} = req.body as ApiDatasetCreateDatasetCollectionParams;
const { teamId, tmbId, dataset } = await authDataset({
req,
authToken: true,
authApiKey: true,
datasetId: body.datasetId,
per: WritePermissionVal
});
const apiServer = dataset.apiServer;
if (!apiServer) {
return Promise.reject('Api server not found');
}
if (!apiFileId) {
return Promise.reject('ApiFileId not found');
}
// Auth same apiFileId
const storeCol = await MongoDatasetCollection.findOne(
{
teamId,
datasetId: dataset._id,
apiFileId
},
'_id'
).lean();
if (storeCol) {
return Promise.reject(DatasetErrEnum.sameApiCollection);
}
const content = await readApiServerFileContent({
apiServer,
apiFileId,
teamId
});
const { collectionId, insertResults } = await createCollectionAndInsertData({
dataset,
rawText: content,
relatedId: apiFileId,
createCollectionParams: {
...body,
teamId,
tmbId,
type: DatasetCollectionTypeEnum.apiFile,
name: name,
trainingType,
chunkSize,
chunkSplitter,
qaPrompt,
apiFileId,
metadata: {
relatedImgId: apiFileId
}
}
});
return { collectionId, results: insertResults };
}
export default NextAPI(handler);

View File

@@ -2,23 +2,16 @@ import type { NextApiRequest } from 'next';
import { readFileContentFromMongo } from '@fastgpt/service/common/file/gridfs/controller';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { FileIdCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import {
DatasetCollectionTypeEnum,
TrainingModeEnum
} from '@fastgpt/global/core/dataset/constants';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { rawText2Chunks } from '@fastgpt/service/core/dataset/read';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { NextAPI } from '@/service/middleware/entry';
import { CreateCollectionResponse } from '@/global/core/dataset/api';
import { MongoRawTextBuffer } from '@fastgpt/service/common/buffer/rawText/schema';
async function handler(req: NextApiRequest): CreateCollectionResponse {
const { datasetId, parentId, fileId, ...body } = req.body as FileIdCreateDatasetCollectionParams;
@@ -39,21 +32,11 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
isQAImport: true
});
// 2. split chunks
const chunks = rawText2Chunks({
const { collectionId, insertResults } = await createCollectionAndInsertData({
dataset,
rawText,
isQAImport: true
});
// 3. auth limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, chunks)
});
return mongoSessionRun(async (session) => {
// 4. create collection
const { _id: collectionId } = await createOneCollection({
isQAImport: true,
createCollectionParams: {
...body,
teamId,
tmbId,
@@ -65,41 +48,13 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
// special metadata
trainingType,
chunkSize: 0,
session
});
// 5. create training bill
const { billId } = await createTrainingUsage({
teamId,
tmbId,
appName: filename,
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel)?.name,
agentModel: getLLMModel(dataset.agentModel)?.name,
session
});
// 6. insert to training queue
const insertResult = await pushDataListToTrainingQueue({
teamId,
tmbId,
datasetId: dataset._id,
collectionId,
agentModel: dataset.agentModel,
vectorModel: dataset.vectorModel,
trainingMode: trainingType,
billId,
data: chunks.map((chunk, index) => ({
q: chunk.q,
a: chunk.a,
chunkIndex: index
})),
session
});
return { collectionId, results: insertResult };
chunkSize: 0
}
});
// remove buffer
await MongoRawTextBuffer.deleteOne({ sourceId: fileId });
return { collectionId, results: insertResults };
}
export default NextAPI(handler);

View File

@@ -1,28 +1,22 @@
import { readFileContentFromMongo } from '@fastgpt/service/common/file/gridfs/controller';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { FileIdCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import {
DatasetCollectionTypeEnum,
TrainingModeEnum
} from '@fastgpt/global/core/dataset/constants';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoImage } from '@fastgpt/service/common/file/image/schema';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { hashStr } from '@fastgpt/global/common/string/tools';
import { MongoRawTextBuffer } from '@fastgpt/service/common/buffer/rawText/schema';
import { rawText2Chunks } from '@fastgpt/service/core/dataset/read';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CreateCollectionResponse } from '@/global/core/dataset/api';
async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>) {
async function handler(
req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
): CreateCollectionResponse {
const {
fileId,
trainingType = TrainingModeEnum.chunk,
@@ -32,7 +26,6 @@ async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
...body
} = req.body;
const start = Date.now();
const { teamId, tmbId, dataset } = await authDataset({
req,
authToken: true,
@@ -48,23 +41,10 @@ async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
fileId
});
// 2. split chunks
const chunks = rawText2Chunks({
const { collectionId, insertResults } = await createCollectionAndInsertData({
dataset,
rawText,
chunkLen: chunkSize,
overlapRatio: trainingType === TrainingModeEnum.chunk ? 0.2 : 0,
customReg: chunkSplitter ? [chunkSplitter] : []
});
// 3. auth limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, chunks)
});
await mongoSessionRun(async (session) => {
// 4. create collection
const { _id: collectionId } = await createOneCollection({
createCollectionParams: {
...body,
teamId,
tmbId,
@@ -79,63 +59,19 @@ async function handler(req: ApiRequestProps<FileIdCreateDatasetCollectionParams>
trainingType,
chunkSize,
chunkSplitter,
qaPrompt,
qaPrompt
},
hashRawText: hashStr(rawText),
rawTextLength: rawText.length,
session
});
// 5. create training bill
const { billId } = await createTrainingUsage({
teamId,
tmbId,
appName: filename,
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel)?.name,
agentModel: getLLMModel(dataset.agentModel)?.name,
session
});
// 6. insert to training queue
await pushDataListToTrainingQueue({
teamId,
tmbId,
datasetId: dataset._id,
collectionId,
agentModel: dataset.agentModel,
vectorModel: dataset.vectorModel,
trainingMode: trainingType,
prompt: qaPrompt,
billId,
data: chunks.map((item, index) => ({
...item,
chunkIndex: index
})),
session
});
// 7. remove related image ttl
await MongoImage.updateMany(
{
teamId,
'metadata.relatedId': fileId
},
{
// Remove expiredTime to avoid ttl expiration
$unset: {
expiredTime: 1
}
},
{
session
}
);
// remove buffer
await MongoRawTextBuffer.deleteOne({ sourceId: fileId });
return collectionId;
relatedId: fileId
});
// remove buffer
await MongoRawTextBuffer.deleteOne({ sourceId: fileId });
return {
collectionId,
results: insertResults
};
}
export default NextAPI(handler);

View File

@@ -1,21 +1,16 @@
import type { NextApiRequest } from 'next';
import type { LinkCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import {
TrainingModeEnum,
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { reloadCollectionChunks } from '@fastgpt/service/core/dataset/collection/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CreateCollectionResponse } from '@/global/core/dataset/api';
import { urlsFetch } from '@fastgpt/service/common/string/cheerio';
import { hashStr } from '@fastgpt/global/common/string/tools';
async function handler(req: NextApiRequest): CreateCollectionResponse {
const {
@@ -35,59 +30,45 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
per: WritePermissionVal
});
// 1. check dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, new Array(10))
const result = await urlsFetch({
urlList: [link],
selector: body?.metadata?.webPageSelector
});
const { title = link, content = '' } = result[0];
return mongoSessionRun(async (session) => {
// 2. create collection
const collection = await createOneCollection({
if (!content) {
return Promise.reject('Can not fetch content from link');
}
const { collectionId, insertResults } = await createCollectionAndInsertData({
dataset,
rawText: content,
createCollectionParams: {
...body,
name: link,
name: title,
teamId,
tmbId,
type: DatasetCollectionTypeEnum.link,
metadata: {
relatedImgId: link,
webPageSelector: body?.metadata?.webPageSelector
},
trainingType,
chunkSize,
chunkSplitter,
qaPrompt,
rawLink: link,
session
});
rawLink: link
},
// 3. create bill and start sync
const { billId } = await createTrainingUsage({
teamId,
tmbId,
appName: 'core.dataset.collection.Sync Collection',
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel).name,
agentModel: getLLMModel(dataset.agentModel).name,
session
});
// load
const result = await reloadCollectionChunks({
collection: {
...collection.toObject(),
datasetId: dataset
},
tmbId,
billId,
session
});
return {
collectionId: collection._id,
results: {
insertLen: result.insertLen
}
};
relatedId: link
});
return {
collectionId,
results: insertResults
};
}
export default NextAPI(handler);

View File

@@ -4,22 +4,10 @@ import { getUploadModel } from '@fastgpt/service/common/file/multer';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { FileCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api';
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import {
DatasetCollectionTypeEnum,
TrainingModeEnum
} from '@fastgpt/global/core/dataset/constants';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { getNanoid, hashStr } from '@fastgpt/global/common/string/tools';
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getDatasetModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoImage } from '@fastgpt/service/common/file/image/schema';
import { readRawTextByLocalFile } from '@fastgpt/service/common/file/read/utils';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -52,12 +40,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): CreateCo
datasetId: data.datasetId
});
const {
trainingType = TrainingModeEnum.chunk,
chunkSize = 512,
chunkSplitter,
qaPrompt
} = data;
const { fileMetadata, collectionMetadata, ...collectionData } = data;
const collectionName = file.originalname;
@@ -89,84 +71,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): CreateCo
// 3. delete tmp file
removeFilesByPaths(filePaths);
// 4. split raw text to chunks
const { chunks } = splitText2Chunks({
text: rawText,
chunkLen: chunkSize,
overlapRatio: trainingType === TrainingModeEnum.chunk ? 0.2 : 0,
customReg: chunkSplitter ? [chunkSplitter] : []
});
// 5. check dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, chunks)
});
// 6. create collection and training bill
const { collectionId, insertResults } = await mongoSessionRun(async (session) => {
const { _id: collectionId } = await createOneCollection({
const { collectionId, insertResults } = await createCollectionAndInsertData({
dataset,
rawText,
relatedId: fileId,
createCollectionParams: {
...collectionData,
name: collectionName,
teamId,
tmbId,
type: DatasetCollectionTypeEnum.file,
fileId,
rawTextLength: rawText.length,
hashRawText: hashStr(rawText),
metadata: {
...collectionMetadata,
relatedImgId
},
session
});
const { billId } = await createTrainingUsage({
teamId,
tmbId,
appName: collectionName,
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel)?.name,
agentModel: getDatasetModel(dataset.agentModel)?.name
});
// 7. push chunks to training queue
const insertResults = await pushDataListToTrainingQueue({
teamId,
tmbId,
datasetId: dataset._id,
collectionId,
agentModel: dataset.agentModel,
vectorModel: dataset.vectorModel,
trainingMode: trainingType,
prompt: qaPrompt,
billId,
data: chunks.map((text, index) => ({
q: text,
chunkIndex: index
}))
});
// 8. remove image expired time
await MongoImage.updateMany(
{
teamId,
'metadata.relatedId': relatedImgId
},
{
// Remove expiredTime to avoid ttl expiration
$unset: {
expiredTime: 1
}
},
{
session
}
);
return {
collectionId,
insertResults
};
}
});
return { collectionId, results: insertResults };

View File

@@ -0,0 +1,131 @@
import { reTrainingDatasetFileCollectionParams } from '@fastgpt/global/core/dataset/api';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import {
DatasetCollectionTypeEnum,
DatasetSourceReadTypeEnum,
TrainingModeEnum
} from '@fastgpt/global/core/dataset/constants';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { hashStr } from '@fastgpt/global/common/string/tools';
import { readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { delOnlyCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { i18nT } from '@fastgpt/web/i18n/utils';
type RetrainingCollectionResponse = {
collectionId: string;
};
// 获取集合并处理
async function handler(
req: ApiRequestProps<reTrainingDatasetFileCollectionParams>
): Promise<RetrainingCollectionResponse> {
const {
collectionId,
trainingType = TrainingModeEnum.chunk,
chunkSize = 512,
chunkSplitter,
qaPrompt
} = req.body;
if (!collectionId) {
return Promise.reject(CommonErrEnum.missingParams);
}
// 凭证校验
const { collection } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId: collectionId,
per: ReadPermissionVal
});
const sourceReadType = await (async () => {
if (collection.type === DatasetCollectionTypeEnum.link) {
if (!collection.rawLink) return Promise.reject('rawLink is missing');
return {
type: DatasetSourceReadTypeEnum.link,
sourceId: collection.rawLink,
selector: collection.metadata?.webPageSelector
};
}
if (collection.type === DatasetCollectionTypeEnum.file) {
if (!collection.fileId) return Promise.reject('fileId is missing');
return {
type: DatasetSourceReadTypeEnum.fileLocal,
sourceId: collection.fileId
};
}
if (collection.type === DatasetCollectionTypeEnum.apiFile) {
if (!collection.apiFileId) return Promise.reject('apiFileId is missing');
return {
type: DatasetSourceReadTypeEnum.apiFile,
sourceId: collection.apiFileId,
apiServer: collection.datasetId.apiServer
};
}
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
if (!collection.externalFileUrl) return Promise.reject('externalFileId is missing');
return {
type: DatasetSourceReadTypeEnum.externalFile,
sourceId: collection.externalFileUrl,
externalFileId: collection.externalFileId
};
}
return Promise.reject(i18nT('dataset:collection_not_support_retraining'));
})();
const rawText = await readDatasetSourceRawText({
teamId: collection.teamId,
...sourceReadType
});
return mongoSessionRun(async (session) => {
const { collectionId } = await createCollectionAndInsertData({
dataset: collection.datasetId,
rawText,
createCollectionParams: {
teamId: collection.teamId,
tmbId: collection.tmbId,
datasetId: collection.datasetId._id,
name: collection.name,
type: collection.type,
fileId: collection.fileId,
rawLink: collection.rawLink,
externalFileId: collection.externalFileId,
externalFileUrl: collection.externalFileUrl,
apiFileId: collection.apiFileId,
hashRawText: hashStr(rawText),
rawTextLength: rawText.length,
tags: collection.tags,
createTime: collection.createTime,
parentId: collection.parentId,
// special metadata
trainingType,
chunkSize,
chunkSplitter,
qaPrompt,
metadata: collection.metadata
}
});
await delOnlyCollection({
collections: [collection],
session
});
return { collectionId };
});
}
export default NextAPI(handler);

View File

@@ -1,20 +1,12 @@
import type { NextApiRequest } from 'next';
import type { TextCreateDatasetCollectionParams } from '@fastgpt/global/core/dataset/api.d';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
import {
TrainingModeEnum,
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
import { pushDataListToTrainingQueue } from '@fastgpt/service/core/dataset/training/controller';
import { hashStr } from '@fastgpt/global/common/string/tools';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CreateCollectionResponse } from '@/global/core/dataset/api';
@@ -38,23 +30,10 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
per: WritePermissionVal
});
// 1. split text to chunks
const { chunks } = splitText2Chunks({
text,
chunkLen: chunkSize,
overlapRatio: trainingType === TrainingModeEnum.chunk ? 0.2 : 0,
customReg: chunkSplitter ? [chunkSplitter] : []
});
// 2. check dataset limit
await checkDatasetLimit({
teamId,
insertLen: predictDataLimitLength(trainingType, chunks)
});
const createResult = await mongoSessionRun(async (session) => {
// 3. create collection
const { _id: collectionId } = await createOneCollection({
const { collectionId, insertResults } = await createCollectionAndInsertData({
dataset,
rawText: text,
createCollectionParams: {
...body,
teamId,
tmbId,
@@ -64,46 +43,14 @@ async function handler(req: NextApiRequest): CreateCollectionResponse {
trainingType,
chunkSize,
chunkSplitter,
qaPrompt,
hashRawText: hashStr(text),
rawTextLength: text.length,
session
});
// 4. create training bill
const { billId } = await createTrainingUsage({
teamId,
tmbId,
appName: name,
billSource: UsageSourceEnum.training,
vectorModel: getVectorModel(dataset.vectorModel)?.name,
agentModel: getLLMModel(dataset.agentModel)?.name,
session
});
// 5. push chunks to training queue
const insertResults = await pushDataListToTrainingQueue({
teamId,
tmbId,
datasetId: dataset._id,
collectionId,
agentModel: dataset.agentModel,
vectorModel: dataset.vectorModel,
trainingMode: trainingType,
prompt: qaPrompt,
billId,
data: chunks.map((text, index) => ({
q: text,
chunkIndex: index
})),
session
});
return { collectionId, results: insertResults };
qaPrompt
}
});
return createResult;
return {
collectionId,
results: insertResults
};
}
export const config = {

View File

@@ -1,6 +1,6 @@
import type { NextApiRequest } from 'next';
import { findCollectionAndChild } from '@fastgpt/service/core/dataset/collection/utils';
import { delCollectionAndRelatedSources } from '@fastgpt/service/core/dataset/collection/controller';
import { delCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { NextAPI } from '@/service/middleware/entry';
@@ -32,8 +32,9 @@ async function handler(req: NextApiRequest) {
// delete
await mongoSessionRun((session) =>
delCollectionAndRelatedSources({
delCollection({
collections,
delRelatedSource: true,
session
})
);

View File

@@ -11,6 +11,7 @@ import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { AIChatItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
export type readCollectionSourceQuery = {};
@@ -145,6 +146,14 @@ async function handler(
if (collection.type === DatasetCollectionTypeEnum.link && collection.rawLink) {
return collection.rawLink;
}
if (collection.type === DatasetCollectionTypeEnum.apiFile && collection.apiFileId) {
const apiServer = collection.datasetId.apiServer;
if (!apiServer) return Promise.reject('apiServer not found');
return useApiDatasetRequest({ apiServer }).getFilePreviewUrl({
apiFileId: collection.apiFileId
});
}
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
if (collection.externalFileId && collection.datasetId.externalReadUrl) {
return collection.datasetId.externalReadUrl.replace(

View File

@@ -0,0 +1,37 @@
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { syncCollection } from '@fastgpt/service/core/dataset/collection/utils';
/*
Collection sync
1. Check collection type: link, api dataset collection
2. Get collection and raw text
3. Check whether the original text is the same: skip if same
4. Create new collection
5. Delete old collection
*/
export type CollectionSyncBody = {
collectionId: string;
};
async function handler(req: ApiRequestProps<CollectionSyncBody>) {
const { collectionId } = req.body;
if (!collectionId) {
return Promise.reject(CommonErrEnum.missingParams);
}
const { collection } = await authDatasetCollection({
req,
authToken: true,
collectionId,
per: WritePermissionVal
});
return syncCollection(collection);
}
export default NextAPI(handler);

View File

@@ -1,106 +0,0 @@
import type { NextApiRequest } from 'next';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import {
getCollectionAndRawText,
reloadCollectionChunks
} from '@fastgpt/service/core/dataset/collection/utils';
import { delCollectionAndRelatedSources } from '@fastgpt/service/core/dataset/collection/controller';
import {
DatasetCollectionSyncResultEnum,
DatasetCollectionTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { createTrainingUsage } from '@fastgpt/service/support/wallet/usage/controller';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { getLLMModel, getVectorModel } from '@fastgpt/service/core/ai/model';
import { createOneCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
async function handler(req: NextApiRequest) {
const { collectionId } = req.body as { collectionId: string };
if (!collectionId) {
return Promise.reject(CommonErrEnum.missingParams);
}
const { collection, tmbId } = await authDatasetCollection({
req,
authToken: true,
collectionId,
per: WritePermissionVal
});
if (collection.type !== DatasetCollectionTypeEnum.link || !collection.rawLink) {
return Promise.reject(DatasetErrEnum.unLinkCollection);
}
const { title, rawText, isSameRawText } = await getCollectionAndRawText({
collection
});
if (isSameRawText) {
return DatasetCollectionSyncResultEnum.sameRaw;
}
/* Not the same original text, create and reload */
const vectorModelData = getVectorModel(collection.datasetId.vectorModel);
const agentModelData = getLLMModel(collection.datasetId.agentModel);
await mongoSessionRun(async (session) => {
// create training bill
const { billId } = await createTrainingUsage({
teamId: collection.teamId,
tmbId,
appName: 'core.dataset.collection.Sync Collection',
billSource: UsageSourceEnum.training,
vectorModel: vectorModelData.name,
agentModel: agentModelData.name,
session
});
// create a collection and delete old
const newCol = await createOneCollection({
teamId: collection.teamId,
tmbId: collection.tmbId,
parentId: collection.parentId,
datasetId: collection.datasetId._id,
name: title || collection.name,
type: collection.type,
trainingType: collection.trainingType,
chunkSize: collection.chunkSize,
chunkSplitter: collection.chunkSplitter,
qaPrompt: collection.qaPrompt,
fileId: collection.fileId,
rawLink: collection.rawLink,
metadata: collection.metadata,
createTime: collection.createTime,
session
});
// start load
await reloadCollectionChunks({
collection: {
...newCol.toObject(),
datasetId: collection.datasetId
},
tmbId,
billId,
rawText,
session
});
// delete old collection
await delCollectionAndRelatedSources({
collections: [collection],
session
});
});
return DatasetCollectionSyncResultEnum.success;
}
export default NextAPI(handler);

View File

@@ -24,7 +24,8 @@ async function handler(
type = DatasetTypeEnum.dataset,
avatar,
vectorModel = global.vectorModels[0].model,
agentModel = getDatasetModel().model
agentModel = getDatasetModel().model,
apiServer
} = req.body;
// auth
@@ -54,7 +55,8 @@ async function handler(
vectorModel,
agentModel,
avatar,
type
type,
apiServer
});
return _id;

View File

@@ -1,6 +1,5 @@
/* push data to training queue */
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import type {
PushDatasetDataProps,
PushDatasetDataResponse
@@ -39,15 +38,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
insertLen: predictDataLimitLength(collection.trainingType, data)
});
jsonRes<PushDatasetDataResponse>(res, {
data: await pushDataListToTrainingQueue({
...body,
teamId,
tmbId,
datasetId: collection.datasetId._id,
agentModel: collection.datasetId.agentModel,
vectorModel: collection.datasetId.vectorModel
})
return pushDataListToTrainingQueue({
...body,
teamId,
tmbId,
datasetId: collection.datasetId._id,
agentModel: collection.datasetId.agentModel,
vectorModel: collection.datasetId.vectorModel
});
}

View File

@@ -30,6 +30,12 @@ async function handler(req: ApiRequestProps<Query>): Promise<DatasetItemType> {
return {
...dataset,
apiServer: dataset.apiServer
? {
baseUrl: dataset.apiServer.baseUrl,
authorization: ''
}
: undefined,
permission,
vectorModel: getVectorModel(dataset.vectorModel),
agentModel: getLLMModel(dataset.agentModel)

View File

@@ -1,19 +1,27 @@
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { rawText2Chunks, readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import {
OwnerPermissionVal,
WritePermissionVal
} from '@fastgpt/global/support/permission/constant';
import { authCollectionFile } from '@fastgpt/service/support/permission/auth/file';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
export type PostPreviewFilesChunksProps = {
datasetId: string;
type: DatasetSourceReadTypeEnum;
sourceId: string;
chunkSize: number;
overlapRatio: number;
customSplitChar?: string;
// Read params
selector?: string;
isQAImport?: boolean;
externalFileId?: string;
};
export type PreviewChunksResponse = {
q: string;
@@ -23,8 +31,17 @@ export type PreviewChunksResponse = {
async function handler(
req: ApiRequestProps<PostPreviewFilesChunksProps>
): Promise<PreviewChunksResponse> {
const { type, sourceId, chunkSize, customSplitChar, overlapRatio, selector, isQAImport } =
req.body;
const {
type,
sourceId,
chunkSize,
customSplitChar,
overlapRatio,
selector,
isQAImport,
datasetId,
externalFileId
} = req.body;
if (!sourceId) {
throw new Error('sourceId is empty');
@@ -33,25 +50,40 @@ async function handler(
throw new Error('chunkSize is too large, should be less than 30000');
}
const { teamId } = await (async () => {
const { teamId, apiServer } = await (async () => {
if (type === DatasetSourceReadTypeEnum.fileLocal) {
return authCollectionFile({
const res = await authCollectionFile({
req,
authToken: true,
authApiKey: true,
fileId: sourceId,
per: OwnerPermissionVal
});
return {
teamId: res.teamId
};
}
return authCert({ req, authApiKey: true, authToken: true });
const { dataset } = await authDataset({
req,
authApiKey: true,
authToken: true,
datasetId,
per: WritePermissionVal
});
return {
teamId: dataset.teamId,
apiServer: dataset.apiServer
};
})();
const rawText = await readDatasetSourceRawText({
teamId,
type,
sourceId: sourceId,
sourceId,
selector,
isQAImport
isQAImport,
apiServer,
externalFileId
});
return rawText2Chunks({

View File

@@ -27,32 +27,51 @@ export type GetDatasetListBody = {
async function handler(req: ApiRequestProps<GetDatasetListBody>) {
const { parentId, type, searchKey } = req.body;
// 凭证校验
const {
dataset: parentDataset,
teamId,
tmbId,
permission: myPer
} = await (async () => {
if (parentId) {
return await authDataset({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal,
datasetId: parentId
// Auth user permission
const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([
authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
}),
...(parentId
? [
authDataset({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal,
datasetId: parentId
})
]
: [])
]);
// Get team all app permissions
const [perList, myGroupMap] = await Promise.all([
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: {
$exists: true
}
}).lean(),
getGroupsByTmbId({
tmbId,
teamId
}).then((item) => {
const map = new Map<string, 1>();
item.forEach((item) => {
map.set(String(item._id), 1);
});
}
return {
...(await authUserPer({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal
})),
dataset: undefined
};
})();
return map;
})
]);
const myPerList = perList.filter(
(item) => String(item.tmbId) === String(tmbId) || myGroupMap.has(String(item.groupId))
);
const findDatasetQuery = (() => {
const searchMatch = searchKey
@@ -63,61 +82,43 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
]
}
: {};
// Filter apps by permission, if not owner, only get apps that I have permission to access
const appIdQuery = teamPer.isOwner
? {}
: { _id: { $in: myPerList.map((item) => item.resourceId) } };
if (searchKey) {
return {
...appIdQuery,
teamId,
...searchMatch
};
}
return {
...appIdQuery,
teamId,
...(type ? (Array.isArray(type) ? { type: { $in: type } } : { type }) : {}),
...parseParentIdInMongo(parentId)
};
})();
const myGroupIds = (
await getGroupsByTmbId({
tmbId,
teamId
const myDatasets = await MongoDataset.find(findDatasetQuery)
.sort({
updateTime: -1
})
).map((item) => String(item._id));
.lean();
const [myDatasets, perList] = await Promise.all([
MongoDataset.find(findDatasetQuery)
.sort({
updateTime: -1
})
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset,
teamId,
resourceId: {
$exists: true
}
}).lean()
]);
const filterDatasets = myDatasets
const formatDatasets = myDatasets
.map((dataset) => {
const { Per, privateDataset } = (() => {
const myPerList = perList.filter(
(item) =>
String(item.tmbId) === String(tmbId) || myGroupIds.includes(String(item.groupId))
);
const getPer = (datasetId: string) => {
const tmbPer = myPerList.find(
(item) => String(item.resourceId) === datasetId && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
myPerList
.filter(
(item) =>
String(item.resourceId) === datasetId && myGroupIds.includes(String(item.groupId))
)
.filter((item) => String(item.resourceId) === datasetId && !!item.groupId)
.map((item) => item.permission)
);
@@ -126,14 +127,14 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
return {
Per: new DatasetPermission({
per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal,
isOwner: String(dataset.tmbId) === String(tmbId) || myPer.isOwner
isOwner: String(dataset.tmbId) === String(tmbId) || teamPer.isOwner
}),
privateDataset: dataset.type === 'folder' ? clbCount <= 1 : clbCount === 0
};
};
// inherit
if (dataset.inheritPermission && parentDataset && dataset.type !== DatasetTypeEnum.folder) {
return getPer(String(parentDataset._id));
if (dataset.inheritPermission && parentId && dataset.type !== DatasetTypeEnum.folder) {
return getPer(String(parentId));
} else {
return getPer(String(dataset._id));
}
@@ -148,7 +149,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
.filter((app) => app.permission.hasReadPer);
const data = await Promise.all(
filterDatasets.map<DatasetListItemType>((item) => ({
formatDatasets.map<DatasetListItemType>((item) => ({
_id: item._id,
avatar: item.avatar,
name: item.name,

View File

@@ -41,8 +41,18 @@ async function handler(
req: ApiRequestProps<DatasetUpdateBody, DatasetUpdateQuery>,
_res: ApiResponseType<any>
): Promise<DatasetUpdateResponse> {
const { id, parentId, name, avatar, intro, agentModel, websiteConfig, externalReadUrl, status } =
req.body;
const {
id,
parentId,
name,
avatar,
intro,
agentModel,
websiteConfig,
externalReadUrl,
apiServer,
status
} = req.body;
if (!id) {
return Promise.reject(CommonErrEnum.missingParams);
@@ -103,6 +113,10 @@ async function handler(
...(status && { status }),
...(intro !== undefined && { intro }),
...(externalReadUrl !== undefined && { externalReadUrl }),
...(!!apiServer?.baseUrl && { 'apiServer.baseUrl': apiServer.baseUrl }),
...(!!apiServer?.authorization && {
'apiServer.authorization': apiServer.authorization
}),
...(isMove && { inheritPermission: true })
},
{ session }
@@ -165,7 +179,8 @@ async function updateTraining({
{
$set: {
model: agentModel,
retryCount: 5
retryCount: 5,
lockTime: new Date()
}
}
);

View File

@@ -1,14 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { pushGenerateVectorUsage } from '@/service/support/wallet/usage/push';
import { connectToDatabase } from '@/service/mongo';
import { getVectorsByText } from '@fastgpt/service/core/ai/embedding';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { checkTeamAIPoints } from '@fastgpt/service/support/permission/teamLimit';
import { EmbeddingTypeEnm } from '@fastgpt/global/core/ai/constants';
import { NextAPI } from '@/service/middleware/entry';
type Props = {
input: string | string[];
@@ -18,65 +17,58 @@ type Props = {
type: `${EmbeddingTypeEnm}`;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { input, model, billId, type } = req.body as Props;
await connectToDatabase();
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
let { input, model, billId, type } = req.body as Props;
if (!Array.isArray(input) && typeof input !== 'string') {
throw new Error('input is nor array or string');
if (!Array.isArray(input) && typeof input !== 'string') {
throw new Error('input is nor array or string');
}
const query = Array.isArray(input) ? input[0] : input;
const { teamId, tmbId, apikey, authType } = await authCert({
req,
authToken: true,
authApiKey: true
});
await checkTeamAIPoints(teamId);
const { tokens, vectors } = await getVectorsByText({
input: query,
model: getVectorModel(model),
type
});
res.json({
object: 'list',
data: vectors.map((item, index) => ({
object: 'embedding',
index: index,
embedding: item
})),
model,
usage: {
prompt_tokens: tokens,
total_tokens: tokens
}
});
const query = Array.isArray(input) ? input[0] : input;
const { totalPoints } = pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model,
billId,
source: getUsageSourceByAuthType({ authType })
});
const { teamId, tmbId, apikey, authType } = await authCert({
req,
authToken: true,
authApiKey: true
});
await checkTeamAIPoints(teamId);
const { tokens, vectors } = await getVectorsByText({
input: query,
model: getVectorModel(model),
type
});
res.json({
object: 'list',
data: vectors.map((item, index) => ({
object: 'embedding',
index: index,
embedding: item
})),
model,
usage: {
prompt_tokens: tokens,
total_tokens: tokens
}
});
const { totalPoints } = pushGenerateVectorUsage({
teamId,
tmbId,
tokens,
model,
billId,
source: getUsageSourceByAuthType({ authType })
});
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints: totalPoints
});
}
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
if (apikey) {
updateApiKeyUsage({
apikey,
totalPoints: totalPoints
});
}
}
export default NextAPI(handler);

View File

@@ -50,13 +50,13 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
const {
register,
setValue,
getValues,
watch,
formState: { errors },
handleSubmit
} = useForm({
defaultValues: appDetail
});
const avatar = getValues('avatar');
const avatar = watch('avatar');
// submit config
const { runAsync: saveSubmitSuccess, loading: btnLoading } = useRequest2(

View File

@@ -283,7 +283,7 @@ const RenderList = React.memo(function RenderList({
{t(item.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'}>
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
{t(item.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{showCost && <CostTooltip cost={item.currentCost} />}

View File

@@ -35,8 +35,7 @@ export const compareSimpleAppSnapshot = (
ttsConfig: appForm1.chatConfig?.ttsConfig || undefined,
whisperConfig: appForm1.chatConfig?.whisperConfig || undefined,
chatInputGuide: appForm1.chatConfig?.chatInputGuide || undefined,
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined,
instruction: appForm1.chatConfig?.instruction || ''
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined
},
{
welcomeText: appForm2.chatConfig?.welcomeText || '',
@@ -45,8 +44,7 @@ export const compareSimpleAppSnapshot = (
ttsConfig: appForm2.chatConfig?.ttsConfig || undefined,
whisperConfig: appForm2.chatConfig?.whisperConfig || undefined,
chatInputGuide: appForm2.chatConfig?.chatInputGuide || undefined,
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined,
instruction: appForm2.chatConfig?.instruction || ''
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined
}
)
) {

View File

@@ -223,25 +223,44 @@ function ExportPopover({
}) {
const { t } = useTranslation();
const { copyData } = useCopyData();
const { flowData2StoreDataAndCheck } = useContextSelector(WorkflowContext, (v) => v);
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
const onExportWorkflow = useCallback(async () => {
const data = flowData2StoreDataAndCheck();
if (data) {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig: chatConfig
},
null,
2
),
t('app:export_config_successful')
);
}
}, [chatConfig, copyData, flowData2StoreDataAndCheck, t]);
const onExportWorkflow = useCallback(
async (mode: 'copy' | 'json') => {
const data = flowData2StoreData();
if (data) {
if (mode === 'copy') {
copyData(
JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig
},
null,
2
),
t('app:export_config_successful')
);
} else if (mode === 'json') {
fileDownload({
text: JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig
},
null,
2
),
type: 'application/json;charset=utf-8',
filename: `${appName}.json`
});
}
}
},
[appName, chatConfig, copyData, flowData2StoreData, t]
);
return (
<MyPopover
@@ -269,7 +288,7 @@ function ExportPopover({
cursor: 'pointer'
}}
borderRadius={'xs'}
onClick={onExportWorkflow}
onClick={() => onExportWorkflow('copy')}
>
<MyIcon name={'copy'} w={'1rem'} mr={2} />
<Box fontSize={'mini'}>{t('common:common.copy_to_clipboard')}</Box>
@@ -284,25 +303,7 @@ function ExportPopover({
cursor: 'pointer'
}}
borderRadius={'xs'}
onClick={() => {
const data = flowData2StoreDataAndCheck();
if (!data) return;
fileDownload({
text: JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
chatConfig: chatConfig
},
null,
2
),
type: 'application/json;charset=utf-8',
filename: `${appName}.json`
});
}}
onClick={() => onExportWorkflow('json')}
>
<MyIcon name={'configmap'} w={'1rem'} mr={2} />
<Box fontSize={'mini'}>{t('common:common.export_to_json')}</Box>

View File

@@ -1,5 +1,10 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Flex,
Grid,
@@ -16,7 +21,6 @@ import type {
} from '@fastgpt/global/core/workflow/type/node.d';
import { useReactFlow, XYPosition } from 'reactflow';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { useTranslation } from 'next-i18next';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
@@ -24,6 +28,7 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
getPreviewPluginNode,
getSystemPlugTemplates,
getPluginGroups,
getSystemPluginPaths
} from '@/web/core/app/api/plugin';
import { useToast } from '@fastgpt/web/hooks/useToast';
@@ -45,17 +50,28 @@ import { useWorkflowUtils } from './hooks/useUtils';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { cloneDeep } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
import { useUserStore } from '@/web/support/user/useUserStore';
import { LoopStartNode } from '@fastgpt/global/core/workflow/template/system/loop/loopStart';
import { LoopEndNode } from '@fastgpt/global/core/workflow/template/system/loop/loopEnd';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { WorkflowNodeEdgeContext } from '../context/workflowInitContext';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import { useMemoizedFn } from 'ahooks';
type ModuleTemplateListProps = {
isOpen: boolean;
onClose: () => void;
};
type RenderHeaderProps = {
templateType: TemplateTypeEnum;
onClose: () => void;
parentId: ParentIdType;
searchKey: string;
loadNodeTemplates: (params: any) => void;
setSearchKey: (searchKey: string) => void;
onUpdateParentId: (parentId: ParentIdType) => void;
};
type RenderListProps = {
templates: NodeTemplateListItemType[];
type: TemplateTypeEnum;
@@ -73,8 +89,6 @@ enum TemplateTypeEnum {
const sliderWidth = 460;
const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
const { t } = useTranslation();
const router = useRouter();
const { loadAndGetTeamMembers } = useUserStore();
const [parentId, setParentId] = useState<ParentIdType>('');
@@ -183,18 +197,6 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
[basicNodes, teamAndSystemApps]
);
// Get paths
const { data: paths = [] } = useRequest2(
() => {
if (templateType === TemplateTypeEnum.teamPlugin) return getAppFolderPath(parentId);
return getSystemPluginPaths(parentId);
},
{
manual: false,
refreshDeps: [parentId]
}
);
const onUpdateParentId = useCallback(
(parentId: ParentIdType) => {
loadNodeTemplates({
@@ -251,119 +253,15 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
userSelect={'none'}
overflow={isOpen ? 'none' : 'hidden'}
>
{/* Header */}
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('common:core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'core/modules/systemPlugin',
label: t('common:core.module.template.System Plugin'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
width={'100%'}
py={'5px'}
value={templateType}
onChange={(e) => {
loadNodeTemplates({
type: e as TemplateTypeEnum,
parentId: ''
});
}}
/>
</Box>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
bg={'myGray.100'}
_hover={{
bg: 'myGray.200',
'& svg': {
color: 'primary.600'
}
}}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
{/* Search */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={
templateType === TemplateTypeEnum.teamPlugin
? t('common:plugin.Search_app')
: t('common:plugin.Search plugin')
}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
{templateType === TemplateTypeEnum.teamPlugin && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
gap={1}
>
<Box>{t('common:create')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
{templateType === TemplateTypeEnum.systemPlugin &&
feConfigs.systemPluginCourseUrl && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
gap={1}
>
<Box>{t('common:plugin.contribute')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
</Flex>
)}
{/* paths */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) &&
!searchKey &&
parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
</Box>
<RenderHeader
templateType={templateType}
onClose={onClose}
parentId={parentId}
onUpdateParentId={onUpdateParentId}
searchKey={searchKey}
loadNodeTemplates={loadNodeTemplates}
setSearchKey={setSearchKey}
/>
<RenderList
templates={templates}
type={templateType}
@@ -378,6 +276,146 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
export default React.memo(NodeTemplatesModal);
const RenderHeader = React.memo(function RenderHeader({
templateType,
onClose,
parentId,
searchKey,
setSearchKey,
loadNodeTemplates,
onUpdateParentId
}: RenderHeaderProps) {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const router = useRouter();
// Get paths
const { data: paths = [] } = useRequest2(
() => {
if (templateType === TemplateTypeEnum.teamPlugin) return getAppFolderPath(parentId);
return getSystemPluginPaths(parentId);
},
{
manual: false,
refreshDeps: [parentId]
}
);
return (
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
{
icon: 'core/modules/basicNode',
label: t('common:core.module.template.Basic Node'),
value: TemplateTypeEnum.basic
},
{
icon: 'phoneTabbar/tool',
label: t('common:navbar.Toolkit'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
width={'100%'}
py={'5px'}
value={templateType}
onChange={(e) => {
loadNodeTemplates({
type: e as TemplateTypeEnum,
parentId: ''
});
}}
/>
</Box>
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
bg={'myGray.100'}
_hover={{
bg: 'myGray.200',
'& svg': {
color: 'primary.600'
}
}}
variant={'grayBase'}
aria-label={''}
onClick={onClose}
/>
</Flex>
{/* Search */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) && (
<Flex mt={2} alignItems={'center'} h={10}>
<InputGroup mr={4} h={'full'}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
h={'full'}
bg={'myGray.50'}
placeholder={
templateType === TemplateTypeEnum.teamPlugin
? t('common:plugin.Search_app')
: t('common:plugin.Search plugin')
}
onChange={(e) => setSearchKey(e.target.value)}
/>
</InputGroup>
<Box flex={1} />
{templateType === TemplateTypeEnum.teamPlugin && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => router.push('/app/list')}
gap={1}
>
<Box>{t('common:create')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
{templateType === TemplateTypeEnum.systemPlugin && feConfigs.systemPluginCourseUrl && (
<Flex
alignItems={'center'}
cursor={'pointer'}
_hover={{
color: 'primary.600'
}}
fontSize={'sm'}
onClick={() => window.open(feConfigs.systemPluginCourseUrl)}
gap={1}
>
<Box>{t('common:plugin.contribute')}</Box>
<MyIcon name={'common/rightArrowLight'} w={'0.8rem'} />
</Flex>
)}
</Flex>
)}
{/* paths */}
{(templateType === TemplateTypeEnum.teamPlugin ||
templateType === TemplateTypeEnum.systemPlugin) &&
!searchKey &&
parentId && (
<Flex alignItems={'center'} mt={2}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
</Box>
);
});
const RenderList = React.memo(function RenderList({
templates,
type,
@@ -386,10 +424,9 @@ const RenderList = React.memo(function RenderList({
setParentId
}: RenderListProps) {
const { t } = useTranslation();
const { feConfigs, setLoading } = useSystemStore();
const { setLoading } = useSystemStore();
const { isPc } = useSystem();
const isSystemPlugin = type === TemplateTypeEnum.systemPlugin;
const { screenToFlowPosition } = useReactFlow();
const { computedNewNodeName } = useWorkflowUtils();
@@ -398,18 +435,48 @@ const RenderList = React.memo(function RenderList({
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const formatTemplates = useMemo<NodeTemplateListType>(() => {
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
copy[index].list.push(item);
});
return copy.filter((item) => item.list.length > 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [templates, parentId]);
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
manual: false
});
const onAddNode = useCallback(
const formatTemplatesArray = useMemo<{ list: NodeTemplateListType; label: string }[]>(() => {
const data = (() => {
if (type === TemplateTypeEnum.systemPlugin) {
return pluginGroups.map((group) => {
const copy: NodeTemplateListType = group.groupTypes.map((type) => ({
list: [],
type: type.typeId,
label: type.typeName
}));
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
copy[index].list.push(item);
});
return {
label: group.groupName,
list: copy.filter((item) => item.list.length > 0)
};
});
}
const copy: NodeTemplateListType = cloneDeep(workflowNodeTemplateList);
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
copy[index].list.push(item);
});
return [
{
label: '',
list: copy.filter((item) => item.list.length > 0)
}
];
})();
return data.filter(({ list }) => list.length > 0);
}, [type, templates, pluginGroups]);
const onAddNode = useMemoizedFn(
async ({
template,
position
@@ -527,8 +594,7 @@ const RenderList = React.memo(function RenderList({
.concat(newNodes);
return newState;
});
},
[screenToFlowPosition, nodeList, computedNewNodeName, t, setNodes, setLoading, toast]
}
);
const gridStyle = useMemo(() => {
@@ -551,118 +617,157 @@ const RenderList = React.memo(function RenderList({
};
}, [type]);
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={'5'}>
<Box mx={'auto'}>
{formatTemplates.map((item, i) => (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
_notLast={{ mb: 5 }}
>
{item.label && formatTemplates.length > 1 && (
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
return (
<>
{list.map((item, i) => {
return (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
>
<Flex>
<Box fontSize={'sm'} mb={3} fontWeight={'500'} flex={1} color={'myGray.900'}>
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
)}
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
{item.list.map((template) => (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<Avatar
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
{item.list.map((template) => {
return (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<MyAvatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
<CostTooltip
cost={template.currentCost}
hasTokenFee={template.hasTokenFee}
/>
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
whiteSpace={'nowrap'}
overflow={'hidden'}
textOverflow={'ellipsis'}
>
<MyAvatar
src={template.avatar}
w={'1.75rem'}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
flexShrink={0}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
<Box
color={'myGray.900'}
fontWeight={'500'}
fontSize={'sm'}
flex={'1 0 0'}
ml={3}
className="textEllipsis"
>
{t(template.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{isSystemPlugin && <CostTooltip cost={template.currentCost} />}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
cursor={'pointer'}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
draggable={!template.isFolder}
onDragEnd={(e) => {
if (e.clientX < sliderWidth) return;
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
}}
onClick={(e) => {
if (template.isFolder) {
return setParentId(template.id);
}
if (isPc) {
return onAddNode({
template,
position: { x: sliderWidth * 1.5, y: 200 }
});
}
onAddNode({
template,
position: { x: e.clientX, y: e.clientY }
});
onClose();
}}
>
<Avatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box ml={3} flex={'1'}>
<Box color={'myGray.900'} fontWeight={'500'} fontSize={'sm'} flex={'1 0 0'}>
{t(template.name as any)}
</Box>
{gridStyle.authorInName && template.author !== undefined && (
<Box fontSize={'xs'} mt={0.5} color={'myGray.500'}>
{`by ${template.author || feConfigs.systemTitle}`}
</Box>
)}
</Box>
{gridStyle.authorInRight && template.authorAvatar && template.author && (
<HStack spacing={1} maxW={'120px'}>
<Avatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
<Box fontSize={'xs'} className="textEllipsis">
{template.author}
</Box>
</HStack>
)}
</Flex>
</MyTooltip>
))}
</Grid>
</Box>
))}
</Box>
{gridStyle.authorInRight && template.authorAvatar && template.author && (
<HStack spacing={1} maxW={'120px'} flexShrink={0}>
<MyAvatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
<Box fontSize={'xs'} className="textEllipsis">
{template.author}
</Box>
</HStack>
)}
</Flex>
</MyTooltip>
);
})}
</Grid>
</Box>
);
})}
</>
);
});
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'} px={formatTemplatesArray.length > 1 ? 2 : 5}>
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
{formatTemplatesArray.length > 1 ? (
<>
{formatTemplatesArray.map(({ list, label }, index) => (
<AccordionItem key={index} border={'none'}>
<AccordionButton
fontSize={'sm'}
fontWeight={'500'}
color={'myGray.900'}
justifyContent={'space-between'}
alignItems={'center'}
borderRadius={'md'}
px={3}
>
{t(label as any)}
<AccordionIcon />
</AccordionButton>
<AccordionPanel py={0}>
<PluginListRender list={list} />
</AccordionPanel>
</AccordionItem>
))}
</>
) : (
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
)}
</Accordion>
</Box>
);
});

View File

@@ -94,7 +94,7 @@ export const useDebug = () => {
});
return Promise.reject();
}
}, [edges, onUpdateNodeError, t, toast]);
}, [edges, getNodes, onUpdateNodeError, t, toast]);
const openDebugNode = useCallback(
async ({ entryNodeId }: { entryNodeId: string }) => {
@@ -163,7 +163,7 @@ export const useDebug = () => {
return acc;
}, {}),
globalVariables: defaultGlobalVariables
variables: defaultGlobalVariables
}
});
const { register, getValues, setValue, handleSubmit } = variablesForm;
@@ -207,11 +207,11 @@ export const useDebug = () => {
: node
),
runtimeEdges: runtimeEdges,
variables: data.globalVariables
variables: data.variables
});
// Filter global variables and set them as default global variable values
setDefaultGlobalVariables(data.globalVariables);
setDefaultGlobalVariables(data.variables);
onClose();
};
@@ -225,8 +225,7 @@ export const useDebug = () => {
}
const hasRequiredGlobalVar =
e.globalVariables &&
Object.values(e.globalVariables).some((item) => item.type === 'required');
e.variables && Object.values(e.variables).some((item) => item.type === 'required');
if (hasRequiredGlobalVar) {
setCurrentTab(TabEnum.global);
@@ -260,7 +259,7 @@ export const useDebug = () => {
{filteredVar.map((item) => (
<VariableInputItem
key={item.id}
item={{ ...item, key: `globalVariables.${item.key}` }}
item={{ ...item, key: item.key }}
variablesForm={variablesForm}
/>
))}

View File

@@ -33,13 +33,13 @@ const NodeDatasetConcat = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const CustomComponent = useMemo(() => {
const quoteList = inputs.filter((item) => item.canEdit);
const tokenLimit = (() => {
let maxTokens = 13000;
let maxTokens = 16000;
nodeList.forEach((item) => {
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 16000;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}

View File

@@ -136,7 +136,8 @@ const InputTypeConfig = ({
FlowNodeInputTypeEnum.JSONEditor,
FlowNodeInputTypeEnum.numberInput,
FlowNodeInputTypeEnum.switch,
FlowNodeInputTypeEnum.select
FlowNodeInputTypeEnum.select,
VariableInputEnum.custom
];
return list.includes(inputType as FlowNodeInputTypeEnum);
@@ -301,7 +302,8 @@ const InputTypeConfig = ({
}}
/>
)}
{inputType === FlowNodeInputTypeEnum.input && (
{(inputType === FlowNodeInputTypeEnum.input ||
inputType === VariableInputEnum.custom) && (
<MyTextarea
{...register('defaultValue')}
bg={'myGray.50'}

View File

@@ -373,7 +373,7 @@ const NodeCard = (props: Props) => {
{RenderToolHandle}
<ConfirmSyncModal />
<EditTitleModal maxLength={20} />
<EditTitleModal maxLength={50} />
</Flex>
);
};

View File

@@ -12,6 +12,7 @@ import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
import { getWebLLMModel } from '@/web/common/system/utils';
import { defaultDatasetMaxTokens } from '@fastgpt/global/core/app/constants';
const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -31,13 +32,13 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
});
const tokenLimit = useMemo(() => {
let maxTokens = 13000;
let maxTokens = defaultDatasetMaxTokens;
nodeList.forEach((item) => {
if ([FlowNodeTypeEnum.chatNode, FlowNodeTypeEnum.tools].includes(item.flowNodeType)) {
const model =
item.inputs.find((item) => item.key === NodeInputKeyEnum.aiModel)?.value || '';
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || 13000;
const quoteMaxToken = getWebLLMModel(model)?.quoteMaxToken || defaultDatasetMaxTokens;
maxTokens = Math.max(maxTokens, quoteMaxToken);
}

View File

@@ -251,7 +251,7 @@ const TemplateMarketModal = ({
position={'relative'}
>
<Avatar src={'/imgs/app/templateFill.svg'} w={'2rem'} objectFit={'fill'} />
<Box color={'myGray.900'}>{t('app:templateMarket.Template_market')}</Box>
<Box color={'myGray.900'}>{t('app:template_market')}</Box>
<Box flex={'1'} />

View File

@@ -30,6 +30,7 @@ import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TemplateMarketModal from './components/TemplateMarketModal';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const CreateModal = dynamic(() => import('./components/CreateModal'));
const EditFolderModal = dynamic(
@@ -134,7 +135,7 @@ const MyApps = () => {
flex={'1 0 0'}
flexDirection={'column'}
h={'100%'}
pr={folderDetail ? [3, 2] : [3, 10]}
pr={folderDetail ? [3, 2] : [3, 8]}
pl={3}
overflowY={'auto'}
overflowX={'hidden'}
@@ -179,6 +180,33 @@ const MyApps = () => {
{isPc && RenderSearchInput}
{isPc && (
<Flex
alignItems={'center'}
gap={1.5}
border={'1px solid'}
borderColor={'myGray.250'}
h={9}
px={4}
fontSize={'14px'}
fontWeight={'medium'}
bg={'white'}
rounded={'sm'}
cursor={'pointer'}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
_hover={{
bg: 'primary.50',
color: 'primary.600'
}}
onClick={() => setTemplateModalType('all')}
>
<MyImage src={'/imgs/app/templateFill.svg'} w={'18px'} />
{t('app:template_market')}
</Flex>
)}
{(folderDetail
? folderDetail.permission.hasWritePer && folderDetail?.type !== AppTypeEnum.httpPlugin
: userInfo?.team.permission.hasWritePer) && (
@@ -218,16 +246,20 @@ const MyApps = () => {
}
]
},
{
children: [
{
icon: '/imgs/app/templateFill.svg',
label: t('app:template_market'),
description: t('app:template_market_description'),
onClick: () => setTemplateModalType('all')
}
]
},
...(isPc
? []
: [
{
children: [
{
icon: '/imgs/app/templateFill.svg',
label: t('app:template_market'),
description: t('app:template_market_description'),
onClick: () => setTemplateModalType('all')
}
]
}
]),
{
children: [
{

View File

@@ -1,11 +1,12 @@
import React, { useMemo } from 'react';
import React from 'react';
import { useChatBox } from '@/components/core/chat/ChatContainer/ChatBox/hooks/useChatBox';
import type { ChatItemType } from '@fastgpt/global/core/chat/type.d';
import { Box, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext';
const ToolMenu = ({
history,
@@ -16,7 +17,8 @@ const ToolMenu = ({
}) => {
const { t } = useTranslation();
const { onExportChat } = useChatBox();
const router = useRouter();
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
return history.length > 0 ? (
<MyMenu
@@ -35,12 +37,7 @@ const ToolMenu = ({
icon: 'core/chat/chatLight',
label: t('common:core.chat.New Chat'),
onClick: () => {
router.replace({
query: {
...router.query,
chatId: ''
}
});
onChangeChatId();
}
}
]

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