V4.6.9-first commit (#899)

* perf: insert mongo dataset data session

* perf: dataset data index

* remove delay

* rename bill schema

* rename bill record

* perf: bill table

* perf: prompt

* perf: sub plan

* change the usage count

* feat: usage bill

* publish usages

* doc

* 新增团队聊天功能 (#20)

* perf: doc

* feat 添加标签部分

feat 信息团队标签配置

feat 新增团队同步管理

feat team分享页面

feat 完成team分享页面

feat 实现模糊搜索

style 格式化

fix 修复迷糊匹配

style 样式修改

fix 团队标签功能修复

* fix 修复鉴权功能

* merge 合并代码

* fix 修复引用错误

* fix 修复pr问题

* fix 修复ts格式问题

---------

Co-authored-by: archer <545436317@qq.com>
Co-authored-by: liuxingwan <liuxingwan.lxw@alibaba-inc.com>

* update extra plan

* fix: ts

* format

* perf: bill field

* feat: standard plan

* fix: ts

* feat 个人账号页面修改 (#22)

* feat 添加标签部分

feat 信息团队标签配置

feat 新增团队同步管理

feat team分享页面

feat 完成team分享页面

feat 实现模糊搜索

style 格式化

fix 修复迷糊匹配

style 样式修改

fix 团队标签功能修复

* fix 修复鉴权功能

* merge 合并代码

* fix 修复引用错误

* fix 修复pr问题

* fix 修复ts格式问题

* feat 修改个人账号页

---------

Co-authored-by: liuxingwan <liuxingwan.lxw@alibaba-inc.com>

* sub plan page (#23)

* fix chunk index; error page text

* feat: dataset process Integral prediction

* feat: stand plan field

* feat: sub plan limit

* perf: index

* query extension

* perf: share link push app name

* perf: plan point unit

* perf: get sub plan

* perf: account page

* feat 新增套餐详情弹窗代码 (#24)

* merge 合并代码

* fix 新增套餐详情弹框

* fix 修复pr问题

* feat: change http node input to prompt editor (#21)

* feat: change http node input to prompt editor

* fix

* split PromptEditor to HttpInput

* Team plans (#25)

* perf: pay check

* perf: team plan test

* plan limit check

* replace sensitive text

* perf: fix some null

* collection null check

* perf: plans modal

* perf: http module

* pacakge (#26)

* individuation page and pay modal amount (#27)

* feat: individuation page

* team chat config

* pay modal

* plan count and replace invalid chars (#29)

* fix: user oneapi

* fix: training queue

* fix: qa queue

* perf: remove space chars

* replace invalid chars

* change httpinput dropdown menu (#28)

* perf: http

* reseet free plan

* perf: plan code to packages

* remove llm config to package

* perf: code

* perf: faq

* fix: get team plan

---------

Co-authored-by: yst <77910600+yu-and-liu@users.noreply.github.com>
Co-authored-by: liuxingwan <liuxingwan.lxw@alibaba-inc.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-02-28 13:19:15 +08:00
committed by GitHub
parent 32686f9e3e
commit 064c64e74c
282 changed files with 7223 additions and 4731 deletions

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { ModalBody, useTheme, ModalFooter, Button, Box, Card, Flex, Grid } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import Avatar from '../Avatar';
@@ -8,7 +8,6 @@ import DatasetSelectModal, { useDatasetSelect } from '@/components/core/dataset/
import dynamic from 'next/dynamic';
import { AdminFbkType } from '@fastgpt/global/core/chat/type.d';
import SelectCollections from '@/web/core/dataset/components/SelectCollections';
import { getDefaultIndex } from '@fastgpt/global/core/dataset/utils';
const InputDataModal = dynamic(() => import('@/pages/dataset/detail/components/InputDataModal'));

View File

@@ -8,10 +8,10 @@ import Tabs from '../Tabs';
import MyModal from '../MyModal';
import MyTooltip from '../MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import Markdown from '../Markdown';
import { QuoteList } from './QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
function Row({
label,
@@ -131,10 +131,10 @@ const ResponseBox = React.memo(function ResponseBox({
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
{activeModule?.price !== undefined && (
{activeModule?.totalPoints !== undefined && (
<Row
label={t('core.chat.response.module price')}
value={`${formatStorePrice2Read(activeModule?.price)}`}
label={t('support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
@@ -142,11 +142,9 @@ const ResponseBox = React.memo(function ResponseBox({
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
<Row label={t('wallet.bill.Chars length')} value={`${activeModule?.charsLength}`} />
<Row label={t('wallet.bill.Input Token Length')} value={`${activeModule?.inputTokens}`} />
<Row
label={t('wallet.bill.Output Token Length')}
value={`${activeModule?.outputTokens}`}
label={t('support.wallet.usage.Chars length')}
value={`${activeModule?.charsLength}`}
/>
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
<Row
@@ -208,14 +206,14 @@ const ResponseBox = React.memo(function ResponseBox({
<Row label={t('core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('core.chat.response.search using reRank')}
value={activeModule?.searchUsingReRank}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('wallet.bill.Extension result')}
label={t('support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
</>
@@ -273,18 +271,18 @@ const ResponseBox = React.memo(function ResponseBox({
{/* plugin */}
<>
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
<Row
label={t('core.chat.response.Plugin Resonse Detail')}
rawDom={<ResponseBox response={activeModule.pluginDetail} isShare={isShare} />}
/>
)}
{activeModule?.pluginOutput && (
<Row
label={t('core.chat.response.plugin output')}
value={`~~~json\n${JSON.stringify(activeModule?.pluginOutput, null, 2)}`}
/>
)}
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
<Row
label={t('core.chat.response.Plugin Resonse Detail')}
rawDom={<ResponseBox response={activeModule.pluginDetail} isShare={isShare} />}
/>
)}
</>
{/* text output */}

View File

@@ -121,6 +121,7 @@ type Props = {
appId?: string;
chatId?: string;
shareId?: string;
shareTeamId?: string;
outLinkUid?: string;
onUpdateVariable?: (e: Record<string, any>) => void;
@@ -146,6 +147,7 @@ const ChatBox = (
appId,
chatId,
shareId,
shareTeamId,
outLinkUid,
onUpdateVariable,
onStartChat,
@@ -396,21 +398,22 @@ const ChatBox = (
};
})
);
setTimeout(() => {
createQuestionGuide({
history: newChatList.map((item, i) =>
i === newChatList.length - 1
? {
...item,
value: responseText
}
: item
)
});
generatingScroll();
isPc && TextareaDom.current?.focus();
}, 100);
if (!shareTeamId) {
setTimeout(() => {
createQuestionGuide({
history: newChatList.map((item, i) =>
i === newChatList.length - 1
? {
...item,
value: responseText
}
: item
)
});
generatingScroll();
isPc && TextareaDom.current?.focus();
}, 100);
}
} catch (err: any) {
toast({
title: t(getErrText(err, 'core.chat.error.Chat error')),

View File

@@ -11,6 +11,7 @@ const unAuthPage: { [key: string]: boolean } = {
'/login/fastlogin': true,
'/appStore': true,
'/chat/share': true,
'/chat/team': true,
'/tools/price': true,
'/price': true
};

View File

@@ -23,6 +23,7 @@ const pcUnShowLayoutRoute: Record<string, boolean> = {
'/login/provider': true,
'/login/fastlogin': true,
'/chat/share': true,
'/chat/team': true,
'/app/edit': true,
'/chat': true,
'/tools/price': true,
@@ -34,6 +35,7 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
'/login/provider': true,
'/login/fastlogin': true,
'/chat/share': true,
'/chat/team': true,
'/tools/price': true,
'/price': true
};
@@ -114,9 +116,10 @@ const Layout = ({ children }: { children: JSX.Element }) => {
</Box>
</>
)}
{!!userInfo && <UpdateInviteModal />}
</Box>
<Loading loading={loading} zIndex={999999} />
{!!userInfo && <UpdateInviteModal />}
</>
);
};

View File

@@ -57,7 +57,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
className={`markdown ${styles.markdown}
${isChatting ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''}
`}
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
remarkPlugins={[RemarkMath, [RemarkGfm, { singleTilde: false }], RemarkBreaks]}
rehypePlugins={[RehypeKatex]}
components={components}
linkTarget={'_blank'}

View File

@@ -2,15 +2,15 @@ import React, { useMemo } from 'react';
import MySelect, { type SelectProps } from './index';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useDisclosure } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const PriceBox = dynamic(() => import('@/components/support/wallet/Price'));
import { useRouter } from 'next/router';
import { AI_POINT_USAGE_CARD_ROUTE } from '@/web/support/wallet/sub/constants';
const SelectAiModel = ({ list, ...props }: SelectProps) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const router = useRouter();
const expandList = useMemo(() => {
return feConfigs.show_pay
? list.concat({
@@ -20,12 +20,6 @@ const SelectAiModel = ({ list, ...props }: SelectProps) => {
: list;
}, [feConfigs.show_pay, list, t]);
const {
isOpen: isOpenPriceBox,
onOpen: onOpenPriceBox,
onClose: onClosePriceBox
} = useDisclosure();
return (
<>
<MySelect
@@ -33,13 +27,12 @@ const SelectAiModel = ({ list, ...props }: SelectProps) => {
{...props}
onchange={(e) => {
if (e === 'price') {
onOpenPriceBox();
router.push(AI_POINT_USAGE_CARD_ROUTE);
return;
}
props.onchange?.(e);
}}
/>
{isOpenPriceBox && <PriceBox onClose={onClosePriceBox} />}
</>
);
};

View File

@@ -0,0 +1,103 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Menu,
MenuButton,
MenuList,
MenuItemOption,
MenuOptionGroup,
Flex,
TagLabel,
TagCloseButton,
HStack,
Tag,
Input
} from '@chakra-ui/react';
import type { TeamTagsSchema } from '@fastgpt/global/support/user/team/type';
const TagEdit = ({
defaultValues,
teamsTags,
setSelectedTags
}: {
defaultValues: [];
teamsTags: Array<TeamTagsSchema>;
setSelectedTags: (item: Array<string>) => void;
}) => {
const [teamTagsOptions, setTeamTagsOptions] = useState(teamsTags);
const setSelectTeamsTags = (item: any) => {
setSelectedTags(item);
};
useMemo(() => {
setTeamTagsOptions(teamsTags);
}, [teamsTags]);
return (
<>
<Menu closeOnSelect={false}>
<MenuButton className="menu-btn" maxHeight={'250'} minWidth={'80%'}>
<HStack
style={{
border: 'solid 2px #f3f3f3',
borderRadius: '5px',
padding: '3px',
flexWrap: 'wrap',
minHeight: '40px'
}}
>
{teamsTags.map((item: TeamTagsSchema, index: number) => {
const key: string = item?.key;
if (defaultValues.indexOf(key as never) > -1) {
return (
<Tag
key={index}
size={'md'}
colorScheme="red"
// maxWidth={"100px"}
borderRadius="full"
>
<TagLabel> {item.label}</TagLabel>
<TagCloseButton />
</Tag>
);
}
})}
</HStack>
</MenuButton>
<MenuList style={{ height: '300px', overflow: 'scroll' }}>
<Input
style={{ border: 'none', borderBottom: 'solid 1px #f6f6f6' }}
placeholder="pleace "
onChange={(e: any) => {
// 对用户输入的搜索文本进行小写转换,以实现不区分大小写的搜索
const searchLower: string = e?.nativeEvent?.data || '';
// 使用filter方法来过滤列表只返回包含搜索文本的项
const resultList = teamsTags.filter((item) => {
const searchValue = item.label || '';
// 对列表中的每一项也进行小写转换
return searchValue.includes(searchLower);
});
!searchLower ? setTeamTagsOptions(teamsTags) : setTeamTagsOptions(resultList);
}}
/>
<MenuOptionGroup
defaultValue={defaultValues}
type="checkbox"
style={{ height: '300px', overflow: 'scroll' }}
onChange={(e) => {
setSelectTeamsTags(e);
}}
>
{teamTagsOptions.map((item, index) => {
return (
<MenuItemOption key={index} value={item.key}>
{item?.label}
</MenuItemOption>
);
})}
</MenuOptionGroup>
</MenuList>
</Menu>
</>
);
};
export default TagEdit;

View File

@@ -27,6 +27,8 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import Tabs from '@/components/Tabs';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import SelectAiModel from '@/components/Select/SelectAiModel';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
export type DatasetParamsProps = {
searchMode: `${DatasetSearchModeEnum}`;
@@ -61,6 +63,8 @@ const DatasetParamsModal = ({
}: DatasetParamsProps & { onClose: () => void; onSuccess: (e: DatasetParamsProps) => void }) => {
const { t } = useTranslation();
const theme = useTheme();
const { toast } = useToast();
const { teamPlanStatus } = useUserStore();
const { reRankModelList, llmModelList } = useSystemStore();
const [refresh, setRefresh] = useState(false);
const [currentTabType, setCurrentTabType] = useState(SearchSettingTabEnum.searchMode);
@@ -71,7 +75,7 @@ const DatasetParamsModal = ({
limit,
similarity,
searchMode,
usingReRank,
usingReRank: !!usingReRank && !!teamPlanStatus?.standardConstants?.permissionReRank,
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel: datasetSearchExtensionModel ?? llmModelList[0]?.model,
datasetSearchExtensionBg
@@ -105,6 +109,10 @@ const DatasetParamsModal = ({
return true;
}, [getValues, similarity]);
const showReRank = useMemo(() => {
return usingReRank !== undefined && reRankModelList.length > 0;
}, [reRankModelList.length, usingReRank]);
return (
<MyModal
isOpen={true}
@@ -148,7 +156,7 @@ const DatasetParamsModal = ({
setRefresh(!refresh);
}}
/>
{usingReRank !== undefined && reRankModelList.length > 0 && (
{showReRank && (
<>
<Divider my={4} />
<Flex
@@ -168,6 +176,15 @@ const DatasetParamsModal = ({
}
: {})}
onClick={(e) => {
if (
teamPlanStatus?.standardConstants &&
!teamPlanStatus?.standardConstants?.permissionReRank
) {
return toast({
status: 'warning',
title: t('support.team.limit.No permission rerank')
});
}
setValue('usingReRank', !getValues('usingReRank'));
setRefresh((state) => !state);
}}
@@ -273,7 +290,7 @@ const DatasetParamsModal = ({
{currentTabType === SearchSettingTabEnum.queryExtension && (
<Box>
<Box fontSize={'xs'} color={'myGray.500'}>
{t('core.module.template.Query extension intro')}
{t('core.dataset.Query extension intro')}
</Box>
<Flex mt={3} alignItems={'center'}>
<Box flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</Box>

View File

@@ -0,0 +1,185 @@
import React from 'react';
import MyModal from '@/components/MyModal';
import { ModalBody, Button, ModalFooter, useDisclosure, Textarea, Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { onChangeNode } from '../../../FlowProvider';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import yaml from 'js-yaml';
import { useForm } from 'react-hook-form';
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
const methodMap: { [K in RequestMethod]: string } = {
get: 'GET',
post: 'POST',
put: 'PUT',
delete: 'DELETE',
patch: 'PATCH'
};
const OpenApiImportModal = ({
children,
moduleId,
inputs
}: {
children: React.ReactElement;
moduleId: string;
inputs: FlowNodeInputItemType[];
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { t } = useTranslation();
const { register, handleSubmit } = useForm({
defaultValues: {
openapiContent: ''
}
});
const { toast } = useToast();
const handleFileProcessing = async (content: string) => {
try {
let data;
try {
data = JSON.parse(content);
} catch (jsonError) {
try {
data = yaml.load(content, { schema: yaml.FAILSAFE_SCHEMA });
} catch (yamlError) {
console.error(yamlError);
throw new Error();
}
}
const firstPathName = Object.keys(data.paths)[0];
const firstPathData = data.paths[firstPathName];
const firstRequestMethod = Object.keys(firstPathData)[0];
const firstRequestMethodData = firstPathData[firstRequestMethod];
const firstRequestParameters = firstRequestMethodData.parameters || [];
const pathParams = [];
const headerParams = [];
for (const parameter of firstRequestParameters) {
if (parameter.in === 'path') {
pathParams.push({
key: parameter.name,
type: parameter.schema.type
});
} else {
headerParams.push({
key: parameter.name,
type: parameter.schema.type
});
}
}
const requestBodySchema =
firstRequestMethodData.requestBody?.content?.['application/json']?.schema;
let requestBodyValue = '';
if (requestBodySchema) {
requestBodyValue = JSON.stringify(requestBodySchema, null, 2);
}
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
const requestMethod = inputs.find((item) => item.key === ModuleInputKeyEnum.httpMethod);
const params = inputs.find((item) => item.key === ModuleInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === ModuleInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === ModuleInputKeyEnum.httpJsonBody);
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpReqUrl,
value: {
...requestUrl,
value: firstPathName
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpMethod,
value: {
...requestMethod,
value: methodMap[firstRequestMethod.toLowerCase() as RequestMethod] || 'GET'
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpParams,
value: {
...params,
value: pathParams
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpHeaders,
value: {
...headers,
value: headerParams
}
});
onChangeNode({
moduleId,
type: 'updateInput',
key: ModuleInputKeyEnum.httpJsonBody,
value: {
...jsonBody,
value: requestBodyValue
}
});
onClose();
toast({
title: t('common.Import success'),
status: 'success'
});
} catch (error: any) {
toast({
title: t('common.Import failed'),
description: error.message,
status: 'error'
});
console.error(error);
}
};
return (
<>
{children && <Box onClick={onOpen}>{children}</Box>}
<MyModal
isOpen={isOpen}
onClose={onClose}
iconSrc="modal/edit"
title={t('common.Import')}
m={'auto'}
w={500}
>
<ModalBody>
<Textarea
height={400}
maxH={500}
mt={2}
{...register('openapiContent')}
placeholder={t('core.module.http.OpenAPI import placeholder')}
/>
</ModalBody>
<ModalFooter>
<Button onClick={handleSubmit((data) => handleFileProcessing(data.openapiContent))}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default React.memo(OpenApiImportModal);

View File

@@ -1,11 +1,11 @@
import React, { useCallback, useMemo, useState, useTransition } from 'react';
import React, { useCallback, useEffect, useMemo, useState, useTransition } from 'react';
import { NodeProps } from 'reactflow';
import NodeCard from '../render/NodeCard';
import NodeCard from '../../render/NodeCard';
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
import Divider from '../modules/Divider';
import Container from '../modules/Container';
import RenderInput from '../render/RenderInput';
import RenderOutput from '../render/RenderOutput';
import Divider from '../../modules/Divider';
import Container from '../../modules/Container';
import RenderInput from '../../render/RenderInput';
import RenderOutput from '../../render/RenderOutput';
import {
Box,
Flex,
@@ -16,17 +16,17 @@ import {
Tr,
Th,
Td,
TableContainer
TableContainer,
Button
} from '@chakra-ui/react';
import MySelect from '@/components/Select';
import { ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
import { onChangeNode, useFlowProviderStore } from '../../../FlowProvider';
import { useTranslation } from 'next-i18next';
import Tabs from '@/components/Tabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useForm } from 'react-hook-form';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
@@ -36,6 +36,9 @@ import {
splitGuideModule
} from '@fastgpt/global/core/module/utils';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import dynamic from 'next/dynamic';
const OpenApiImportModal = dynamic(() => import('./OpenApiImportModal'));
enum TabEnum {
params = 'params',
@@ -133,11 +136,18 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
return (
<Box>
<Box mb={2}>{t('core.module.Http request settings')}</Box>
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
<span>{t('core.module.Http request settings')}</span>
<span>
<OpenApiImportModal moduleId={moduleId} inputs={inputs}>
<Button variant={'link'}>{t('core.module.http.OpenAPI import')}</Button>
</OpenApiImportModal>
</span>
</Box>
<Flex alignItems={'center'} className="nodrag">
<MySelect
h={'34px'}
w={'80px'}
w={'88px'}
bg={'myGray.50'}
width={'100%'}
value={requestMethods?.value}
@@ -149,6 +159,18 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
{
label: 'POST',
value: 'POST'
},
{
label: 'PUT',
value: 'PUT'
},
{
label: 'DELETE',
value: 'DELETE'
},
{
label: 'PATCH',
value: 'PATCH'
}
]}
onchange={(e) => {
@@ -178,10 +200,6 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
);
});
const defaultForm = {
key: '',
value: ''
};
function RenderHttpProps({
moduleId,
inputs
@@ -261,7 +279,7 @@ function RenderHttpProps({
<Tabs
list={[
{ label: <RenderPropsItem text="Params" num={paramsLength} />, id: TabEnum.params },
...(requestMethods === 'POST'
...(!['GET', 'DELETE'].includes(requestMethods)
? [
{
label: (
@@ -303,36 +321,75 @@ const RenderForm = ({
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [_, startSts] = useTransition();
const { register, reset, handleSubmit } = useForm({
defaultValues: defaultForm
});
const list = useMemo(() => (input.value || []) as PropsArrType[], [input.value]);
const [list, setList] = useState<PropsArrType[]>(input.value || []);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [shouldUpdateNode, setShouldUpdateNode] = useState(false);
const addNewProps = useCallback(
({ key, value }: { key: string; value: string }) => {
const checkExist = list.find((item) => item.key === key);
if (checkExist) {
return toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
}
if (!key) return;
const leftVariables = useMemo(() => {
return variables.filter((variable) => {
const existVariables = list.map((item) => item.key);
return !existVariables.includes(variable.key);
});
}, [list, variables]);
useEffect(() => {
setList(input.value || []);
}, [input.value]);
useEffect(() => {
if (shouldUpdateNode) {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: [...list, { key, type: 'string', value }]
value: list
}
});
reset(defaultForm);
},
[input, list, moduleId, reset, t, toast]
);
setShouldUpdateNode(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list]);
const handleKeyChange = (index: number, newKey: string) => {
setList((prevList) => {
if (!newKey) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key cannot be empty')
});
return prevList;
}
const checkExist = prevList.find((item, i) => i !== index && item.key == newKey);
if (checkExist) {
setUpdateTrigger((prev) => !prev);
toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
return prevList;
}
return prevList.map((item, i) => (i === index ? { ...item, key: newKey } : item));
});
setShouldUpdateNode(true);
};
const handleAddNewProps = (key: string, value: string = '') => {
const checkExist = list.find((item) => item.key === key);
if (checkExist) {
return toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
}
if (!key) return;
setList((prevList) => [...prevList, { key, type: 'string', value }]);
setShouldUpdateNode(true);
};
return (
<TableContainer>
@@ -347,103 +404,69 @@ const RenderForm = ({
{list.map((item, index) => (
<Tr key={`${input.key}${index}`}>
<Td p={0} w={'150px'}>
<Input
w={'150px'}
defaultValue={item.key}
variant={'unstyled'}
paddingLeft={2}
placeholder={t('core.module.http.Props name')}
onBlur={(e) => {
const val = e.target.value;
if (!val) {
return toast({
status: 'warning',
title: t('core.module.http.Key cannot be empty')
});
}
const checkExist = list.find((item, i) => i !== index && item.key == val);
if (checkExist) {
return toast({
status: 'warning',
title: t('core.module.http.Key already exists')
});
}
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list.map((item, i) => (i === index ? { ...item, key: val } : item))
}
});
});
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={true}
setDropdownValue={(value) => {
handleKeyChange(index, value);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Props name')}
value={item.key}
variables={leftVariables}
onBlur={(val) => {
handleKeyChange(index, val);
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td p={0} display={'flex'} alignItems={'center'}>
<Input
flex={'1 0 0'}
w={'150px'}
defaultValue={item.value}
variant={'unstyled'}
paddingLeft={2}
placeholder={t('core.module.http.Props value')}
onBlur={(e) => {
const val = e.target.value;
startSts(() => {
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list.map((item, i) =>
i === index ? { ...item, value: val } : item
)
}
});
});
}}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={(e) => {
e.stopPropagation();
onChangeNode({
moduleId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: list.filter((val) => val.key !== item.key)
}
});
}}
/>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('core.module.http.Props value')}
value={item.value}
variables={variables}
onBlur={(val) => {
setList((prevList) =>
prevList.map((item, i) => (i === index ? { ...item, value: val } : item))
);
setShouldUpdateNode(true);
}}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={() => {
setList((prevlist) => prevlist.filter((val) => val.key !== item.key));
setShouldUpdateNode(true);
}}
/>
</Box>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<Input
w={'150px'}
variant={'unstyled'}
paddingLeft={2}
<HttpInput
hasDropDownPlugin={true}
setDropdownValue={(val) => {
handleAddNewProps(val);
}}
placeholder={t('core.module.http.Add props')}
{...register('key', {
onBlur: handleSubmit(addNewProps)
})}
value={''}
h={40}
variables={leftVariables}
onBlur={(val) => {
handleAddNewProps(val);
}}
/>
</Td>
<Td p={0}>
<Input variant={'unstyled'} paddingLeft={2} {...register('value')} />
<Box display={'flex'} alignItems={'center'}>
<HttpInput />
</Box>
</Td>
</Tr>
</Tbody>
@@ -460,7 +483,9 @@ const RenderJson = ({
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
}) => {
const { t } = useTranslation();
const [_, startSts] = useTransition();
return (
<Box mt={1}>
<JSONEditor
@@ -468,6 +493,7 @@ const RenderJson = ({
height={200}
resize
value={input.value}
placeholder={t('core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
@@ -488,7 +514,7 @@ const RenderJson = ({
};
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
return (
<Flex alignItems={'center'} fontSize={'xs'} transform={'scale(0.8)'}>
<Flex alignItems={'center'}>
<Box>{text}</Box>
{num > 0 && (
<Box ml={1} borderRadius={'50%'} bg={'myGray.200'} px={2} py={'1px'}>
@@ -500,6 +526,7 @@ const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
};
const NodeHttp = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
const { t } = useTranslation();
const { moduleId, inputs, outputs } = data;
const CustomComponents = useMemo(
@@ -511,12 +538,12 @@ const NodeHttp = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
<>
<RenderHttpProps moduleId={moduleId} inputs={inputs} />
<Box mt={2} transform={'translateY(10px)'}>
{t('core.module.Variable import')}
</Box>
</>
)
}),
[inputs, moduleId]
[inputs, moduleId, t]
);
return (

View File

@@ -93,47 +93,38 @@ const FieldEditModal = ({
const { register, getValues, setValue, handleSubmit, watch } = useForm<EditNodeFieldType>({
defaultValues: defaultField
});
const inputType = watch('inputType');
const outputType = watch('outputType');
const valueType = watch('valueType');
const [refresh, setRefresh] = useState(false);
const showDataTypeSelect = useMemo(() => {
if (!editField.dataType) return false;
const inputType = getValues('inputType');
const outputType = getValues('outputType');
if (inputType === FlowNodeInputTypeEnum.target) return true;
if (outputType === FlowNodeOutputTypeEnum.source) return true;
return false;
}, [editField.dataType, getValues, refresh]);
}, [editField.dataType, inputType, outputType]);
const showRequired = useMemo(() => {
const inputType = getValues('inputType');
const valueType = getValues('valueType');
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.required;
}, [editField.required, getValues, refresh]);
}, [editField.required, inputType]);
const showNameInput = useMemo(() => {
const inputType = getValues('inputType');
return editField.name;
}, [editField.name, getValues, refresh]);
}, [editField.name]);
const showKeyInput = useMemo(() => {
const inputType = getValues('inputType');
const valueType = getValues('valueType');
if (inputType === FlowNodeInputTypeEnum.addInputParam) return false;
return editField.key;
}, [editField.key, getValues, refresh]);
}, [editField.key, inputType]);
const showDescriptionInput = useMemo(() => {
const inputType = getValues('inputType');
return editField.description;
}, [editField.description, getValues, refresh]);
}, [editField.description]);
return (
<MyModal
@@ -209,7 +200,18 @@ const FieldEditModal = ({
{showKeyInput && (
<Flex mb={5} alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('core.module.Field key')}</Box>
<Input placeholder="appointment/sql" {...register('key', { required: true })} />
<Input
placeholder="appointment/sql"
{...register('key', {
required: true,
onChange: (e) => {
const value = e.target.value;
if (!showNameInput) {
setValue('label', value);
}
}
})}
/>
</Flex>
)}
{showDescriptionInput && (

View File

@@ -46,11 +46,7 @@ const RenderList: {
Component: dynamic(() => import('./templates/AiSetting'))
},
{
types: [
FlowNodeInputTypeEnum.selectChatModel,
FlowNodeInputTypeEnum.selectCQModel,
FlowNodeInputTypeEnum.selectExtractModel
],
types: [FlowNodeInputTypeEnum.selectLLMModel],
Component: dynamic(() => import('./templates/SelectAiModel'))
},
{

View File

@@ -55,7 +55,7 @@ const JsonEditor = ({ inputs = [], item, moduleId }: RenderInputProps) => {
return (
<JSONEditor
bg={'myGray.50'}
placeholder={t(item.placeholder || '')}
placeholder={item.placeholder}
resize
value={value}
onChange={(e) => {

View File

@@ -33,7 +33,7 @@ const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
[FlowNodeTypeEnum.pluginInput]: dynamic(() => import('./components/nodes/NodePluginInput')),
[FlowNodeTypeEnum.pluginOutput]: dynamic(() => import('./components/nodes/NodePluginOutput')),
[FlowNodeTypeEnum.pluginModule]: NodeSimple,
[FlowNodeTypeEnum.cfr]: NodeSimple
[FlowNodeTypeEnum.queryExtension]: NodeSimple
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge

View File

@@ -46,7 +46,7 @@ type EditProps = EditApiKeyProps & { _id?: string };
const defaultEditData: EditProps = {
name: '',
limit: {
credit: -1
maxUsagePoints: -1
}
};
@@ -139,10 +139,9 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
<Tr>
<Th>{t('Name')}</Th>
<Th>Api Key</Th>
<Th>{t('support.openapi.Usage')}</Th>
<Th>{t('support.outlink.Usage points')}</Th>
{feConfigs?.isPlus && (
<>
<Th>{t('support.openapi.Max usage')}</Th>
<Th>{t('common.Expired Time')}</Th>
</>
)}
@@ -153,18 +152,18 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{apiKeys.map(({ _id, name, usage, limit, apiKey, createTime, lastUsedTime }) => (
{apiKeys.map(({ _id, name, usagePoints, limit, apiKey, createTime, lastUsedTime }) => (
<Tr key={_id}>
<Td>{name}</Td>
<Td>{apiKey}</Td>
<Td>{usage}</Td>
<Td>
{Math.round(usagePoints)}/
{feConfigs?.isPlus && limit?.maxUsagePoints && limit?.maxUsagePoints > -1
? `${limit?.maxUsagePoints}`
: t('common.Unlimited')}
</Td>
{feConfigs?.isPlus && (
<>
<Td>
{limit?.credit && limit?.credit > -1
? `${limit?.credit}`
: t('common.Unlimited')}
</Td>
<Td whiteSpace={'pre-wrap'}>
{limit?.expiredTime
? dayjs(limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
@@ -334,15 +333,15 @@ function EditKeyModal({
<>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common.Max credit')}:
<MyTooltip label={t('common.Max credit tips' || '')}>
{t('support.outlink.Max usage points')}:
<MyTooltip label={t('support.outlink.Max usage points tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Input
{...register('limit.credit', {
{...register('limit.maxUsagePoints', {
min: -1,
max: 1000,
max: 10000000,
valueAsNumber: true,
required: true
})}

View File

@@ -2,13 +2,16 @@ import React, { useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { DragHandleIcon } from '@chakra-ui/icons';
import {
getTeamList,
getTeamMembers,
putSwitchTeam,
putUpdateMember,
delRemoveMember,
delLeaveTeam
delLeaveTeam,
getTeamsTags,
insertTeamsTags
} from '@/web/support/user/team/api';
import {
Box,
@@ -22,7 +25,6 @@ import {
Th,
Td,
TableContainer,
useTheme,
useDisclosure,
MenuButton
} from '@chakra-ui/react';
@@ -43,15 +45,19 @@ import { FormDataType, defaultForm } from './EditModal';
import MyMenu from '@/components/MyMenu';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const EditModal = dynamic(() => import('./EditModal'));
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagsAsync = dynamic(() => import('../TeamTagsAsync'));
const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const theme = useTheme();
const { t } = useTranslation();
const { Loading } = useLoading();
const { toast } = useToast();
const { teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const [teamsTags, setTeamTags] = useState<any>();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
@@ -61,6 +67,11 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const { userInfo, initUserInfo } = useUserStore();
const [editTeamData, setEditTeamData] = useState<FormDataType>();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const {
data: myTeams = [],
@@ -76,6 +87,8 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
mutationFn: async (teamId: string) => {
const token = await putSwitchTeam(teamId);
token && setToken(token);
// get team tags
await getTeamsTags(teamId);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
@@ -86,6 +99,11 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
['getMembers', userInfo?.team?.teamId],
() => {
if (!userInfo?.team?.teamId) return [];
// get team tags
getTeamsTags(userInfo.team.teamId).then((res: any) => {
setTeamTags(res);
});
return getTeamMembers(userInfo.team.teamId);
}
);
@@ -108,7 +126,9 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
mutationFn: async (teamId?: string) => {
if (!teamId) return;
// change to personal team
// get members
await onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
@@ -184,6 +204,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
bg: 'myGray.100'
}
})}
onClick={() => onSwitchTeam(team.teamId)}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
@@ -196,6 +217,17 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
: {})}
>
{team.teamName}
{/* {userInfo?.team?.teamId === team.teamId && (
<HStack spacing={1}>
{teamsTags.slice(0, 3).map((item: any, index) => {
return (
<Tag key={index} size={'sm'} variant="outline" colorScheme="blue">
{item.label}
</Tag>
);
})}
</HStack>
)} */}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
@@ -229,7 +261,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
borderBottomColor={'myGray.100'}
mb={3}
>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'}>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'} alignItems={'center'}>
{userInfo.team.teamName}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
@@ -258,25 +290,50 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
{userInfo.team.role === TeamMemberRoleEnum.owner &&
teamPlanStatus?.standardConstants &&
teamPlanStatus.standardConstants.maxTeamMember > members.length && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={
<MyIcon name={'common/inviteLight'} w={'14px'} color={'primary.500'} />
}
onClick={() => {
if (userInfo.team.maxSize <= members.length) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', { max: userInfo.team.maxSize })
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'primary.500'} />}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
if (userInfo.team.maxSize <= members.length) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', { max: userInfo.team.maxSize })
title: t('user.team.Team Tags Async', { max: userInfo.team.maxSize })
});
} else {
onOpenInvite();
onOpenTeamTagsAsync();
}
}}
>
{t('user.team.Invite Member')}
{t('user.team.Team Tags Async')}
</Button>
)}
<Box flex={1} />
@@ -435,6 +492,13 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && (
<TeamTagsAsync
teamInfo={teamsTags?.tagsUrl}
teamsTags={teamsTags?.list || []}
onClose={onCloseTeamTagsAsync}
/>
)}
<ConfirmRemoveMemberModal />
<ConfirmLeaveTeamModal />
</>

View File

@@ -0,0 +1,182 @@
import React, { useEffect, useMemo, useState } from 'react';
import MyModal from '@/components/MyModal';
import {
Box,
Button,
Flex,
ModalBody,
Tag,
ModalFooter,
Input,
HStack,
Avatar
} from '@chakra-ui/react';
import { AttachmentIcon, CopyIcon, DragHandleIcon } from '@chakra-ui/icons';
import { putUpdateTeamTags, updateTags } from '@/web/support/user/team/api';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import type { TeamTagsSchema } from '@fastgpt/global/support/user/team/type';
import { useRequest } from '@/web/common/hooks/useRequest';
import { RepeatIcon } from '@chakra-ui/icons';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useCopyData } from '@/web/common/hooks/useCopyData';
const TeamTagsAsync = ({
teamsTags,
teamInfo,
onClose
}: {
teamsTags: Array<TeamTagsSchema>;
teamInfo: any;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const [_teamsTags, setTeamTags] = useState<Array<TeamTagsSchema>>(teamsTags);
const { register, setValue, getValues, handleSubmit } = useForm<any>({
defaultValues: { ...teamInfo }
});
const { copyData } = useCopyData();
const baseUrl = global.feConfigs?.customSharePageDomain || location?.origin;
const linkUrl = `${baseUrl}/chat/team?shareTeamId=${teamInfo?._id}${
getValues('showHistory') ? '' : '&showHistory=0'
}`;
// tags Async
const { mutate: onclickAsync, isLoading: creating } = useRequest({
mutationFn: async (data: any) => {
return putUpdateTeamTags({ tagsUrl: data.tagsUrl, teamId: teamInfo?._id });
},
onSuccess(id: string) {
onClose();
},
successToast: t('user.team.Team Tags Async Success'),
errorToast: t('common.Create Failed')
});
const asyncTags = async () => {
console.log('getValues', getValues());
const res: Array<TeamTagsSchema> = await updateTags(teamInfo?._id, getValues().tagsUrl);
setTeamTags(res);
toast({ status: 'success', title: '团队标签同步成功' });
};
useEffect(() => {
console.log('teamInfo', teamInfo);
}, []);
// 获取
return (
<>
<MyModal
isOpen
onClose={onClose}
maxW={['70vw', '1000px']}
w={'100%'}
h={'550px'}
iconSrc="/imgs/modal/team.svg"
isCentered
bg={'white'}
overflow={'hidden'}
title={
<Box>
<Box>{teamInfo?.name}</Box>
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
{'填写标签同步链接,点击同步按钮即可同步'}
</Box>
</Box>
}
>
<ModalBody style={{ padding: '10rpx' }}>
<Flex mt={3} alignItems={'center'}>
<Box mb={2} fontWeight="semibold">
{t('同步链接')}
</Box>
<Input
flex={1}
ml={4}
autoFocus
bg={'myWhite.600'}
placeholder="请输入同步标签"
{...register('tagsUrl', {
required: t('core.app.error.App name can not be empty')
})}
/>
</Flex>
<Flex mt={3} alignItems={'center'}>
<Box mb={2} fontWeight="semibold">
{t('分享链接')}
</Box>
{/* code */}
<Box ml={4} borderRadius={'md'} overflow={'hidden'}>
<Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'} bg={'myWhite.600'} color="blue">
{linkUrl}
</Box>
<MyIcon
name={'copy'}
w={'16px'}
p={3}
bg={'primary.500'}
color={'myWhite.600'}
cursor={'pointer'}
_hover={{ bg: 'primary.400' }}
onClick={() => {
copyData(linkUrl);
}}
/>
</Flex>
</Box>
</Flex>
<Flex mt={3} alignItems={'center'}>
<Box mb={2} fontWeight="semibold">
{t('标签列表')}
</Box>
<HStack
ml={4}
maxHeight={'250'}
bg={'myWhite.600'}
style={{
border: 'solid 2px #f3f3f377',
borderRadius: '5px',
padding: '10px',
maxWidth: '70%',
flexWrap: 'wrap',
overflow: 'scroll'
}}
spacing={1}
>
{_teamsTags.map((item, index) => {
return (
<Tag key={index} mt={2} size={'md'} colorScheme="red" borderRadius="full">
<Avatar
src="https://bit.ly/sage-adeb"
size="xs"
name={item.label}
ml={-2}
mr={2}
/>
{item.label}
</Tag>
);
})}
</HStack>
<Button ml={4} size="md" leftIcon={<RepeatIcon />} onClick={asyncTags}>
</Button>
</Flex>
</ModalBody>
<ModalFooter mb={2}>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
<Button isLoading={creating} onClick={handleSubmit((data) => onclickAsync(data))}>
{t('user.team.Tags Async')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default React.memo(TeamTagsAsync);

View File

@@ -1,97 +0,0 @@
import React from 'react';
import { Box, CloseButton } from '@chakra-ui/react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import ReactDOM from 'react-dom';
import Markdown from '@/components/Markdown';
const Price = ({ onClose }: { onClose: () => void }) => {
const { llmModelList, vectorModelList, audioSpeechModelList, whisperModel } = useSystemStore();
const list = [
{
title: 'AI语言模型',
describe: '',
md: `
| 模型 | 输入价格(¥) | 输出价格(¥) |
| --- | --- | --- |
${llmModelList
?.map((item) => `| ${item.name} | ${item.inputPrice}/1k tokens | ${item.outputPrice}/1k tokens |`)
.join('\n')}`
},
{
title: '索引模型(文档训练 & 文档检索)',
describe: '',
md: `
| 模型 | 价格(¥) |
| --- | --- |
${vectorModelList?.map((item) => `| ${item.name} | ${item.inputPrice}/1k 字符 |`).join('\n')}
`
},
{
title: '语音播放',
describe: '',
md: `
| 模型 | 价格(¥) |
| --- | --- |
${audioSpeechModelList
?.map((item) => `| ${item.name} | ${item.inputPrice}/1k 字符 | - |`)
.join('\n')}`
},
...(whisperModel
? [
{
title: '语音输入',
describe: '',
md: `
| 模型 | 价格(¥) |
| --- | --- |
| ${whisperModel.name} | ${whisperModel.inputPrice}/分钟 | - |`
}
]
: [])
];
return ReactDOM.createPortal(
<Box position={'fixed'} top={0} right={0} bottom={0} left={0} zIndex={99999} bg={'white'}>
<CloseButton
position={'absolute'}
top={'10px'}
right={'20px'}
bg={'myGray.200'}
w={'30px'}
h={'30px'}
borderRadius={'50%'}
onClick={onClose}
/>
<Box overflow={'overlay'} h={'100%'}>
<Box py={[0, 10]} px={5} mx={'auto'} maxW={'1200px'}>
{list.map((item) => (
<Box
display={['block', 'flex']}
key={item.title}
w={'100%'}
mb={4}
pb={6}
_notLast={{
borderBottom: '1px',
borderBottomColor: 'borderColor.high'
}}
>
<Box fontSize={'xl'} fontWeight={'bold'} mb={1} flex={'1 0 0'}>
{item.title}
</Box>
<Box w={['100%', '410px']}>
<Markdown source={item.md}></Markdown>
</Box>
</Box>
))}
</Box>
</Box>
</Box>,
// @ts-ignore
document.querySelector('body')
);
};
export default Price;

View File

@@ -0,0 +1,84 @@
import MyModal from '@/components/MyModal';
import React, { useEffect } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, ModalBody, ModalFooter } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { checkBalancePayResult } from '@/web/support/wallet/bill/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router';
import { getErrText } from '@fastgpt/global/common/error/utils';
export type QRPayProps = {
readPrice: number;
codeUrl: string;
billId: string;
};
const QRCodePayModal = ({
readPrice,
codeUrl,
billId,
onSuccess
}: QRPayProps & { onSuccess?: () => any }) => {
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const dom = document.getElementById('payQRCode');
useEffect(() => {
if (dom && window.QRCode) {
new window.QRCode(dom, {
text: codeUrl,
width: 128,
height: 128,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
}, [dom]);
useQuery(
[billId],
() => {
if (!billId) return null;
return checkBalancePayResult(billId);
},
{
enabled: !!billId,
refetchInterval: 3000,
onSuccess: async (res) => {
if (!res) return;
try {
await onSuccess?.();
toast({
title: res,
status: 'success'
});
} catch (error) {
toast({
title: getErrText(error),
status: 'error'
});
}
setTimeout(() => {
router.reload();
}, 1000);
}
}
);
return (
<MyModal isOpen title={t('user.Pay')} iconSrc="/imgs/modal/pay.svg">
<ModalBody textAlign={'center'}>
<Box mb={3}>: {readPrice}</Box>
<Box id={'payQRCode'} display={'inline-block'} h={'128px'}></Box>
</ModalBody>
<ModalFooter />
</MyModal>
);
};
export default React.memo(QRCodePayModal);

View File

@@ -0,0 +1,116 @@
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/wallet/sub/constants';
import React, { useMemo } from 'react';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { Box, Flex, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
const StandardPlanContentList = ({
level,
mode
}: {
level: `${StandardSubLevelEnum}`;
mode: `${SubModeEnum}`;
}) => {
const { t } = useTranslation();
const { subPlans } = useSystemStore();
const planContent = useMemo(() => {
const plan = subPlans?.standard?.[level];
if (!plan) return;
return {
price: plan.price * (mode === SubModeEnum.month ? 1 : 10),
level: level as `${StandardSubLevelEnum}`,
...standardSubLevelMap[level as `${StandardSubLevelEnum}`],
maxTeamMember: plan.maxTeamMember,
maxAppAmount: plan.maxAppAmount,
maxDatasetAmount: plan.maxDatasetAmount,
chatHistoryStoreDuration: plan.chatHistoryStoreDuration,
maxDatasetSize: plan.maxDatasetSize,
permissionCustomApiKey: plan.permissionCustomApiKey,
permissionCustomCopyright: plan.permissionCustomCopyright,
trainingWeight: plan.trainingWeight,
permissionReRank: plan.permissionReRank,
totalPoints: plan.totalPoints * (mode === SubModeEnum.month ? 1 : 12),
permissionWebsiteSync: plan.permissionWebsiteSync
};
}, [subPlans?.standard, level, mode]);
return planContent ? (
<Grid gap={4}>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max members', {
amount: planContent.maxTeamMember
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max app', {
amount: planContent.maxAppAmount
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.Max dataset', {
amount: planContent.maxDatasetAmount
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.function.History store', {
amount: planContent.chatHistoryStoreDuration
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box fontWeight={'bold'}>
{t('support.wallet.subscription.function.Max dataset size', {
amount: planContent.maxDatasetSize
})}
</Box>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>
{t('support.wallet.subscription.function.Points', {
amount: planContent.totalPoints
})}
</Box>
</Flex>
</Flex>
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>
{t('support.wallet.subscription.Training weight', {
weight: planContent.trainingWeight
})}
</Box>
</Flex>
{!!planContent.permissionReRank && (
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}></Box>
</Flex>
)}
{!!planContent.permissionWebsiteSync && (
<Flex alignItems={'center'}>
<MyIcon name={'price/right'} w={'16px'} mr={3} />
<Box color={'myGray.600'}>Web站点同步</Box>
</Flex>
)}
</Grid>
) : null;
};
export default StandardPlanContentList;

View File

@@ -1,240 +0,0 @@
import React, { useState } from 'react';
import MyModal from '@/components/MyModal';
import { useTranslation } from 'next-i18next';
import {
Box,
Flex,
ModalBody,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
ModalFooter,
Button
} from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import {
getTeamDatasetValidSub,
posCheckTeamDatasetSizeSub,
postUpdateTeamDatasetSizeSub,
putTeamDatasetSubStatus
} from '@/web/support/wallet/sub/api';
import Markdown from '@/components/Markdown';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { useRequest } from '@/web/common/hooks/useRequest';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
import MySelect from '@/components/Select';
import {
SubStatusEnum,
SubTypeEnum,
subSelectMap
} from '@fastgpt/global/support/wallet/sub/constants';
import { SubDatasetSizePreviewCheckResponse } from '@fastgpt/global/support/wallet/sub/api.d';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/bill/tools';
import { useUserStore } from '@/web/support/user/useUserStore';
const SubDatasetModal = ({ onClose }: { onClose: () => void }) => {
const { subPlans } = useSystemStore();
const datasetStorePrice = subPlans?.extraDatasetSize?.price || 0;
const { t } = useTranslation();
const router = useRouter();
const { ConfirmModal, openConfirm } = useConfirm({});
const { userInfo } = useUserStore();
const [datasetSize, setDatasetSize] = useState(0);
const [isRenew, setIsRenew] = useState('false');
const { data: teamSubPlan } = useQuery(['getTeamDatasetValidSub'], getTeamDatasetValidSub, {
onSuccess(res) {
setIsRenew(res?.extraDatasetSize?.status === SubStatusEnum.active ? 'true' : 'false');
setDatasetSize((res?.extraDatasetSize?.nextExtraDatasetSize || 0) / 1000);
}
});
const { mutate: onClickUpdateSub, isLoading: isPaying } = useRequest({
mutationFn: () => postUpdateTeamDatasetSizeSub({ size: datasetSize }),
onSuccess() {
setTimeout(() => {
router.reload();
}, 100);
},
successToast: t('common.Update success'),
errorToast: t('common.error.Update error')
});
const { mutate: onClickPreviewCheck, isLoading: isFetchingPreviewCheck } = useRequest({
mutationFn: () =>
posCheckTeamDatasetSizeSub({
size: datasetSize
}),
onSuccess(res: SubDatasetSizePreviewCheckResponse) {
if (!res.payForNewSub) {
onClickUpdateSub('');
return;
} else {
openConfirm(
() => {
if (!res.balanceEnough) return;
onClickUpdateSub('');
},
undefined,
<Box>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{teamSubPlan?.extraDatasetSize?.currentExtraDatasetSize || 0}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{res.newSubSize}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{formatStorePrice2Read(res.newPlanPrice)}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{formatStorePrice2Read(res.payPrice)}</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>30</Box>
</Flex>
<Flex>
<Box flex={'0 0 100px'}>:</Box>
<Box>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</Box>
</Flex>
{!res.balanceEnough && (
<Box mt={1} color={'red.600'}>
</Box>
)}
</Box>
)();
}
},
errorToast: t('common.error.Update error')
});
const { mutate: onUpdateStatus } = useRequest({
mutationFn: (e: 'true' | 'false') => {
setIsRenew(e);
return putTeamDatasetSubStatus({
status: subSelectMap[e],
type: SubTypeEnum.extraDatasetSize
});
},
successToast: t('common.Update success'),
errorToast: t('common.error.Update error')
});
const isLoading = isPaying || isFetchingPreviewCheck;
return (
<MyModal
isOpen
iconSrc="/imgs/module/db.png"
title={t('support.wallet.subscription.Dataset store')}
>
<ModalBody>
<>
<Flex alignItems={'center'}>
{t('support.user.Price')}
<MyTooltip label={t('support.wallet.subscription.Dataset store price tip')}>
<QuestionOutlineIcon ml={1} />
</MyTooltip>
</Flex>
<Markdown
source={`
| 套餐知识库容量 | ${teamSubPlan?.standardMaxDatasetSize || Infinity}条 |
| --- | --- |
| 额外知识库 | ${datasetStorePrice}元/1000条/月 |
`}
/>
</>
<Flex mt={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Current dataset store')}: </Box>
<Box ml={2} fontWeight={'bold'} flex={1}>
{teamSubPlan?.extraDatasetSize?.currentExtraDatasetSize || 0}
{t('core.dataset.data.unit')}
</Box>
</Flex>
{teamSubPlan?.extraDatasetSize?.nextExtraDatasetSize !== undefined && (
<Flex mt={4}>
<Box flex={'0 0 120px'}>{t('support.wallet.subscription.Next sub dataset size')}: </Box>
<Box ml={2} fontWeight={'bold'} flex={1}>
{teamSubPlan?.extraDatasetSize?.nextExtraDatasetSize || 0}
{t('core.dataset.data.unit')}
</Box>
</Flex>
)}
{!!teamSubPlan?.extraDatasetSize?.startTime && (
<Flex mt={3}>
<Box flex={'0 0 120px'}>: </Box>
<Box ml={2}>{formatTime2YMDHM(teamSubPlan?.extraDatasetSize?.startTime)}</Box>
</Flex>
)}
{!!teamSubPlan?.extraDatasetSize?.expiredTime && (
<Flex mt={3}>
<Box flex={'0 0 120px'}>: </Box>
<Box ml={2}>{formatTime2YMDHM(teamSubPlan?.extraDatasetSize?.expiredTime)}</Box>
</Flex>
)}
<Flex mt={3} alignItems={'center'}>
<Box flex={'0 0 120px'}>: </Box>
<MySelect
ml={2}
value={isRenew}
size={'sm'}
w={'150px'}
list={[
{ label: '自动续费', value: 'true' },
{ label: '不自动续费', value: 'false' }
]}
onchange={onUpdateStatus}
/>
</Flex>
<Box mt={4}>
<Box>{t('support.wallet.subscription.Update extra dataset size')}</Box>
<Flex alignItems={'center'} mt={1}>
<NumberInput
flex={1}
min={0}
max={10000}
step={1}
value={datasetSize}
position={'relative'}
onChange={(e) => {
setDatasetSize(Number(e));
}}
>
<NumberInputField value={datasetSize} step={1} min={0} max={10000} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<Box ml={2}>000{t('core.dataset.data.unit')}</Box>
</Flex>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Close')}
</Button>
{datasetSize * 1000 !== teamSubPlan?.extraDatasetSize?.nextExtraDatasetSize && (
<Button ml={3} isLoading={isLoading} onClick={onClickPreviewCheck}>
{t('common.Confirm')}
</Button>
)}
</ModalFooter>
<ConfirmModal />
</MyModal>
);
};
export default SubDatasetModal;