4.8.11 code perf (#2804)

* perf: support child layout

* perf: user form ui

* perf: plugin tool output code

* perf: code

* perf: node fold hook
This commit is contained in:
Archer
2024-09-26 16:27:28 +08:00
committed by GitHub
parent e6bad93b32
commit 54e0a0eab1
23 changed files with 153 additions and 114 deletions

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: 飞书话题群 - name: 飞书话题群
url: https://oss.laf.run/otnvvf-imgs/1719505774252.jpg url: https://oss.laf.run/otnvvf-imgs/feishu3.png
about: FastGPT 全是问题群 about: FastGPT 全是问题群

View File

@@ -92,15 +92,16 @@ weight: 813
6. 新增 - 支持 Openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature``max_tokens``stream`配置o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看。 6. 新增 - 支持 Openai o1 模型,需增加模型的 `defaultConfig` 配置,覆盖 `temperature``max_tokens``stream`配置o1 不支持 stream 模式, 详细可重新拉取 `config.json` 配置文件查看。
7. 新增 - AI 对话节点知识库引用,支持配置 role=system 和 role=user已配置的过自定义提示词的节点将会保持 user 模式,其余用户将转成 system 模式。 7. 新增 - AI 对话节点知识库引用,支持配置 role=system 和 role=user已配置的过自定义提示词的节点将会保持 user 模式,其余用户将转成 system 模式。
8. 新增 - 插件支持上传系统文件。 8. 新增 - 插件支持上传系统文件。
9. 新增 - 支持工作流嵌套子应用时,可以设置`非流模式`,同时简易模式也可以选择工作流作为插件了,简易模式调用子应用时,都将强制使用非流模式 9. 新增 - 插件输出,支持指定字段作为工具响应
10. 新增 - 调试模式下,子应用调用,支持返回详细运行数据 10. 新增 - 支持工作流嵌套子应用时,可以设置`非流模式`,同时简易模式也可以选择工作流作为插件了,简易模式调用子应用时,都将强制使用非流模式
11. 新增 - 保留所有模式下子应用嵌套调用的日志 11. 新增 - 调试模式下子应用调用,支持返回详细运行数据
12. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环 12. 新增 - 保留所有模式下子应用嵌套调用的日志
13. 优化 - 工作流 handler 性能优化 13. 优化 - 工作流嵌套层级限制 20 层,避免因编排不合理导致的无限死循环
14. 优化 - 工作流快捷键,避免调试测试时也会触发 14. 优化 - 工作流 handler 性能优化
15. 优化 - 流输出,切换 tab 时仍可以继续输出 15. 优化 - 工作流快捷键,避免调试测试时也会触发
16. 优化 - 完善外部文件知识库相关 API 16. 优化 - 流输出,切换 tab 时仍可以继续输出。
17. 修复 - 知识库选择权限问题。 17. 优化 - 完善外部文件知识库相关 API
18. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常 18. 修复 - 知识库选择权限问题
19. 修复 - createDataset 接口intro 为赋值 19. 修复 - 空 chatId 发起对话,首轮携带用户选择时会异常
20. 修复 - 对话框渲染性能问题 20. 修复 - createDataset 接口intro 为赋值
21. 修复 - 对话框渲染性能问题。

View File

@@ -46,8 +46,8 @@ export const FormInputNode: FlowNodeTemplateType = {
id: NodeOutputKeyEnum.formInputResult, id: NodeOutputKeyEnum.formInputResult,
key: NodeOutputKeyEnum.formInputResult, key: NodeOutputKeyEnum.formInputResult,
required: true, required: true,
label: i18nT('app:workflow.form_input_result'), label: i18nT('workflow:form_input_result'),
description: i18nT('app:workflow.form_input_result_tip'), description: i18nT('workflow:form_input_result_tip'),
valueType: WorkflowIOValueTypeEnum.object, valueType: WorkflowIOValueTypeEnum.object,
type: FlowNodeOutputTypeEnum.static type: FlowNodeOutputTypeEnum.static
} }

View File

@@ -9,6 +9,7 @@ import {
UserInputFormItemType, UserInputFormItemType,
UserInputInteractive UserInputInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type'; } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { addLog } from '../../../../common/system/log';
type Props = ModuleDispatchProps<{ type Props = ModuleDispatchProps<{
[NodeInputKeyEnum.description]: string; [NodeInputKeyEnum.description]: string;
@@ -48,7 +49,8 @@ export const dispatchFormInput = async (props: Props): Promise<FormInputResponse
try { try {
return JSON.parse(text); return JSON.parse(text);
} catch (error) { } catch (error) {
return text; addLog.warn('formInput error', { error });
return {};
} }
})(); })();

View File

@@ -47,15 +47,13 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
const plugin = await getChildAppRuntimeById(pluginId); const plugin = await getChildAppRuntimeById(pluginId);
const outputFilterMap = plugin.nodes const outputFilterMap =
.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginOutput)! plugin.nodes
.inputs.reduce( .find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginOutput)
(acc, cur) => { ?.inputs.reduce<Record<string, boolean>>((acc, cur) => {
acc[cur.key] = cur.isToolOutput === false ? false : true; acc[cur.key] = cur.isToolOutput === false ? false : true;
return acc; return acc;
}, }, {}) ?? {};
{} as Record<string, boolean>
);
const runtimeNodes = storeNodes2RuntimeNodes( const runtimeNodes = storeNodes2RuntimeNodes(
plugin.nodes, plugin.nodes,
@@ -125,13 +123,12 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
moduleLogo: plugin.avatar, moduleLogo: plugin.avatar,
totalPoints: usagePoints, totalPoints: usagePoints,
pluginOutput: output?.pluginOutput, pluginOutput: output?.pluginOutput,
pluginDetail: pluginDetail: pluginData?.permission?.hasWritePer // Not system plugin
pluginData && pluginData.permission.hasWritePer // Not system plugin ? flowResponses.filter((item) => {
? flowResponses.filter((item) => { const filterArr = [FlowNodeTypeEnum.pluginOutput];
const filterArr = [FlowNodeTypeEnum.pluginOutput]; return !filterArr.includes(item.moduleType as any);
return !filterArr.includes(item.moduleType as any); })
}) : undefined
: undefined
}, },
[DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [
{ {
@@ -143,14 +140,11 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
[DispatchNodeResponseKeyEnum.toolResponses]: output?.pluginOutput [DispatchNodeResponseKeyEnum.toolResponses]: output?.pluginOutput
? Object.keys(output.pluginOutput) ? Object.keys(output.pluginOutput)
.filter((key) => outputFilterMap[key]) .filter((key) => outputFilterMap[key])
.reduce( .reduce<Record<string, any>>((acc, key) => {
(acc, key) => { acc[key] = output.pluginOutput![key];
acc[key] = output.pluginOutput![key]; return acc;
return acc; }, {})
}, : null,
{} as Record<string, any>
)
: {},
...(output ? output.pluginOutput : {}) ...(output ? output.pluginOutput : {})
}; };
}; };

View File

@@ -145,8 +145,6 @@
"workflow.file_url": "Document Link", "workflow.file_url": "Document Link",
"workflow.form_input": "Form input", "workflow.form_input": "Form input",
"workflow.form_input_description_placeholder": "For example: \nAdd your information", "workflow.form_input_description_placeholder": "For example: \nAdd your information",
"workflow.form_input_result": "Full input result",
"workflow.form_input_result_tip": "An object of full result",
"workflow.form_input_tip": " This module can configure multiple inputs to guide users in entering specific content.", "workflow.form_input_tip": " This module can configure multiple inputs to guide users in entering specific content.",
"workflow.input_description_tip": "You can add a description to explain to users what they need to input", "workflow.input_description_tip": "You can add a description to explain to users what they need to input",
"workflow.read_files": "Document Parsing", "workflow.read_files": "Document Parsing",

View File

@@ -430,7 +430,6 @@
"core.chat.response.Read complete response tips": "Click to View Detailed Process", "core.chat.response.Read complete response tips": "Click to View Detailed Process",
"core.chat.response.Tool call tokens": "Tool Call Tokens Consumption", "core.chat.response.Tool call tokens": "Tool Call Tokens Consumption",
"core.chat.response.context total length": "Total Context Length", "core.chat.response.context total length": "Total Context Length",
"core.chat.response.form_input_result": "Form input result",
"core.chat.response.loop_input": "Loop Input Array", "core.chat.response.loop_input": "Loop Input Array",
"core.chat.response.loop_input_element": "Loop Input Element", "core.chat.response.loop_input_element": "Loop Input Element",
"core.chat.response.loop_output": "Loop Output Array", "core.chat.response.loop_output": "Loop Output Array",

View File

@@ -50,6 +50,8 @@
"field_required": "Required", "field_required": "Required",
"field_used_as_tool_input": "Used as Tool Call Parameter", "field_used_as_tool_input": "Used as Tool Call Parameter",
"filter_description": "Currently supports filtering by tags and creation time. Fill in the format as follows:\n{\n \"tags\": {\n \"$and\": [\"Tag 1\",\"Tag 2\"],\n \"$or\": [\"When there are $and tags, and is effective, or is not effective\"]\n },\n \"createTime\": {\n \"$gte\": \"YYYY-MM-DD HH:mm format, collection creation time greater than this time\",\n \"$lte\": \"YYYY-MM-DD HH:mm format, collection creation time less than this time, can be used with $gte\"\n }\n}", "filter_description": "Currently supports filtering by tags and creation time. Fill in the format as follows:\n{\n \"tags\": {\n \"$and\": [\"Tag 1\",\"Tag 2\"],\n \"$or\": [\"When there are $and tags, and is effective, or is not effective\"]\n },\n \"createTime\": {\n \"$gte\": \"YYYY-MM-DD HH:mm format, collection creation time greater than this time\",\n \"$lte\": \"YYYY-MM-DD HH:mm format, collection creation time less than this time, can be used with $gte\"\n }\n}",
"form_input_result": "User complete input result",
"form_input_result_tip": "an object containing the complete result",
"full_field_extraction": "Full Field Extraction", "full_field_extraction": "Full Field Extraction",
"full_field_extraction_description": "Returns true when all fields are fully extracted (success includes model extraction or using default values)", "full_field_extraction_description": "Returns true when all fields are fully extracted (success includes model extraction or using default values)",
"full_response_data": "Full Response Data", "full_response_data": "Full Response Data",
@@ -79,7 +81,7 @@
"is_equal_to": "Is Equal To", "is_equal_to": "Is Equal To",
"is_not_empty": "Is Not Empty", "is_not_empty": "Is Not Empty",
"is_not_equal": "Is Not Equal", "is_not_equal": "Is Not Equal",
"is_tool_output": "Whether to use this field as tool output when the tool is called", "is_tool_output_label": "as tool response",
"judgment_result": "Judgment Result", "judgment_result": "Judgment Result",
"knowledge_base_reference": "Dataset Reference", "knowledge_base_reference": "Dataset Reference",
"knowledge_base_search_merge": "Dataset Search Merge", "knowledge_base_search_merge": "Dataset Search Merge",
@@ -112,6 +114,7 @@
"plugin.Instruction_Tip": "You can configure an instruction to explain the purpose of the plugin. This instruction will be displayed each time the plugin is used. Supports standard Markdown syntax.", "plugin.Instruction_Tip": "You can configure an instruction to explain the purpose of the plugin. This instruction will be displayed each time the plugin is used. Supports standard Markdown syntax.",
"plugin.Instructions": "Instructions", "plugin.Instructions": "Instructions",
"plugin_input": "Plugin Input", "plugin_input": "Plugin Input",
"plugin_output_tool": "When the plug-in is executed as a tool, whether this field responds as a result of the tool",
"question_classification": "Question Classification", "question_classification": "Question Classification",
"question_optimization": "Question Optimization", "question_optimization": "Question Optimization",
"quote_content_placeholder": "The structure of the reference content can be customized to better suit different scenarios. \nSome variables can be used for template configuration\n\n{{q}} - main content\n\n{{a}} - auxiliary data\n\n{{source}} - source name\n\n{{sourceId}} - source ID\n\n{{index}} - nth reference", "quote_content_placeholder": "The structure of the reference content can be customized to better suit different scenarios. \nSome variables can be used for template configuration\n\n{{q}} - main content\n\n{{a}} - auxiliary data\n\n{{source}} - source name\n\n{{sourceId}} - source ID\n\n{{index}} - nth reference",
@@ -157,6 +160,9 @@
"update_link_error": "Error updating link", "update_link_error": "Error updating link",
"update_specified_node_output_or_global_variable": "Can update the output value of a specified node or update global variables", "update_specified_node_output_or_global_variable": "Can update the output value of a specified node or update global variables",
"use_user_id": "User ID", "use_user_id": "User ID",
"user_form_input_config": "Form configuration",
"user_form_input_description": "describe",
"user_form_input_name": "Name",
"user_question": "User Question", "user_question": "User Question",
"user_question_tool_desc": "User input questions (questions need to be improved)", "user_question_tool_desc": "User input questions (questions need to be improved)",
"value_type": "Value type", "value_type": "Value type",

View File

@@ -145,8 +145,6 @@
"workflow.file_url": "文档链接", "workflow.file_url": "文档链接",
"workflow.form_input": "表单输入", "workflow.form_input": "表单输入",
"workflow.form_input_description_placeholder": "例如:\n补充您的信息", "workflow.form_input_description_placeholder": "例如:\n补充您的信息",
"workflow.form_input_result": "完整输入结果",
"workflow.form_input_result_tip": "一个包含完整结果的对象",
"workflow.form_input_tip": "该模块可以配置多种输入,引导用户输入特定内容。", "workflow.form_input_tip": "该模块可以配置多种输入,引导用户输入特定内容。",
"workflow.input_description_tip": "你可以添加一段说明文字,用以向用户说明需要输入的内容", "workflow.input_description_tip": "你可以添加一段说明文字,用以向用户说明需要输入的内容",
"workflow.read_files": "文档解析", "workflow.read_files": "文档解析",
@@ -156,7 +154,7 @@
"workflow.select_description": "说明文字", "workflow.select_description": "说明文字",
"workflow.select_description_placeholder": "例如: \n冰箱里是否有西红柿", "workflow.select_description_placeholder": "例如: \n冰箱里是否有西红柿",
"workflow.select_description_tip": "你可以添加一段说明文字,用以向用户说明每个选项代表的含义。", "workflow.select_description_tip": "你可以添加一段说明文字,用以向用户说明每个选项代表的含义。",
"workflow.select_result": "选择结果", "workflow.select_result": "选择结果",
"workflow.template.communication": "通信", "workflow.template.communication": "通信",
"workflow.user_file_input": "文件链接", "workflow.user_file_input": "文件链接",
"workflow.user_file_input_desc": "用户上传的文档和图片链接", "workflow.user_file_input_desc": "用户上传的文档和图片链接",

View File

@@ -429,7 +429,6 @@
"core.chat.response.Read complete response tips": "点击查看详细流程", "core.chat.response.Read complete response tips": "点击查看详细流程",
"core.chat.response.Tool call tokens": "工具调用 tokens 消耗", "core.chat.response.Tool call tokens": "工具调用 tokens 消耗",
"core.chat.response.context total length": "上下文总长度", "core.chat.response.context total length": "上下文总长度",
"core.chat.response.form_input_result": "表单输入结果",
"core.chat.response.loop_input": "输入数组", "core.chat.response.loop_input": "输入数组",
"core.chat.response.loop_input_element": "输入数组元素", "core.chat.response.loop_input_element": "输入数组元素",
"core.chat.response.loop_output": "输出数组", "core.chat.response.loop_output": "输出数组",

View File

@@ -50,6 +50,8 @@
"field_required": "必填", "field_required": "必填",
"field_used_as_tool_input": "作为工具调用参数", "field_used_as_tool_input": "作为工具调用参数",
"filter_description": "目前支持标签和创建时间过滤,需按照以下格式填写:\n{\n \"tags\": {\n \"$and\": [\"标签 1\",\"标签 2\"],\n \"$or\": [\"有 $and 标签时and 生效or 不生效\"]\n },\n \"createTime\": {\n \"$gte\": \"YYYY-MM-DD HH:mm 格式即可,集合的创建时间大于该时间\",\n \"$lte\": \"YYYY-MM-DD HH:mm 格式即可,集合的创建时间小于该时间,可和 $gte 共同使用\"\n }\n}", "filter_description": "目前支持标签和创建时间过滤,需按照以下格式填写:\n{\n \"tags\": {\n \"$and\": [\"标签 1\",\"标签 2\"],\n \"$or\": [\"有 $and 标签时and 生效or 不生效\"]\n },\n \"createTime\": {\n \"$gte\": \"YYYY-MM-DD HH:mm 格式即可,集合的创建时间大于该时间\",\n \"$lte\": \"YYYY-MM-DD HH:mm 格式即可,集合的创建时间小于该时间,可和 $gte 共同使用\"\n }\n}",
"form_input_result": "用户完整输入结果",
"form_input_result_tip": "一个包含完整结果的对象",
"full_field_extraction": "字段完全提取", "full_field_extraction": "字段完全提取",
"full_field_extraction_description": "提取字段全部填充时返回 true (模型提取或使用默认值均属于成功)", "full_field_extraction_description": "提取字段全部填充时返回 true (模型提取或使用默认值均属于成功)",
"full_response_data": "完整响应数据", "full_response_data": "完整响应数据",
@@ -79,7 +81,7 @@
"is_equal_to": "等于", "is_equal_to": "等于",
"is_not_empty": "不为空", "is_not_empty": "不为空",
"is_not_equal": "不等于", "is_not_equal": "不等于",
"is_tool_output": "是否在工具调用时,将该字段作为工具输出", "is_tool_output_label": "作为工具响应",
"judgment_result": "判断结果", "judgment_result": "判断结果",
"knowledge_base_reference": "知识库引用", "knowledge_base_reference": "知识库引用",
"knowledge_base_search_merge": "知识库搜索引用合并", "knowledge_base_search_merge": "知识库搜索引用合并",
@@ -112,6 +114,7 @@
"plugin.Instruction_Tip": "可以配置一段说明,以解释该插件的用途。每次使用插件前,会显示该段说明。支持标准 Markdown 语法。", "plugin.Instruction_Tip": "可以配置一段说明,以解释该插件的用途。每次使用插件前,会显示该段说明。支持标准 Markdown 语法。",
"plugin.Instructions": "使用说明", "plugin.Instructions": "使用说明",
"plugin_input": "插件输入", "plugin_input": "插件输入",
"plugin_output_tool": "插件作为工具执行时,该字段是否作为工具响应结果",
"question_classification": "问题分类", "question_classification": "问题分类",
"question_optimization": "问题优化", "question_optimization": "问题优化",
"quote_content_placeholder": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置\n{{q}} - 主要内容\n{{a}} - 辅助数据\n{{source}} - 来源名\n{{sourceId}} - 来源ID\n{{index}} - 第 n 个引用", "quote_content_placeholder": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置\n{{q}} - 主要内容\n{{a}} - 辅助数据\n{{source}} - 来源名\n{{sourceId}} - 来源ID\n{{index}} - 第 n 个引用",
@@ -157,6 +160,9 @@
"update_link_error": "更新链接异常", "update_link_error": "更新链接异常",
"update_specified_node_output_or_global_variable": "可以更新指定节点的输出值或更新全局变量", "update_specified_node_output_or_global_variable": "可以更新指定节点的输出值或更新全局变量",
"use_user_id": "使用者 ID", "use_user_id": "使用者 ID",
"user_form_input_config": "表单配置",
"user_form_input_description": "描述",
"user_form_input_name": "标题",
"user_question": "用户问题", "user_question": "用户问题",
"user_question_tool_desc": "用户输入的问题(问题需要完善)", "user_question_tool_desc": "用户输入的问题(问题需要完善)",
"value_type": "数据类型", "value_type": "数据类型",

View File

@@ -299,18 +299,12 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
); );
if (value.type === ChatItemValueTypeEnum.tool && value.tools) if (value.type === ChatItemValueTypeEnum.tool && value.tools)
return <RenderTool showAnimation={isChatting} tools={value.tools} />; return <RenderTool showAnimation={isChatting} tools={value.tools} />;
if ( if (value.type === ChatItemValueTypeEnum.interactive && value.interactive) {
value.type === ChatItemValueTypeEnum.interactive && if (value.interactive.type === 'userSelect')
value.interactive && return <RenderUserSelectInteractive interactive={value.interactive} />;
value.interactive.type === 'userSelect' if (value.interactive?.type === 'userInput')
) return <RenderUserFormInteractive interactive={value.interactive} />;
return <RenderUserSelectInteractive interactive={value.interactive} />; }
if (
value.type === ChatItemValueTypeEnum.interactive &&
value.interactive &&
value.interactive?.type === 'userInput'
)
return <RenderUserFormInteractive interactive={value.interactive} />;
}; };
export default React.memo(AIResponseBox); export default React.memo(AIResponseBox);

View File

@@ -354,10 +354,7 @@ export const WholeResponseContent = ({
/> />
{/* form input */} {/* form input */}
<Row <Row label={t('workflow:form_input_result')} value={activeModule?.formInputResult} />
label={t('common:core.chat.response.form_input_result')}
value={activeModule?.formInputResult}
/>
</Box> </Box>
) : null; ) : null;
}; };

View File

@@ -11,9 +11,17 @@ import { useInitApp } from '@/web/context/useInitApp';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import '@/web/styles/reset.scss'; import '@/web/styles/reset.scss';
import NextHead from '@/components/common/NextHead'; import NextHead from '@/components/common/NextHead';
import { useEffect } from 'react'; import { ReactElement, useEffect } from 'react';
import { NextPage } from 'next';
function App({ Component, pageProps }: AppProps) { type NextPageWithLayout = NextPage & {
setLayout?: (page: ReactElement) => JSX.Element;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function App({ Component, pageProps }: AppPropsWithLayout) {
const { feConfigs, scripts, title } = useInitApp(); const { feConfigs, scripts, title } = useInitApp();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -30,6 +38,8 @@ function App({ Component, pageProps }: AppProps) {
); );
}, []); }, []);
const setLayout = Component.setLayout || ((page) => <>{page}</>);
return ( return (
<> <>
<NextHead <NextHead
@@ -46,9 +56,7 @@ function App({ Component, pageProps }: AppProps) {
<QueryClientContext> <QueryClientContext>
<I18nContextProvider> <I18nContextProvider>
<ChakraUIContext> <ChakraUIContext>
<Layout> <Layout>{setLayout(<Component {...pageProps} />)}</Layout>
<Component {...pageProps} />
</Layout>
</ChakraUIContext> </ChakraUIContext>
</I18nContextProvider> </I18nContextProvider>
</QueryClientContext> </QueryClientContext>

View File

@@ -1,10 +1,11 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow'; import { BezierEdge, getBezierPath, EdgeLabelRenderer, EdgeProps } from 'reactflow';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeOutputKeyEnum, RuntimeEdgeStatusEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context'; import { WorkflowContext } from '../../context';
import { useThrottleEffect } from 'ahooks';
const ButtonEdge = (props: EdgeProps) => { const ButtonEdge = (props: EdgeProps) => {
const { nodes, nodeList, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector( const { nodes, nodeList, setEdges, workflowDebugData, hoverEdgeId } = useContextSelector(
@@ -28,13 +29,11 @@ const ButtonEdge = (props: EdgeProps) => {
style style
} = props; } = props;
// If parentNode is folded, the edge will not be displayed
const parentNode = useMemo(() => { const parentNode = useMemo(() => {
for (const node of nodeList) { return nodeList.find(
if ((node.nodeId === source || node.nodeId === target) && node.parentNodeId) { (node) => (node.nodeId === source || node.nodeId === target) && node.parentNodeId
return nodeList.find((parent) => parent.nodeId === node.parentNodeId); );
}
}
return undefined;
}, [nodeList, source, target]); }, [nodeList, source, target]);
const defaultZIndex = useMemo( const defaultZIndex = useMemo(
@@ -52,12 +51,20 @@ const ButtonEdge = (props: EdgeProps) => {
[setEdges] [setEdges]
); );
const highlightEdge = useMemo(() => { // Selected edge or source/target node selected
const connectNode = nodes.find((node) => { const [highlightEdge, setHighlightEdge] = useState(false);
return node.selected && (node.id === props.source || node.id === props.target); useThrottleEffect(
}); () => {
return !!(connectNode || selected); const connectNode = nodes.find((node) => {
}, [nodes, props.source, props.target, selected]); return node.selected && (node.id === props.source || node.id === props.target);
});
setHighlightEdge(!!connectNode || !!selected);
},
[nodes, selected, props.source, props.target],
{
wait: 100
}
);
const [, labelX, labelY] = getBezierPath({ const [, labelX, labelY] = getBezierPath({
sourceX, sourceX,

View File

@@ -17,6 +17,7 @@ import { useTranslation } from 'next-i18next';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { maxZoom, minZoom } from '../index'; import { maxZoom, minZoom } from '../index';
import { useKeyPress } from 'ahooks'; import { useKeyPress } from 'ahooks';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
const buttonStyle = { const buttonStyle = {
border: 'none', border: 'none',
@@ -61,11 +62,16 @@ const FlowController = React.memo(function FlowController() {
zoomOut(); zoomOut();
}); });
/*
id: Render node id
*/
const MiniMapNode = useCallback( const MiniMapNode = useCallback(
({ x, y, width, height, color, id }: MiniMapNodeProps) => { ({ x, y, width, height, color, id }: MiniMapNodeProps) => {
// If the node parentNode is folded, the child node will not be displayed
const node = nodeList.find((node) => node.nodeId === id); const node = nodeList.find((node) => node.nodeId === id);
const parentNode = nodeList.find((n) => n.nodeId === node?.parentNodeId); const parentNode = node?.parentNodeId
? nodeList.find((n) => n.nodeId === node?.parentNodeId)
: undefined;
if (parentNode?.isFolded) { if (parentNode?.isFolded) {
return null; return null;
} }

View File

@@ -277,6 +277,7 @@ export const useWorkflow = () => {
const { const {
setConnectingEdge, setConnectingEdge,
nodes, nodes,
nodeList,
onNodesChange, onNodesChange,
setEdges, setEdges,
onChangeNode, onChangeNode,
@@ -370,10 +371,12 @@ export const useWorkflow = () => {
const checkNodeOverLoopNode = useMemoizedFn((node: Node) => { const checkNodeOverLoopNode = useMemoizedFn((node: Node) => {
if (!node) return; if (!node) return;
// 获取所有与当前节点相交的节点,不包含折叠的节点 // 获取所有与当前节点相交的节点
const intersections = getIntersectingNodes(node).filter((node) => !node.data.isFolded); const intersections = getIntersectingNodes(node);
// 获取所有与当前节点相交的节点中,类型为 loop 的节点 // 获取所有与当前节点相交的节点中,类型为 loop 的节点且它不能是折叠状态
const parentNode = intersections.find((item) => item.type === FlowNodeTypeEnum.loop); const parentNode = intersections.find(
(item) => !item.data.isFolded && item.type === FlowNodeTypeEnum.loop
);
const unSupportedTypes = [ const unSupportedTypes = [
FlowNodeTypeEnum.workflowStart, FlowNodeTypeEnum.workflowStart,
@@ -548,8 +551,10 @@ export const useWorkflow = () => {
const onConnectStart = useCallback( const onConnectStart = useCallback(
(event: any, params: OnConnectStartParams) => { (event: any, params: OnConnectStartParams) => {
if (!params.nodeId) return; if (!params.nodeId) return;
const sourceNode = nodes.find((node) => node.id === params.nodeId);
if (sourceNode?.data.isFolded) { // If node is folded, unfold it when connecting
const sourceNode = nodeList.find((node) => node.nodeId === params.nodeId);
if (sourceNode?.isFolded) {
return onChangeNode({ return onChangeNode({
nodeId: params.nodeId, nodeId: params.nodeId,
type: 'attr', type: 'attr',
@@ -559,7 +564,7 @@ export const useWorkflow = () => {
} }
setConnectingEdge(params); setConnectingEdge(params);
}, },
[nodes, setConnectingEdge, onChangeNode] [nodeList, setConnectingEdge, onChangeNode]
); );
const onConnectEnd = useCallback(() => { const onConnectEnd = useCallback(() => {
setConnectingEdge(undefined); setConnectingEdge(undefined);

View File

@@ -120,7 +120,7 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return ( return (
<Box> <Box>
<HStack className="nodrag" cursor={'default'} mb={3}> <HStack className="nodrag" cursor={'default'} mb={3}>
<FormLabel>{t('common:core.module.input_form')}</FormLabel> <FormLabel>{t('workflow:user_form_input_config')}</FormLabel>
<Box flex={'1 0 0'} /> <Box flex={'1 0 0'} />
<Button <Button
variant={'ghost'} variant={'ghost'}
@@ -150,9 +150,9 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<Thead> <Thead>
<Tr> <Tr>
<Th borderBottomLeftRadius={'none !important'}> <Th borderBottomLeftRadius={'none !important'}>
{t('common:core.module.input_name')} {t('workflow:user_form_input_name')}
</Th> </Th>
<Th>{t('common:core.module.input_description')}</Th> <Th>{t('workflow:user_form_input_description')}</Th>
<Th>{t('common:common.Require Input')}</Th> <Th>{t('common:common.Require Input')}</Th>
<Th borderBottomRightRadius={'none !important'}>{t('user:operations')}</Th> <Th borderBottomRightRadius={'none !important'}>{t('user:operations')}</Th>
</Tr> </Tr>
@@ -204,7 +204,7 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
); );
} }
}), }),
[nodeId, editField, t, setEditField, onChangeNode] [t, editField, onChangeNode, nodeId, outputs]
); );
return ( return (

View File

@@ -26,6 +26,7 @@ import { useI18n } from '@/web/context/I18n';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart'; import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import PluginOutputEditModal, { defaultOutput } from './PluginOutputEditModal'; import PluginOutputEditModal, { defaultOutput } from './PluginOutputEditModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const customOutputConfig = { const customOutputConfig = {
selectValueTypeList: Object.values(WorkflowIOValueTypeEnum), selectValueTypeList: Object.values(WorkflowIOValueTypeEnum),
@@ -197,19 +198,21 @@ function Reference({
onClick={openConfirm(onDel)} onClick={openConfirm(onDel)}
/> />
</Flex> </Flex>
<MyIcon <MyTooltip label={t('workflow:plugin_output_tool')}>
name={ <MyIcon
input.isToolOutput !== false name={
? 'core/workflow/template/toolkitActive' input.isToolOutput !== false
: 'core/workflow/template/toolkitInactive' ? 'core/workflow/template/toolkitActive'
} : 'core/workflow/template/toolkitInactive'
w={'14px'} }
color={'myGray.500'} w={'14px'}
cursor={'pointer'} color={'myGray.500'}
mr={2} cursor={'pointer'}
_hover={{ color: 'red.600' }} mr={2}
onClick={() => setEditField(input)} _hover={{ color: 'red.600' }}
/> onClick={() => setEditField(input)}
/>
</MyTooltip>
</Flex> </Flex>
<ReferSelector <ReferSelector
placeholder={t((input.referencePlaceholder as any) || 'select_reference_variable')} placeholder={t((input.referencePlaceholder as any) || 'select_reference_variable')}

View File

@@ -24,6 +24,7 @@ import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const PluginOutputEditModal = ({ const PluginOutputEditModal = ({
customOutputConfig, customOutputConfig,
@@ -167,8 +168,10 @@ const PluginOutputEditModal = ({
<FormLabel flex={'0 0 70px'}>{t('workflow:input_description')}</FormLabel> <FormLabel flex={'0 0 70px'}>{t('workflow:input_description')}</FormLabel>
<Textarea bg={'myGray.50'} {...register('description', {})} /> <Textarea bg={'myGray.50'} {...register('description', {})} />
</Flex> </Flex>
<Flex mt={3} alignItems={'center'} justifyContent={'space-between'}> <Flex mt={3} alignItems={'center'}>
<FormLabel>{t('workflow:is_tool_output')}</FormLabel> <FormLabel>{t('workflow:is_tool_output_label')}</FormLabel>
<QuestionTip label={t('workflow:plugin_output_tool')} ml={1} />
<Box flex={1} />
<Switch {...register('isToolOutput')} /> <Switch {...register('isToolOutput')} />
</Flex> </Flex>
</Stack> </Stack>

View File

@@ -13,9 +13,7 @@ export const ConnectionSourceHandle = ({
nodeId: string; nodeId: string;
isFoldNode?: boolean; isFoldNode?: boolean;
}) => { }) => {
const connectingEdge = useContextSelector(WorkflowContext, (ctx) => ctx.connectingEdge); const { connectingEdge, nodeList, edges } = useContextSelector(WorkflowContext, (ctx) => ctx);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => { const { showSourceHandle, RightHandle, LeftHandlee, TopHandlee, BottomHandlee } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId); const node = nodeList.find((node) => node.nodeId === nodeId);
@@ -32,6 +30,15 @@ export const ConnectionSourceHandle = ({
const rightTargetConnected = edges.some( const rightTargetConnected = edges.some(
(edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right) (edge) => edge.targetHandle === getHandleId(nodeId, 'target', Position.Right)
); );
/*
If the node is folded, must show the handle
hide handle when:
- not folded
- not node
- not sourceHandle
- already connected
*/
if (!isFoldNode && (!node || !node?.sourceHandle?.right || rightTargetConnected)) return null; if (!isFoldNode && (!node || !node?.sourceHandle?.right || rightTargetConnected)) return null;
return ( return (

View File

@@ -82,9 +82,13 @@ const NodeCard = (props: Props) => {
[isTool, nodeList] [isTool, nodeList]
); );
// Current node and parent node
const { node, parentNode } = useMemo(() => { const { node, parentNode } = useMemo(() => {
const node = nodeList.find((node) => node.nodeId === nodeId); const node = nodeList.find((node) => node.nodeId === nodeId);
const parentNode = nodeList.find((n) => n.nodeId === node?.parentNodeId); const parentNode = node?.parentNodeId
? nodeList.find((n) => n.nodeId === node?.parentNodeId)
: undefined;
return { node, parentNode }; return { node, parentNode };
}, [nodeList, nodeId]); }, [nodeList, nodeId]);

View File

@@ -48,10 +48,12 @@ export const uiWorkflow2StoreWorkflow = ({
.filter( .filter(
// Filter out edges that do not have both sourceHandle and targetHandle // Filter out edges that do not have both sourceHandle and targetHandle
(item) => { (item) => {
// Not in react flow page
if (!reactFlowViewport) return true; if (!reactFlowViewport) return true;
const currentSourceNode = nodes.find((node) => node.data.nodeId === item.source); const currentSourceNode = nodes.find((node) => node.data.nodeId === item.source);
if (currentSourceNode?.data.isFolded) return true; if (currentSourceNode?.data.isFolded) return true;
// Not in react flow page
return handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle); return handleIdList.includes(item.sourceHandle) && handleIdList.includes(item.targetHandle);
} }
); );