Fix workflow detail (#3382)

* fix: loop node init

* fix: workflow detail

* fix: point table

* add null check
This commit is contained in:
Archer
2024-12-12 17:14:46 +08:00
committed by GitHub
parent 181b854342
commit ddddd998c8
34 changed files with 292 additions and 237 deletions

View File

@@ -40,6 +40,18 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4815' \
会重置应用定时执行的字段,把 null 去掉,减少索引大小。
----
从任意终端,发起 1 个 HTTP 请求。其中 {{rootkey}} 替换成环境变量里的 `rootkey`{{host}} 替换成**FastGPT 域名**。
```bash
curl --location --request POST 'https://{{host}}/api/admin/refreshFreeUser' \
--header 'rootkey: {{rootkey}}' \
--header 'Content-Type: application/json'
```
重新计算一次免费版用户的时长,之前有版本升级时没有重新计算时间,导致会误发通知。
## 完整更新内容

View File

@@ -12,4 +12,9 @@ weight: 808
1.
2. 新增 - 商业版支持 API 知识库和链接集合定时同步。
3. 修复 - 站点同步知识库,链接同步时未使用选择器
3. 优化 - 工作流/简易模式变量初始化代码,去除监听初始化,避免因渲染顺序不一致导致的失败
4. 修复 - 无法自动切换默认语言。增加分享链接,强制执行一次切换默认语言。
5. 修复 - 数组选择器自动兼容 4.8.13 以前的数据。
6. 修复 - 站点同步知识库,链接同步时未使用选择器。
7. 修复 - 简易模式转工作流,没有把系统配置项转化。
8. 修复 - 插件独立运行,变量初始值未赋上。

View File

@@ -29,7 +29,7 @@ export const simpleText = (text = '') => {
replace {{variable}} to value
*/
export function replaceVariable(text: any, obj: Record<string, string | number>) {
if (!(typeof text === 'string')) return text;
if (typeof text !== 'string') return text;
for (const key in obj) {
const val = obj[key];

View File

@@ -251,6 +251,7 @@ export const getReferenceVariableValue = ({
return variables[outputId];
}
// 避免 value 刚好就是二个元素的字符串数组
const node = nodes.find((node) => node.nodeId === sourceNodeId);
if (!node) {
return value;

View File

@@ -193,6 +193,18 @@ export const MultipleRowArraySelect = ({
ref: ref,
handler: onClose
});
const onChange = useCallback(
(val: any[][]) => {
// Filter invalid value
const validList = val.filter((item) => {
const listItem = list.find((v) => v.value === item[0]);
if (!listItem) return false;
return listItem.children?.some((v) => v.value === item[1]);
});
onSelect(validList);
},
[onSelect]
);
const RenderList = useCallback(
({ index, list }: { index: number; list: MultipleSelectProps['list'] }) => {
@@ -213,9 +225,9 @@ export const MultipleRowArraySelect = ({
const newValue = [parentValue, item.value];
if (newValues.some((v) => v[0] === parentValue && v[1] === item.value)) {
onSelect(newValues.filter((v) => !(v[0] === parentValue && v[1] === item.value)));
onChange(newValues.filter((v) => !(v[0] === parentValue && v[1] === item.value)));
} else {
onSelect([...newValues, newValue]);
onChange([...newValues, newValue]);
}
}
};

View File

@@ -43,14 +43,16 @@ export const useI18nLng = () => {
setLang(lang);
await i18n?.changeLanguage?.(lang);
if (prevLang && prevLang !== lang) {
if (!i18n.hasResourceBundle(lang, 'common') && prevLang !== lang) {
window?.location?.reload?.();
}
};
const setUserDefaultLng = () => {
const setUserDefaultLng = (forceGetDefaultLng: boolean = false) => {
if (!navigator || !localStorage) return;
if (getLang()) return onChangeLng(getLang() as string);
if (getLang() && !forceGetDefaultLng) return onChangeLng(getLang() as string);
const lang = languageMap[navigator.language] || 'en';

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { Controller, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Textarea } from '@chakra-ui/react';
@@ -121,21 +121,7 @@ const VariableInput = ({
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList);
const { setValue, handleSubmit: handleSubmitChat } = variablesForm;
const defaultValues = useMemo(() => {
return variableList.reduce((acc: Record<string, any>, item) => {
acc[item.key] = item.defaultValue;
return acc;
}, {});
}, [variableList]);
useEffect(() => {
const values = variablesForm.getValues('variables');
// If form is not empty, do not reset the variables
if (Object.values(values).filter(Boolean).length > 0) return;
setValue('variables', defaultValues);
}, [defaultValues, setValue, variablesForm]);
const { handleSubmit: handleSubmitChat } = variablesForm;
return (
<Box py={3}>

View File

@@ -805,7 +805,7 @@ const ChatBox = ({
setQuestionGuide([]);
setValue('chatStarted', false);
abortRequest('leave');
}, [abortRequest, setValue]);
}, [chatId, appId, abortRequest, setValue]);
// Add listener
useEffect(() => {

View File

@@ -110,17 +110,15 @@ const RenderInput = () => {
return;
}
const defaultFormValues = formatPluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
reset({
files: [],
variables: defaultFormValues
variables: formatPluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
)
});
return;
}
@@ -164,7 +162,7 @@ const RenderInput = () => {
files: historyFileList
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [histories]);
}, [histories, formatPluginInputs]);
const [uploading, setUploading] = useState(false);

View File

@@ -172,7 +172,6 @@ const RenderPluginInput = ({
return (
<Textarea
value={value}
defaultValue={input.defaultValue}
onChange={onChange}
isDisabled={isDisabled}
placeholder={t(input.placeholder as any)}
@@ -192,7 +191,6 @@ const RenderPluginInput = ({
isInvalid={isInvalid}
value={value}
onChange={onChange}
defaultValue={input.defaultValue}
/>
);
}
@@ -203,7 +201,6 @@ const RenderPluginInput = ({
onChange={onChange}
isDisabled={isDisabled}
isInvalid={isInvalid}
defaultChecked={!!input.defaultValue}
/>
);
}
@@ -216,7 +213,6 @@ const RenderPluginInput = ({
value={value}
onChange={onChange}
isInvalid={isInvalid}
defaultValue={input.defaultValue}
/>
);
})();

View File

@@ -2,12 +2,15 @@ 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 { Box, Flex, Grid, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { getAiPointUsageCardRoute } from '@/web/support/wallet/sub/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import dynamic from 'next/dynamic';
const AiPointsModal = dynamic(() =>
import('@/pages/price/components/Points').then((mod) => mod.AiPointsModal)
);
const StandardPlanContentList = ({
level,
@@ -18,7 +21,12 @@ const StandardPlanContentList = ({
}) => {
const { t } = useTranslation();
const { subPlans } = useSystemStore();
const router = useRouter();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const planContent = useMemo(() => {
const plan = subPlans?.standard?.[level];
@@ -95,9 +103,7 @@ const StandardPlanContentList = ({
<QuestionTip
ml={1}
label={t('common:support.wallet.subscription.AI points click to read tip')}
onClick={() => {
router.push(getAiPointUsageCardRoute());
}}
onClick={onOpenAiPointsModal}
></QuestionTip>
</Flex>
</Flex>
@@ -121,6 +127,7 @@ const StandardPlanContentList = ({
<Box color={'myGray.600'}>{t('common:support.wallet.subscription.web_site_sync')}</Box>
</Flex>
)}
{isOpenAiPointsModal && <AiPointsModal onClose={onCloseAiPointsModal} />}
</Grid>
) : null;
};

View File

@@ -42,7 +42,7 @@ export type InitChatResponse = {
appId: string;
userAvatar?: string;
title?: string;
variables: Record<string, any>;
variables?: Record<string, any>;
app: {
chatConfig?: AppChatConfigType;
chatModels?: string[];

View File

@@ -38,6 +38,7 @@ async function handler(
type: AppTypeEnum.workflow,
modules: app.modules,
edges: app.edges,
chatConfig: app.chatConfig,
teamId: app.teamId,
tmbId
});

View File

@@ -1,4 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
@@ -10,7 +10,6 @@ import { PostPublishAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getScheduleTriggerApp } from '@/service/core/app/utils';
async function handler(
req: ApiRequestProps<PostPublishAppProps>,

View File

@@ -52,7 +52,7 @@ async function handler(
appId,
title: chat?.title,
userAvatar: undefined,
variables: chat?.variables || {},
variables: chat?.variables,
app: {
chatConfig: getAppChatConfig({
chatConfig,

View File

@@ -43,7 +43,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
title: chat?.title,
//@ts-ignore
userAvatar: tmb?.userId?.avatar,
variables: chat?.variables || {},
variables: chat?.variables,
app: {
chatConfig: getAppChatConfig({
chatConfig,

View File

@@ -50,7 +50,7 @@ async function handler(req: ApiRequestProps<InitTeamChatProps>, res: NextApiResp
appId,
title: chat?.title,
userAvatar: team?.avatar,
variables: chat?.variables || {},
variables: chat?.variables,
app: {
chatConfig: getAppChatConfig({
chatConfig,

View File

@@ -48,7 +48,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
setChatBoxData(res);
resetVariables({
variables: res.variables
variables: res.variables,
variableList: res.app?.chatConfig?.variables
});
return res;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import {
Flex,
Box,
@@ -31,7 +31,8 @@ import { cardStyles } from '../constants';
import dynamic from 'next/dynamic';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useUserStore } from '@/web/support/user/useUserStore';
import Tag from '@fastgpt/web/components/common/Tag';
import { useMount } from 'ahooks';
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
const Logs = () => {
@@ -41,9 +42,9 @@ const Logs = () => {
const appId = useContextSelector(AppContext, (v) => v.appId);
const { teamMembers, loadAndGetTeamMembers } = useUserStore();
useEffect(() => {
useMount(() => {
loadAndGetTeamMembers();
}, []);
});
const [dateRange, setDateRange] = useState<DateRangeType>({
from: addDays(new Date(), -7),
@@ -140,11 +141,11 @@ const Logs = () => {
) : (
<HStack>
<Avatar
src={teamMembers.find((v) => v.tmbId === item.tmbId)?.avatar}
src={teamMembers?.find((v) => v.tmbId === item.tmbId)?.avatar}
w="1.25rem"
/>
<Box fontSize={'sm'} ml={1}>
{teamMembers.find((v) => v.tmbId === item.tmbId)?.memberName}
{teamMembers?.find((v) => v.tmbId === item.tmbId)?.memberName}
</Box>
</HStack>
)}

View File

@@ -10,8 +10,6 @@ import {
NodePositionChange,
XYPosition,
useReactFlow,
getNodesBounds,
Rect,
NodeRemoveChange,
NodeSelectionChange,
EdgeRemoveChange
@@ -26,15 +24,12 @@ import { WorkflowContext } from '../../context';
import { THelperLine } from '@fastgpt/global/core/workflow/type';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useDebounceEffect, useMemoizedFn } from 'ahooks';
import {
Input_Template_Node_Height,
Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../../context/workflowInitContext';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import { AppContext } from '../../../context';
import { WorkflowEventContext } from '../../context/workflowEventContext';
import { WorkflowStatusContext } from '../../context/workflowStatusContext';
/*
Compute helper lines for snapping nodes to each other
@@ -282,18 +277,22 @@ export const useWorkflow = () => {
const edges = useContextSelector(WorkflowNodeEdgeContext, (state) => state.edges);
const setEdges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.setEdges);
const onEdgesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onEdgesChange);
const { setConnectingEdge, nodeList, onChangeNode, pushPastSnapshot } = useContextSelector(
WorkflowContext,
(v) => v
);
const setConnectingEdge = useContextSelector(WorkflowContext, (v) => v.setConnectingEdge);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const pushPastSnapshot = useContextSelector(WorkflowContext, (v) => v.pushPastSnapshot);
const setHoverEdgeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverEdgeId);
const setMenu = useContextSelector(WorkflowEventContext, (v) => v.setMenu);
const resetParentNodeSizeAndPosition = useContextSelector(
WorkflowStatusContext,
(v) => v.resetParentNodeSizeAndPosition
);
const { getIntersectingNodes } = useReactFlow();
const { isDowningCtrl } = useKeyboard();
const { resetParentNodeSizeAndPosition } = useLoopNode();
/* helper line */
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine>();
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine>();
@@ -669,73 +668,6 @@ export const useWorkflow = () => {
};
};
export const useLoopNode = () => {
const nodes = useContextSelector(WorkflowInitContext, (state) => state.nodes);
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
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;
// Update parentNode size and position
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeWidth,
value: {
...Input_Template_Node_Width,
value: width
}
});
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeHeight,
value: {
...Input_Template_Node_Height,
value: height
}
});
// Update parentNode position
onNodesChange([
{
id: parentId,
type: 'position',
position: {
x: rect.x - 70,
y: rect.y - offsetHeight - 240
}
}
]);
});
return {
resetParentNodeSizeAndPosition
};
};
export default function Dom() {
return <></>;
}

View File

@@ -3,7 +3,6 @@
When the childNodes of loopFlow change, it automatically calculates the rectangular width, height, and position of the childNodes,
thereby further updating the width and height properties of the loop node.
*/
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import React, { useEffect, useMemo, useRef } from 'react';
import { Background, NodeProps } from 'reactflow';
@@ -31,8 +30,8 @@ import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { AppContext } from '../../../../context';
import { isValidArrayReferenceValue } from '@fastgpt/global/core/workflow/utils';
import { ReferenceArrayValueType } from '@fastgpt/global/core/workflow/type/io';
import { useLoopNode } from '../../hooks/useWorkflow';
import { useSize } from 'ahooks';
import { WorkflowStatusContext } from '../../../context/workflowStatusContext';
const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
@@ -40,8 +39,10 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const { resetParentNodeSizeAndPosition } = useLoopNode();
const resetParentNodeSizeAndPosition = useContextSelector(
WorkflowStatusContext,
(v) => v.resetParentNodeSizeAndPosition
);
const {
nodeWidth,
@@ -50,11 +51,11 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
loopNodeInputHeight = Input_Template_LOOP_NODE_OFFSET
} = useMemo(() => {
return {
nodeWidth: Number(
inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value?.toFixed(0)
nodeWidth: Math.round(
Number(inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value) || 500
),
nodeHeight: Number(
inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value?.toFixed(0)
nodeHeight: Math.round(
Number(inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value) || 500
),
loopInputArray: inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray),
loopNodeInputHeight: inputs.find(
@@ -113,7 +114,7 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return JSON.stringify(
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
);
}, [nodeId, nodeList]);
}, [nodeId, nodeList.length]);
useEffect(() => {
onChangeNode({
nodeId,
@@ -148,39 +149,54 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}, 50);
}, [size?.height]);
const Render = useMemo(() => {
const RenderInputDom = useMemo(() => {
return (
<NodeCard selected={selected} maxW="full" menuForbid={{ copy: true }} {...data}>
<Container position={'relative'} flex={1}>
<IOTitle text={t('common:common.Input')} />
<Box mb={6} maxW={'500px'} ref={inputBoxRef}>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Box>
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
{t('workflow:loop_body')}
</FormLabel>
<Box
flex={1}
position={'relative'}
border={'base'}
bg={'myGray.100'}
rounded={'8px'}
{...(!isFolded && {
minW: nodeWidth,
minH: nodeHeight
})}
>
<Background />
</Box>
</Container>
<Container>
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
</NodeCard>
<Box mb={6} maxW={'500px'} ref={inputBoxRef}>
<RenderInput nodeId={nodeId} flowInputList={inputs} />
</Box>
);
}, [selected, isFolded, nodeWidth, nodeHeight, data, t, nodeId, inputs, outputs]);
}, [inputs, nodeId]);
const RenderChildrenNodes = useMemo(() => {
return (
<>
<FormLabel required fontWeight={'medium'} mb={3} color={'myGray.600'}>
{t('workflow:loop_body')}
</FormLabel>
<Box
flex={1}
position={'relative'}
border={'base'}
bg={'myGray.100'}
rounded={'8px'}
{...(!isFolded && {
minW: nodeWidth,
minH: nodeHeight
})}
>
<Background />
</Box>
</>
);
}, [isFolded, nodeHeight, nodeWidth, t]);
return Render;
const MemoRenderOutput = useMemo(() => {
return (
<Container>
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
);
}, [nodeId, outputs]);
return (
<NodeCard selected={selected} maxW="full" menuForbid={{ copy: true }} {...data}>
<Container position={'relative'} flex={1}>
<IOTitle text={t('common:common.Input')} />
{RenderInputDom}
{RenderChildrenNodes}
</Container>
{MemoRenderOutput}
</NodeCard>
);
};
export default React.memo(NodeLoop);

View File

@@ -23,13 +23,15 @@ const typeMap = {
[WorkflowIOValueTypeEnum.arrayString]: WorkflowIOValueTypeEnum.string,
[WorkflowIOValueTypeEnum.arrayNumber]: WorkflowIOValueTypeEnum.number,
[WorkflowIOValueTypeEnum.arrayBoolean]: WorkflowIOValueTypeEnum.boolean,
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object
[WorkflowIOValueTypeEnum.arrayObject]: WorkflowIOValueTypeEnum.object,
[WorkflowIOValueTypeEnum.arrayAny]: WorkflowIOValueTypeEnum.any
};
const NodeLoopStart = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { nodeId, outputs } = data;
const { nodeList, onChangeNode } = useContextSelector(WorkflowContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const loopStartNode = useMemo(
() => nodeList.find((node) => node.nodeId === nodeId),

View File

@@ -88,6 +88,13 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
const filterInputs = useMemo(() => {
return flowInputList.filter((input) => {
if (input.isPro && !feConfigs?.isPlus) return false;
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
if (renderType === FlowNodeInputTypeEnum.hidden) return false;
const isDynamic = !!input.canEdit;
if (isDynamic) return false;
return true;
});
}, [feConfigs?.isPlus, flowInputList]);
@@ -96,7 +103,6 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
<>
{filterInputs.map((input) => {
const renderType = input.renderTypeList?.[input.selectedTypeIndex || 0];
const isDynamic = !!input.canEdit;
const RenderComponent = (() => {
if (renderType === FlowNodeInputTypeEnum.custom && CustomComponent?.[input.key]) {
@@ -109,7 +115,7 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
return <Component inputs={filterInputs} item={input} nodeId={nodeId} />;
})();
return renderType !== FlowNodeInputTypeEnum.hidden && !isDynamic ? (
return (
<Box key={input.key} _notLast={{ mb }} position={'relative'}>
{!!input.label && !hideLabelTypeList.includes(renderType) && (
<InputLabel nodeId={nodeId} input={input} />
@@ -120,7 +126,7 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
</Box>
)}
</Box>
) : null;
);
})}
</>
);

View File

@@ -115,7 +115,10 @@ export const useReference = ({
const Reference = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const { onChangeNode, nodeList } = useContextSelector(WorkflowContext, (v) => v);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const isArray = item.valueType?.includes('array') ?? false;
const onSelect = useCallback(
@@ -254,22 +257,25 @@ const MultipleReferenceSelector = ({
});
}, [getSelectValue, value]);
// useEffect(() => {
// const validList = formatList.filter((item) => item.nodeName && item.outputName);
// if (validList.length !== value?.length) {
// onSelect(validList.map((item) => item.rawValue));
// }
// }, [formatList, onSelect, value]);
useEffect(() => {
// Adapt array type from old version
if (Array.isArray(value) && typeof value[0] === 'string') {
// @ts-ignore
onSelect([value]);
}
}, [formatList, onSelect, value]);
const invalidList = useMemo(() => {
return formatList.filter((item) => item.nodeName && item.outputName);
}, [formatList]);
const ArraySelector = useMemo(() => {
return (
<MultipleRowArraySelect
label={
formatList.length > 0 ? (
invalidList.length > 0 ? (
<Grid py={3} gridTemplateColumns={'1fr 1fr'} gap={2} fontSize={'sm'}>
{formatList.map(({ nodeName, outputName }, index) => {
if (!nodeName || !outputName) return null;
{invalidList.map(({ nodeName, outputName }, index) => {
return (
<Flex
alignItems={'center'}
@@ -325,7 +331,7 @@ const MultipleReferenceSelector = ({
popDirection={popDirection}
/>
);
}, [formatList, list, onSelect, placeholder, popDirection, value]);
}, [invalidList, list, onSelect, placeholder, popDirection, value]);
return ArraySelector;
};

View File

@@ -1,7 +1,7 @@
import { createContext } from 'use-context-selector';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { useMemoizedFn } from 'ahooks';
import { useCreation, useMemoizedFn } from 'ahooks';
import React, { Dispatch, SetStateAction, ReactNode, useEffect, useMemo } from 'react';
import { Edge, EdgeChange, Node, NodeChange, useEdgesState, useNodesState } from 'reactflow';
@@ -50,7 +50,7 @@ const WorkflowInitContextProvider = ({ children }: { children: ReactNode }) => {
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowNodeItemType>([]);
const getNodes = useMemoizedFn(() => nodes);
const nodeListString = JSON.stringify(nodes.map((node) => node.data));
const nodeList = useMemo(
const nodeList = useCreation(
() => JSON.parse(nodeListString) as FlowNodeItemType[],
[nodeListString]
);
@@ -73,7 +73,7 @@ const WorkflowInitContextProvider = ({ children }: { children: ReactNode }) => {
: item
)
);
}, [edges.length]);
}, [nodeList, edges.length]);
const actionContextValue = useMemo(
() => ({

View File

@@ -1,4 +1,4 @@
import { useDebounceEffect } from 'ahooks';
import { useDebounceEffect, useMemoizedFn } from 'ahooks';
import React, { ReactNode, useMemo, useRef, useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import { WorkflowInitContext, WorkflowNodeEdgeContext } from './workflowInitContext';
@@ -7,10 +7,18 @@ 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 { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
Input_Template_Node_Height,
Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input';
type WorkflowStatusContextType = {
isSaved: boolean;
leaveSaveSign: React.MutableRefObject<boolean>;
resetParentNodeSizeAndPosition: (parentId: string) => void;
};
export const WorkflowStatusContext = createContext<WorkflowStatusContextType>({
@@ -75,12 +83,72 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
}
});
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
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;
// Update parentNode size and position
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeWidth,
value: {
...Input_Template_Node_Width,
value: width
}
});
onChangeNode({
nodeId: parentId,
type: 'updateInput',
key: NodeInputKeyEnum.nodeHeight,
value: {
...Input_Template_Node_Height,
value: height
}
});
// Update parentNode position
onNodesChange([
{
id: parentId,
type: 'position',
position: {
x: Math.round(rect.x - 70),
y: Math.round(rect.y - offsetHeight - 240)
}
}
]);
});
const contextValue = useMemo(() => {
return {
isSaved,
leaveSaveSign
leaveSaveSign,
resetParentNodeSizeAndPosition
};
}, [isSaved]);
}, [isSaved, resetParentNodeSizeAndPosition]);
return (
<WorkflowStatusContext.Provider value={contextValue}>{children}</WorkflowStatusContext.Provider>
);

View File

@@ -107,8 +107,10 @@ export const useChatTest = ({
async () => {
if (!appId || !chatId) return;
const res = await getInitChatInfo({ appId, chatId });
resetVariables({
variables: res.variables
variables: res.variables,
variableList: res.app?.chatConfig?.variables
});
},
{

View File

@@ -74,7 +74,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
// reset chat variables
resetVariables({
variables: res.variables
variables: res.variables,
variableList: res.app?.chatConfig?.variables
});
},
{

View File

@@ -22,8 +22,7 @@ import { connectToDatabase } from '@/service/mongo';
import NextHead from '@/components/common/NextHead';
import { useContextSelector } from 'use-context-selector';
import ChatContextProvider, { ChatContext } from '@/web/core/chat/context/chatContext';
import { InitChatResponse } from '@/global/core/chat/api';
import { defaultChatData, GetChatTypeEnum } from '@/global/core/chat/constants';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { useMount } from 'ahooks';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -37,6 +36,8 @@ import ChatRecordContextProvider, {
} from '@/web/core/chat/context/chatRecordContext';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
const CustomPluginRunBox = dynamic(() => import('./components/CustomPluginRunBox'));
type Props = {
@@ -102,7 +103,8 @@ const OutLink = (props: Props) => {
setChatBoxData(res);
resetVariables({
variables: res.variables
variables: res.variables,
variableList: res.app?.chatConfig?.variables
});
return res;
@@ -299,8 +301,9 @@ const OutLink = (props: Props) => {
const Render = (props: Props) => {
const { shareId, authToken, customUid, appId } = props;
const { localUId, loaded } = useShareChatStore();
const { localUId } = useShareChatStore();
const { source, chatId, setSource, setAppId, setOutLinkAuthData } = useChatStore();
const { setUserDefaultLng } = useI18nLng();
const chatHistoryProviderParams = useMemo(() => {
return { shareId, outLinkUid: authToken || customUid || localUId };
@@ -317,6 +320,7 @@ const Render = (props: Props) => {
useMount(() => {
setSource('share');
setUserDefaultLng(true);
});
// Set outLinkAuthData

View File

@@ -79,7 +79,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
// reset chat records
resetVariables({
variables: res.variables
variables: res.variables,
variableList: res.app?.chatConfig?.variables
});
},
{

View File

@@ -23,5 +23,7 @@ export const langMap = {
export const serviceSideProps = (content: any, ns: I18nNsType = []) => {
const lang = content.req?.cookies?.NEXT_LOCALE || content.locale;
return serverSideTranslations(lang, ['common', ...ns], null);
const extraLng = content.req?.cookies?.NEXT_LOCALE ? undefined : content.locales;
return serverSideTranslations(lang, ['common', ...ns], null, extraLng);
};

View File

@@ -6,7 +6,7 @@ import { ComponentRef as ChatComponentRef } from '@/components/core/chat/ChatCon
import { useForm, UseFormReturn } from 'react-hook-form';
import { defaultChatData } from '@/global/core/chat/constants';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import { AppChatConfigType, VariableItemType } from '@fastgpt/global/core/app/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
type ChatBoxDataType = {
@@ -29,7 +29,10 @@ type ChatItemContextType = {
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
pluginRunTab: PluginRunBoxTabEnum;
setPluginRunTab: React.Dispatch<React.SetStateAction<PluginRunBoxTabEnum>>;
resetVariables: (props?: { variables?: Record<string, any> }) => void;
resetVariables: (props?: {
variables?: Record<string, any>;
variableList?: VariableItemType[];
}) => void;
clearChatRecords: () => void;
chatBoxData: ChatBoxDataType;
setChatBoxData: React.Dispatch<React.SetStateAction<ChatBoxDataType>>;
@@ -44,7 +47,10 @@ export const ChatItemContext = createContext<ChatItemContextType>({
setPluginRunTab: function (value: React.SetStateAction<PluginRunBoxTabEnum>): void {
throw new Error('Function not implemented.');
},
resetVariables: function (props?: { variables?: Record<string, any> }): void {
resetVariables: function (props?: {
variables?: Record<string, any>;
variableList?: VariableItemType[];
}): void {
throw new Error('Function not implemented.');
},
clearChatRecords: function (): void {
@@ -69,27 +75,21 @@ const ChatItemContextProvider = ({ children }: { children: ReactNode }) => {
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);
const resetVariables = useCallback(
(props?: { variables?: Record<string, any> }) => {
const { variables = {} } = props || {};
(props?: { variables?: Record<string, any>; variableList?: VariableItemType[] }) => {
const { variables, variableList = [] } = props || {};
// Reset to empty input
const data = variablesForm.getValues();
// Reset the old variables to empty
const resetVariables: Record<string, any> = {};
for (const key in data.variables) {
resetVariables[key] = (() => {
if (Array.isArray(data.variables[key])) {
return [];
}
return '';
})();
let newVariableValue: Record<string, any> = {};
if (variables) {
variableList.forEach((item) => {
newVariableValue[item.key] = variables[item.key];
});
} else {
variableList.forEach((item) => {
newVariableValue[item.key] = item.defaultValue;
});
}
variablesForm.setValue('variables', {
...resetVariables,
...variables
});
variablesForm.setValue('variables', newVariableValue);
},
[variablesForm]
);

View File

@@ -119,6 +119,7 @@ export const storeNode2FlowNode = ({
selectedTypeIndex: storeInput.selectedTypeIndex ?? templateInput.selectedTypeIndex,
value: storeInput.value ?? templateInput.value,
valueType: storeInput.valueType ?? templateInput.valueType,
label: storeInput.label ?? templateInput.label
};
})
@@ -148,7 +149,8 @@ export const storeNode2FlowNode = ({
id: storeOutput.id ?? templateOutput.id,
label: storeOutput.label ?? templateOutput.label,
value: storeOutput.value ?? templateOutput.value
value: storeOutput.value ?? templateOutput.value,
valueType: storeOutput.valueType ?? templateOutput.valueType
};
})
.concat(

View File

@@ -1,14 +1,6 @@
import { getDocPath } from '@/web/common/system/doc';
import { useSystemStore } from '@/web/common/system/useSystemStore';
export const AI_POINT_USAGE_CARD_ROUTE = '/price#point-card';
export const getAiPointUsageCardRoute = () => {
const subPlans = useSystemStore.getState().subPlans;
return subPlans?.planDescriptionUrl
? getDocPath(subPlans.planDescriptionUrl)
: AI_POINT_USAGE_CARD_ROUTE;
};
export const EXTRA_PLAN_CARD_ROUTE = '/price#extra-plan';
export const getExtraPlanCardRoute = () => {
const subPlans = useSystemStore.getState().subPlans;