V4.9.7 feature (#4669)

* update doc

* feat: Add coupon redemption feature for team subscriptions (#4595)

* feat: Add coupon redemption feature for team subscriptions

- Introduced `TeamCouponSub` and `TeamCouponSchema` types
- Added `redeemCoupon` API endpoint
- Updated UI to include a modal for coupon redemption
- Added new icon and translations for "Redeem coupon"

* perf: remove field teamId

* perf: use dynamic import

* refactor: move to page component

* perf: coupon code

* perf: mcp server

* perf: test

* auto layout (#4634)

* fix 4.9.6 (#4631)

* fix debug quote list

* delete next text node match

* fix extract default boolean value

* export latest 100 chat items

* fix quote item ui

* doc

* fix doc

* feat: auto layout

* perf: auto layout

* fix: auto layout null

* add start node

---------

Co-authored-by: heheer <heheer@sealos.io>

* fix: share link (#4644)

* Add workflow run duration;Get audio duration (#4645)

* add duration

* get audio duration

* Custom config path (#4649)

* feat: 通过环境变量DATA_PATH获取配置文件目录 (#4622)

通过环境变量DATA_PATH获取配置文件目录,以应对不同的部署方式的多样化需求

* feat: custom configjson path

* doc

---------

Co-authored-by: John Chen <sss1991@163.com>

* 程序api调用场景下,如果大量调用带有图片或视频,产生的聊天记录会导致后台mongo数据库异常。这个修改给api客户端一个禁止生成聊天记录的选项,避免这个后果。 (#3964)

* update special chatId

* perf: vector db rename

* update operationLog (#4647)

* update operationLog

* combine operationLogMap

* solve operationI18nLogMap bug

* remoce log

* feat: Rerank usage (#4654)

* refresh concat when update (#4655)

* fix: refresh code

* perf: timer lock

* Fix operationLog (#4657)

* perf: http streamable mcp

* add alipay (#4630)

* perf: subplan ui

* perf: pay code

* hiden bank tip

* Fix: pay error (#4665)

* fix quote number (#4666)

* remove log

---------

Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: John Chen <sss1991@163.com>
Co-authored-by: gaord <bengao168@msn.com>
Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com>
This commit is contained in:
Archer
2025-04-26 16:17:21 +08:00
committed by GitHub
parent a669a60fe6
commit 0720bbe4da
143 changed files with 2067 additions and 1093 deletions

View File

@@ -20,13 +20,14 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import {
BillStatusEnum,
BillTypeEnum,
billPayWayMap,
billStatusMap,
billTypeMap
} from '@fastgpt/global/support/wallet/bill/constants';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { standardSubLevelMap, subModeMap } from '@fastgpt/global/support/wallet/sub/constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyModal from '@fastgpt/web/components/common/MyModal';
@@ -68,39 +69,33 @@ const BillTable = () => {
defaultRequest: false
});
const { mutate: handleRefreshPayOrder, isLoading: isRefreshing } = useRequest({
mutationFn: async (payId: string) => {
try {
const data = await checkBalancePayResult(payId);
const { runAsync: handleRefreshPayOrder, loading: isRefreshing } = useRequest2(
async (payId: string) => {
const { status, description } = await checkBalancePayResult(payId);
if (status === BillStatusEnum.SUCCESS) {
toast({
title: data,
title: t('common:pay_success'),
status: 'success'
});
} catch (error: any) {
} else {
toast({
title: error?.message,
title: t(description as any),
status: 'warning'
});
console.log(error);
}
try {
if (status === BillStatusEnum.SUCCESS || status === BillStatusEnum.CLOSED) {
getData(1);
} catch (error) {}
}
}
});
);
useEffect(() => {
getData(1);
}, [billType]);
return (
<MyBox
isLoading={isLoading || isRefreshing}
position={'relative'}
h={'100%'}
minH={'50vh'}
overflow={'overlay'}
>
<MyBox isLoading={isLoading} position={'relative'} h={'100%'} minH={'50vh'}>
<TableContainer>
<Table>
<Thead>
@@ -135,7 +130,12 @@ const BillTable = () => {
<Td>{t(billStatusMap[item.status]?.label as any)}</Td>
<Td>
{item.status === 'NOTPAY' && (
<Button mr={4} onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
<Button
isLoading={isRefreshing}
mr={4}
onClick={() => handleRefreshPayOrder(item._id)}
size={'sm'}
>
{t('account_bill:update')}
</Button>
)}
@@ -210,11 +210,13 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
<Box>{t(billPayWayMap[bill.metadata.payWay]?.label as any)}</Box>
</Flex>
)}
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('account_bill:support_wallet_amount')}:</FormLabel>
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(bill.price) })}</Box>
</Flex>
{bill.metadata && (
{!!bill.price && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('account_bill:support_wallet_amount')}:</FormLabel>
<Box>{t('account_bill:yuan', { amount: formatStorePrice2Read(bill.price) })}</Box>
</Flex>
)}
{bill.metadata && !!bill.price && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('account_bill:has_invoice')}:</FormLabel>
{bill.metadata.payWay === 'balance' ? (
@@ -239,7 +241,7 @@ function BillDetailModal({ bill, onClose }: { bill: BillSchemaType; onClose: ()
{bill.metadata?.month !== undefined && (
<Flex alignItems={'center'} pb={4}>
<FormLabel flex={'0 0 120px'}>{t('account_bill:subscription_mode_month')}:</FormLabel>
<Box>{bill.metadata?.month}</Box>
<Box>{`${bill.metadata?.month} ${t('account_bill:month')}`}</Box>
</Flex>
)}
{bill.metadata?.datasetSize !== undefined && (

View File

@@ -221,7 +221,7 @@ const InvoiceHeaderForm = () => {
return (
<>
<MyBox isLoading={isLoading} pt={['1rem', '3.5rem']}>
<MyBox isLoading={isLoading} pt={'1rem'}>
<Flex w={'100%'} overflow={'auto'} justify={'center'} flexDir={'column'} align={'center'}>
<InvoiceHeaderSingleForm inputForm={inputForm} />
<Flex w={'100%'} justify={'center'} mt={'3rem'}>

View File

@@ -22,6 +22,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import dayjs from 'dayjs';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import MyModal from '@fastgpt/web/components/common/MyModal';
const InvoiceTable = () => {
const { t } = useTranslation();
const [invoiceDetailData, setInvoiceDetailData] = useState<InvoiceSchemaType | ''>('');

View File

@@ -0,0 +1,57 @@
import { redeemCoupon } from '@/web/support/user/team/api';
import { Button, Input, VStack, Text, ModalBody, Box, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import React from 'react';
import { useTranslation } from 'react-i18next';
const RedeemCouponModal = ({
onClose,
onSuccess
}: {
onClose: () => void;
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const [couponCode, setCouponCode] = React.useState('');
const { runAsync: redeemCouponAsync, loading } = useRequest2(redeemCoupon, {
manual: true,
onSuccess: () => {
onSuccess();
onClose();
},
successToast: t('common:common.Success')
});
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="support/account/coupon"
title={t('account_info:redeem_coupon')}
>
<ModalBody>
<Box fontWeight={500} color={'myGray.900'} mb={'1'}>
{t('account_info:redeem_coupon')}
</Box>
<Input
placeholder={t('account_info:redeem_coupon')}
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button ml={2} isLoading={loading} onClick={() => redeemCouponAsync(couponCode)}>
{t('account_info:confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default RedeemCouponModal;

View File

@@ -127,11 +127,11 @@ export const ModelEditModal = ({
);
const priceUnit = useMemo(() => {
if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens';
if (isLLMModel || isEmbeddingModel || isRerankModel) return '/ 1k Tokens';
if (isTTSModel) return `/ 1k ${t('common:unit.character')}`;
if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`;
return '';
}, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]);
}, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel, isRerankModel]);
const { runAsync: updateModel, loading: updatingModel } = useRequest2(
async (data: SystemModelItemType) => {

View File

@@ -141,6 +141,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
typeLabel: t('common:model.type.embedding'),
priceLabel: (
<Flex color={'myGray.700'}>
{`${t('common:common.Input')}: `}
<Box fontWeight={'bold'} color={'myGray.900'} mr={0.5}>
{item.charsPointsPrice || 0}
</Box>
@@ -184,7 +185,17 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
.map((item) => ({
...item,
typeLabel: t('common:model.type.reRank'),
priceLabel: <Flex color={'myGray.700'}>- </Flex>,
priceLabel: item.charsPointsPrice ? (
<Flex color={'myGray.700'}>
{`${t('common:common.Input')}: `}
<Box fontWeight={'bold'} color={'myGray.900'} mr={0.5}>
{item.charsPointsPrice}
</Box>
{` ${t('common:support.wallet.subscription.point')} / 1K Tokens`}
</Flex>
) : (
'-'
),
tagColor: 'red'
}));

View File

@@ -190,8 +190,9 @@ function EditModal({
/>
{isOpenContact && (
<UpdateContact
onClose={() => {
onCloseContact();
onClose={onCloseContact}
onSuccess={(val) => {
setValue('notificationAccount', val);
}}
mode="notification_account"
/>

View File

@@ -17,7 +17,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getOperationLogs } from '@/web/support/user/team/operantionLog/api';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { operationLogI18nMap } from '@fastgpt/service/support/operationLog/constants';
import { operationLogMap } from '@fastgpt/service/support/operationLog/constants';
import { OperationLogEventEnum } from '@fastgpt/global/support/operationLog/constants';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import UserBox from '@fastgpt/web/components/common/UserBox';
@@ -52,7 +52,7 @@ function OperationLogTable({ Tabs }: { Tabs: React.ReactNode }) {
const eventOptions = useMemo(
() =>
Object.values(OperationLogEventEnum).map((event) => ({
label: t(operationLogI18nMap[event].typeLabel),
label: t(operationLogMap[event].typeLabel),
value: event
})),
[t]
@@ -158,7 +158,7 @@ function OperationLogTable({ Tabs }: { Tabs: React.ReactNode }) {
</Thead>
<Tbody>
{operationLogs?.map((log) => {
const i18nData = operationLogI18nMap[log.event];
const i18nData = operationLogMap[log.event];
const metadata = { ...log.metadata };
if (log.event === OperationLogEventEnum.ASSIGN_PERMISSION) {

View File

@@ -124,7 +124,7 @@ const EditForm = ({
}
}));
}
}, [selectedModel]);
}, [selectedModel, setAppForm]);
return (
<>

View File

@@ -1,34 +1,305 @@
import { Box, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import { Box, HStack, StackProps } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comment';
import { useContextSelector } from 'use-context-selector';
import { useReactFlow } from 'reactflow';
import { Node, useReactFlow } from 'reactflow';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { WorkflowEventContext } from '../../context/workflowEventContext';
import { WorkflowContext } from '../../context';
import dagre from '@dagrejs/dagre';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { WorkflowStatusContext } from '../../context/workflowStatusContext';
import { cloneDeep } from 'lodash';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
const ContextMenu = () => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setNodes);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu!);
const setMenu = useContextSelector(WorkflowEventContext, (ctx) => ctx.setMenu);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
const getParentNodeSizeAndPosition = useContextSelector(
WorkflowStatusContext,
(v) => v.getParentNodeSizeAndPosition
);
const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({
template: CommentNode,
position: screenToFlowPosition({ x: menu?.left ?? 0, y: menu?.top ?? 0 }),
t
});
const { fitView, screenToFlowPosition } = useReactFlow();
const allUnFolded = useMemo(() => {
return !!menu ? nodeList.some((node) => node.isFolded) : false;
}, [nodeList, menu]);
return !!menu ? (
const onLayout = useCallback(() => {
const updateChildNodesPosition = ({
startNode,
nodes,
edges
}: {
startNode: Node<FlowNodeItemType>;
nodes: Node<FlowNodeItemType>[];
edges: any[];
}) => {
const startPosition = { x: startNode.position.x, y: startNode.position.y };
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({
rankdir: 'LR',
nodesep: 80, // Horizontal space
ranksep: 120 // Vertical space
});
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: node.width!, height: node.height! });
});
// Find connected nodes
const connectedNodeIds = new Set<string>();
edges.forEach((edge) => {
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedStartNode = dagreGraph.node(startNode.data.nodeId);
const offsetX = startPosition.x - (layoutedStartNode.x - startNode.width! / 2);
const offsetY = startPosition.y - (layoutedStartNode.y - startNode.height! / 2);
nodes.forEach((node) => {
if (!connectedNodeIds.has(node.id)) {
return;
}
const nodeWithPosition = dagreGraph.node(node.id);
node.position = {
x: nodeWithPosition.x - node.width! / 2 + offsetX,
y: nodeWithPosition.y - node.height! / 2 + offsetY
};
});
};
const updateParentNodesPosition = ({
startNode,
nodes,
edges
}: {
startNode: Node<FlowNodeItemType>;
nodes: Node<FlowNodeItemType>[];
edges: any[];
}) => {
const startPosition = { x: startNode.position.x, y: startNode.position.y };
const childNodeIdsSet = new Set(
nodes.filter((node) => !!node.data.parentNodeId).map((node) => node.data.nodeId)
);
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({
rankdir: 'LR',
nodesep: 80, // Horizontal space
ranksep: 120 // Vertical space
});
nodes.forEach((node) => {
if (childNodeIdsSet.has(node.data.nodeId)) return;
dagreGraph.setNode(node.id, { width: node.width!, height: node.height! });
});
// Find connected nodes
const connectedNodeIds = new Set<string>();
edges.forEach((edge) => {
if (childNodeIdsSet.has(edge.source)) return;
if (childNodeIdsSet.has(edge.target)) return;
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedStartNode = dagreGraph.node(startNode.data.nodeId);
const offsetX = startPosition.x - (layoutedStartNode.x - startNode.width! / 2);
const offsetY = startPosition.y - (layoutedStartNode.y - startNode.height! / 2);
nodes.forEach((node) => {
if (!connectedNodeIds.has(node.id) || childNodeIdsSet.has(node.data.nodeId)) {
return;
}
const nodeWithPosition = dagreGraph.node(node.id);
const targetX = nodeWithPosition.x - node.width! / 2 + offsetX;
const targetY = nodeWithPosition.y - node.height! / 2 + offsetY;
const diffX = targetX - node.position.x;
const diffY = targetY - node.position.y;
node.position = {
x: targetX,
y: targetY
};
// Update child nodes position
nodes.forEach((childNode) => {
if (childNode.data.parentNodeId === node.data.nodeId) {
childNode.position = {
x: childNode.position.x + diffX,
y: childNode.position.y + diffY
};
}
});
});
};
setNodes((nodes) => {
let newNodes = cloneDeep(nodes);
setEdges((edges) => {
const childNodesIdSet = new Set();
// 1. Layout child nodes
const childNodesMap: Record<string, Node<FlowNodeItemType>[]> = {};
newNodes.forEach((node) => {
const parentId = node.data.parentNodeId;
if (parentId) {
childNodesIdSet.add(parentId);
if (!childNodesMap[parentId]) {
childNodesMap[parentId] = [];
}
childNodesMap[parentId].push(node);
}
});
const childNodesArr = Object.values(childNodesMap);
if (childNodesArr.length > 0) {
childNodesArr.forEach((childNodes) => {
updateChildNodesPosition({
startNode: childNodes[0],
nodes: childNodes,
edges
});
});
}
// 2. Reset parent node size and position
const parentNodes = newNodes.filter((node) => childNodesIdSet.has(node.data.nodeId));
parentNodes.forEach((node) => {
const res = getParentNodeSizeAndPosition({
nodes: newNodes,
parentId: node.data.nodeId
});
if (!res) return;
const { parentX, parentY, nodeWidth, nodeHeight, childHeight, childWidth } = res;
node.position = {
x: parentX,
y: parentY
};
node.width = nodeWidth;
node.height = nodeHeight;
node.data.inputs.forEach((input) => {
if (input.key === NodeInputKeyEnum.nodeHeight) {
input.value = childHeight;
} else if (input.key === NodeInputKeyEnum.nodeWidth) {
input.value = childWidth;
}
});
});
// 3. Layout parent node
updateParentNodesPosition({
startNode:
newNodes.find((node) =>
[
FlowNodeTypeEnum.systemConfig,
FlowNodeTypeEnum.pluginConfig,
FlowNodeTypeEnum.workflowStart,
FlowNodeTypeEnum.pluginInput
].includes(node.data.flowNodeType)
) || newNodes[0],
nodes: newNodes,
edges
});
return edges;
});
return newNodes;
});
setTimeout(() => {
fitView();
});
}, []);
const onAddComment = useCallback(() => {
const newNode = nodeTemplate2FlowNode({
template: CommentNode,
position: screenToFlowPosition({ x: menu?.left ?? 0, y: (menu?.top ?? 0) + 100 }),
t
});
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}, [menu]);
const onFold = useCallback(() => {
setNodes((state) => {
return state.map((node) => ({
...node,
data: {
...node.data,
isFolded: !allUnFolded
}
}));
});
}, [allUnFolded]);
const ContextMenuItem = useCallback(
({
icon,
label,
onClick,
...props
}: {
icon: string;
label: string;
onClick: () => any;
} & StackProps) => {
return (
<HStack
px={2}
py={1}
cursor={'pointer'}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
onClick();
setMenu(null);
}}
{...props}
>
<MyIcon name={icon as any} w={'1rem'} ml={1} />
<Box fontSize={'sm'} fontWeight={'500'}>
{label}
</Box>
</HStack>
);
},
[setMenu]
);
return (
<Box position="relative">
<Box
position="absolute"
@@ -55,61 +326,26 @@ const ContextMenu = () => {
p={1}
zIndex={10}
>
<Flex
alignItems={'center'}
px={2}
py={1}
cursor={'pointer'}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
>
<MyIcon name="comment" w={'1rem'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
<Flex
mt={1}
alignItems={'center'}
px={2}
py={1}
cursor={'pointer'}
borderRadius={'sm'}
_hover={{ bg: 'myGray.50', color: 'primary.500' }}
onClick={() => {
setMenu(null);
setNodes((state) => {
return state.map((node) => ({
...node,
data: {
...node.data,
isFolded: !allUnFolded
}
}));
});
}}
>
<MyIcon name="common/select" w={'1rem'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{allUnFolded ? t('workflow:unFoldAll') : t('workflow:foldAll')}
</Box>
</Flex>
<ContextMenuItem
mb={1}
icon="alignLeft"
label={t('workflow:auto_align')}
onClick={onLayout}
/>
<ContextMenuItem
mb={1}
icon="comment"
label={t('workflow:context_menu.add_comment')}
onClick={onAddComment}
/>
<ContextMenuItem
icon="common/select"
label={allUnFolded ? t('workflow:unFoldAll') : t('workflow:foldAll')}
onClick={onFold}
/>
</Box>
</Box>
) : null;
);
};
export default React.memo(ContextMenu);

View File

@@ -73,6 +73,7 @@ const Workflow = () => {
WorkflowEventContext,
(v) => v.workflowControlMode
);
const menu = useContextSelector(WorkflowEventContext, (v) => v.menu);
const {
handleNodesChange,
@@ -163,7 +164,7 @@ const Workflow = () => {
: {})}
onNodeDragStop={onNodeDragStop}
>
<ContextMenu />
{!!menu && <ContextMenu />}
<FlowController />
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />
</ReactFlow>

View File

@@ -26,7 +26,7 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
searchMode: DatasetSearchModeEnum.embedding,
limit: 5,
similarity: 0.5,
usingReRank: false
usingReRank: true
});
const {

View File

@@ -27,7 +27,7 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
embeddingWeight: 0.5,
limit: 3000,
similarity: 0.5,
usingReRank: false,
usingReRank: true,
rerankModel: defaultModels.llm?.model,
rerankWeight: 0.6,
datasetSearchUsingExtensionQuery: true,

View File

@@ -7,7 +7,7 @@ import { AppContext } from '../../context';
import { compareSnapshot } from '@/web/core/workflow/utils';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useTranslation } from 'next-i18next';
import { getNodesBounds, Node } from 'reactflow';
import { Node } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
@@ -20,6 +20,22 @@ type WorkflowStatusContextType = {
isSaved: boolean;
leaveSaveSign: React.MutableRefObject<boolean>;
resetParentNodeSizeAndPosition: (parentId: string) => void;
getParentNodeSizeAndPosition: ({
nodes,
parentId
}: {
nodes: Node<FlowNodeItemType>[];
parentId: string;
}) =>
| {
parentX: number;
parentY: number;
childWidth: number;
childHeight: number;
nodeWidth: number;
nodeHeight: number;
}
| undefined;
};
export const WorkflowStatusContext = createContext<WorkflowStatusContextType>({
@@ -94,30 +110,70 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const getParentNodeSizeAndPosition = useMemoizedFn(
({ nodes, parentId }: { nodes: Node<FlowNodeItemType>[]; parentId: string }) => {
const { childNodes, loopNode } = nodes.reduce(
(acc, node) => {
if (node.data.parentNodeId === parentId) {
acc.childNodes.push(node);
}
if (node.id === parentId) {
acc.loopNode = node;
}
return acc;
},
{ childNodes: [] as Node[], loopNode: undefined as Node<FlowNodeItemType> | undefined }
);
if (!loopNode) return;
const loopChilWidth =
loopNode.data.inputs.find((node) => node.key === NodeInputKeyEnum.nodeWidth)?.value ?? 0;
const loopChilHeight =
loopNode.data.inputs.find((node) => node.key === NodeInputKeyEnum.nodeHeight)?.value ?? 0;
// 初始化为第一个节点的边界
let minX = childNodes[0].position.x;
let minY = childNodes[0].position.y;
let maxX = childNodes[0].position.x + (childNodes[0].width || 0);
let maxY = childNodes[0].position.y + (childNodes[0].height || 0);
// 遍历所有节点找出最小/最大边界
childNodes.forEach((node) => {
const nodeWidth = node.width || 0;
const nodeHeight = node.height || 0;
minX = Math.min(minX, node.position.x);
minY = Math.min(minY, node.position.y);
maxX = Math.max(maxX, node.position.x + nodeWidth);
maxY = Math.max(maxY, node.position.y + nodeHeight);
});
const childWidth = Math.max(maxX - minX + 80, 840);
const childHeight = Math.max(maxY - minY + 80, 600);
const diffWidth = childWidth - loopChilWidth;
const diffHeight = childHeight - loopChilHeight;
const targetNodeWidth = (loopNode.width ?? 0) + diffWidth;
const targetNodeHeight = (loopNode.height ?? 0) + diffHeight;
const offsetHeight =
loopNode.data.inputs.find((input) => input.key === NodeInputKeyEnum.loopNodeInputHeight)
?.value ?? 83;
return {
parentX: Math.round(minX - 70),
parentY: Math.round(minY - offsetHeight - 240),
childWidth,
childHeight,
nodeWidth: targetNodeWidth,
nodeHeight: targetNodeHeight
};
}
);
const resetParentNodeSizeAndPosition = useMemoizedFn((parentId: string) => {
const { childNodes, loopNode } = nodes.reduce(
(acc, node) => {
if (node.data.parentNodeId === parentId) {
acc.childNodes.push(node);
}
if (node.id === parentId) {
acc.loopNode = node;
}
return acc;
},
{ childNodes: [] as Node[], loopNode: undefined as Node<FlowNodeItemType> | undefined }
);
if (!loopNode) return;
const rect = getNodesBounds(childNodes);
// Calculate parent node size with minimum width/height constraints
const width = Math.max(rect.width + 80, 840);
const height = Math.max(rect.height + 80, 600);
const offsetHeight =
loopNode.data.inputs.find((input) => input.key === NodeInputKeyEnum.loopNodeInputHeight)
?.value ?? 83;
const res = getParentNodeSizeAndPosition({ nodes, parentId });
if (!res) return;
const { parentX, parentY, childWidth, childHeight } = res;
// Update parentNode size and position
onChangeNode({
@@ -126,7 +182,7 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
key: NodeInputKeyEnum.nodeWidth,
value: {
...Input_Template_Node_Width,
value: width
value: childWidth
}
});
onChangeNode({
@@ -135,7 +191,7 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
key: NodeInputKeyEnum.nodeHeight,
value: {
...Input_Template_Node_Height,
value: height
value: childHeight
}
});
// Update parentNode position
@@ -144,8 +200,8 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
id: parentId,
type: 'position',
position: {
x: Math.round(rect.x - 70),
y: Math.round(rect.y - offsetHeight - 240)
x: parentX,
y: parentY
}
}
]);
@@ -155,9 +211,10 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
return {
isSaved,
leaveSaveSign,
resetParentNodeSizeAndPosition
resetParentNodeSizeAndPosition,
getParentNodeSizeAndPosition
};
}, [isSaved, resetParentNodeSizeAndPosition]);
}, [isSaved, resetParentNodeSizeAndPosition, getParentNodeSizeAndPosition]);
return (
<WorkflowStatusContext.Provider value={contextValue}>{children}</WorkflowStatusContext.Provider>
);

View File

@@ -35,6 +35,7 @@ import { getAppFolderPath } from '@/web/core/app/api/app';
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { postCreateMcpServer, putUpdateMcpServer } from '../../../web/support/mcp/api';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
export type EditMcForm = {
id?: string;
@@ -62,7 +63,7 @@ const SelectAppModal = ({
{
appId: string;
toolName: string;
toolAlias: string;
appName: string;
avatar: string;
description: string;
}[]
@@ -76,7 +77,7 @@ const SelectAppModal = ({
data.map((item) => ({
appId: item.id,
toolName: item.name,
toolAlias: item.name,
appName: item.name,
avatar: item.avatar,
description: selectedApps.find((app) => app.appId === item.id)?.description || ''
}))
@@ -176,7 +177,7 @@ const SelectAppModal = ({
{
appId: item._id,
toolName: item.name,
toolAlias: item.name,
appName: item.name,
avatar: item.avatar,
description: item.intro
}
@@ -281,7 +282,7 @@ const EditMcpModal = ({
apps: data.apps.map((item) => ({
appId: item.appId,
toolName: item.toolName,
toolAlias: item.toolAlias,
appName: item.appName,
description: item.description
}))
}),
@@ -299,7 +300,7 @@ const EditMcpModal = ({
apps: data.apps.map((item) => ({
appId: item.appId,
toolName: item.toolName,
toolAlias: item.toolAlias,
appName: item.appName,
description: item.description
}))
}),
@@ -317,7 +318,7 @@ const EditMcpModal = ({
iconSrc="key"
title={isEdit ? t('dashboard_mcp:edit_mcp') : t('dashboard_mcp:create_mcp')}
w={'100%'}
maxW={['90vw', '600px']}
maxW={['90vw', '800px']}
isOpen
onClose={onClose}
>
@@ -339,8 +340,11 @@ const EditMcpModal = ({
<Table>
<Thead>
<Tr>
<Th>
{t('dashboard_mcp:tool_name')}
<QuestionTip label={t('dashboard_mcp:tool_name_tip')} />
</Th>
<Th>{t('dashboard_mcp:app_name')}</Th>
<Th>{t('dashboard_mcp:app_tool_name')}</Th>
<Th>{t('dashboard_mcp:app_description')}</Th>
<Th></Th>
</Tr>
@@ -349,15 +353,15 @@ const EditMcpModal = ({
{apps.map((app, index) => {
return (
<Tr key={app.id} fontWeight={500} fontSize={'mini'} color={'myGray.900'}>
<Td>{app.toolName}</Td>
<Td>
<Input
{...register(`apps.${index}.toolAlias`)}
placeholder={app.toolName}
{...register(`apps.${index}.toolName`, { required: true })}
placeholder={t('dashboard_mcp:tool_name_placeholder')}
bg={'myGray.50'}
w={'100%'}
/>
</Td>
<Td>{app.appName}</Td>
<Td>
<Input
{...register(`apps.${index}.description`, { required: true })}
@@ -413,7 +417,7 @@ const EditMcpModal = ({
e.map((item) => ({
appId: item.appId,
toolName: item.toolName,
toolAlias: item.toolAlias,
appName: item.appName,
description: item.description
}))
);

View File

@@ -71,7 +71,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
searchParams: {
searchMode: DatasetSearchModeEnum.embedding,
embeddingWeight: 0.5,
usingReRank: false,
usingReRank: true,
rerankModel: defaultModels?.rerank?.model,
rerankWeight: 0.5,
limit: 5000,

View File

@@ -1,21 +1,20 @@
import { Box, Flex, Grid, Button, VStack } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useState } from 'react';
import React, { useState } from 'react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useForm } from 'react-hook-form';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { getWxPayQRCode } from '@/web/support/wallet/bill/api';
import { postCreatePayBill } from '@/web/support/wallet/bill/api';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { subPlans } = useSystemStore();
const [loading, setLoading] = useState(false);
const [qrPayData, setQRPayData] = useState<QRPayProps>();
// extra dataset
@@ -26,40 +25,33 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => {
month: 1
}
});
const onclickBuyDatasetSize = useCallback(
const { runAsync: onclickBuyDatasetSize, loading: isLoadingBuyDatasetSize } = useRequest2(
async ({ datasetSize, month }: { datasetSize: number; month: number }) => {
try {
datasetSize = Math.ceil(datasetSize);
month = Math.ceil(month);
datasetSize = Math.ceil(datasetSize);
month = Math.ceil(month);
const datasetSizePayAmount = datasetSize * month * extraDatasetPrice;
if (datasetSizePayAmount === 0) {
return toast({
status: 'warning',
title: t('common:support.wallet.amount_0')
});
}
setLoading(true);
const res = await getWxPayQRCode({
type: BillTypeEnum.extraDatasetSub,
month,
extraDatasetSize: datasetSize
});
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
});
} catch (err) {
toast({
title: getErrText(err),
status: 'error'
const datasetSizePayAmount = datasetSize * month * extraDatasetPrice;
if (datasetSizePayAmount === 0) {
return toast({
status: 'warning',
title: t('common:support.wallet.amount_0')
});
}
setLoading(false);
const res = await postCreatePayBill({
type: BillTypeEnum.extraDatasetSub,
month,
extraDatasetSize: datasetSize
});
setQRPayData({
tip: t('common:button.extra_dataset_size_tip'),
...res
});
},
[extraDatasetPrice, toast]
{
manual: true,
refreshDeps: [extraDatasetPrice]
}
);
// extra ai points
@@ -70,41 +62,34 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => {
month: 1
}
});
const onclickBuyExtraPoints = useCallback(
const { runAsync: onclickBuyExtraPoints, loading: isLoadingBuyExtraPoints } = useRequest2(
async ({ points }: { points: number }) => {
try {
points = Math.ceil(points);
points = Math.ceil(points);
const month = 1;
const payAmount = points * month * extraPointsPrice;
const month = 1;
const payAmount = points * month * extraPointsPrice;
if (payAmount === 0) {
return toast({
status: 'warning',
title: t('common:support.wallet.amount_0')
});
}
setLoading(true);
const res = await getWxPayQRCode({
type: BillTypeEnum.extraPoints,
extraPoints: points
});
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
});
} catch (err) {
toast({
title: getErrText(err),
status: 'error'
if (payAmount === 0) {
return toast({
status: 'warning',
title: t('common:support.wallet.amount_0')
});
}
setLoading(false);
const res = await postCreatePayBill({
type: BillTypeEnum.extraPoints,
extraPoints: points
});
setQRPayData({
tip: t('common:button.extra_points_tip'),
...res
});
},
[extraPointsPrice, toast]
{
manual: true,
refreshDeps: [extraPointsPrice]
}
);
return (
@@ -184,7 +169,7 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => {
mt={6}
w={'100%'}
variant={'primaryGhost'}
isLoading={loading}
isLoading={isLoadingBuyDatasetSize}
onClick={handleSubmitDatasetSize(onclickBuyDatasetSize)}
color={'primary.700'}
>
@@ -264,7 +249,7 @@ const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => {
mt={6}
w={'100%'}
variant={'primaryGhost'}
isLoading={loading}
isLoading={isLoadingBuyExtraPoints}
onClick={handleSubmitExtraPoints(onclickBuyExtraPoints)}
color={'primary.700'}
>

View File

@@ -8,7 +8,7 @@ import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constant
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
import { getWxPayQRCode } from '@/web/support/wallet/bill/api';
import { postCreatePayBill } from '@/web/support/wallet/bill/api';
import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
@@ -53,9 +53,9 @@ const Standard = ({
permissionCustomApiKey: value.permissionCustomApiKey,
permissionCustomCopyright: value.permissionCustomCopyright,
trainingWeight: value.trainingWeight,
permissionReRank: value.permissionReRank,
totalPoints: value.totalPoints * (selectSubMode === SubModeEnum.month ? 1 : 12),
permissionWebsiteSync: value.permissionWebsiteSync
permissionWebsiteSync: value.permissionWebsiteSync,
permissionTeamOperationLog: value.permissionTeamOperationLog
};
})
: [];
@@ -65,13 +65,9 @@ const Standard = ({
const [qrPayData, setQRPayData] = useState<QRPayProps>();
/* Get pay code */
const { runAsync: onPay, loading: isLoading } = useRequest2(getWxPayQRCode, {
const { runAsync: onPay, loading: isLoading } = useRequest2(postCreatePayBill, {
onSuccess(res) {
setQRPayData({
readPrice: res.readPrice,
codeUrl: res.codeUrl,
billId: res.billId
});
setQRPayData(res);
}
});