From 05c7ba4483e430674e6977739ddfedb1e1ae7621 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 29 May 2025 14:29:28 +0800 Subject: [PATCH] feat: Workflow node search (#4920) * add node find (#4902) * add node find * plugin header * fix * fix * remove * type * add searched status * optimize * perf: search nodes --------- Co-authored-by: heheer --- .../zh-cn/docs/development/upgrading/4911.md | 3 +- packages/global/core/workflow/type/node.d.ts | 1 + .../common/String/HighlightText.tsx | 31 ++- packages/web/hooks/useSystem.ts | 4 +- packages/web/i18n/en/workflow.json | 9 +- packages/web/i18n/zh-CN/workflow.json | 7 + packages/web/i18n/zh-Hant/workflow.json | 9 +- .../app/detail/Plugin/Header.tsx | 18 +- .../app/detail/Workflow/Header.tsx | 11 +- .../detail/Workflow/components/SaveButton.tsx | 1 + .../Workflow/components/SearchButton.tsx | 220 ++++++++++++++++++ .../detail/WorkflowComponents/Flow/index.tsx | 15 +- .../Flow/nodes/render/NodeCard.tsx | 11 +- 13 files changed, 312 insertions(+), 28 deletions(-) create mode 100644 projects/app/src/pageComponents/app/detail/Workflow/components/SearchButton.tsx diff --git a/docSite/content/zh-cn/docs/development/upgrading/4911.md b/docSite/content/zh-cn/docs/development/upgrading/4911.md index 1037b3b88..9c9410b17 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4911.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4911.md @@ -10,7 +10,8 @@ weight: 789 ## 🚀 新增内容 -1. 工作流中,子流程版本控制,可选择“保持最新版本”,无需手动更新。 +1. 工作流中增加节点搜索功能。 +2. 工作流中,子流程版本控制,可选择“保持最新版本”,无需手动更新。 ## ⚙️ 优化 diff --git a/packages/global/core/workflow/type/node.d.ts b/packages/global/core/workflow/type/node.d.ts index 9e03094a9..8a7d27923 100644 --- a/packages/global/core/workflow/type/node.d.ts +++ b/packages/global/core/workflow/type/node.d.ts @@ -125,6 +125,7 @@ export type FlowNodeItemType = FlowNodeTemplateType & { nodeId: string; parentNodeId?: string; isError?: boolean; + searchedText?: string; debugResult?: { status: 'running' | 'success' | 'skipped' | 'failed'; message?: string; diff --git a/packages/web/components/common/String/HighlightText.tsx b/packages/web/components/common/String/HighlightText.tsx index 85f09148c..dfc590d35 100644 --- a/packages/web/components/common/String/HighlightText.tsx +++ b/packages/web/components/common/String/HighlightText.tsx @@ -1,17 +1,26 @@ import { Box } from '@chakra-ui/react'; -import React from 'react'; +import React, { useMemo } from 'react'; const HighlightText = ({ rawText, matchText, - color = 'primary.600' + color = 'primary.600', + mode = 'text' }: { rawText: string; matchText: string; color?: string; + mode?: 'text' | 'bg'; }) => { - const regex = new RegExp(`(${matchText})`, 'gi'); - const parts = rawText.split(regex); + const { parts } = useMemo(() => { + const regx = new RegExp(`(${matchText})`, 'gi'); + const parts = rawText.split(regx); + + return { + regx, + parts + }; + }, [rawText, matchText]); return ( @@ -28,7 +37,17 @@ const HighlightText = ({ } return ( - + {part} ); @@ -37,4 +56,4 @@ const HighlightText = ({ ); }; -export default HighlightText; +export default React.memo(HighlightText); diff --git a/packages/web/hooks/useSystem.ts b/packages/web/hooks/useSystem.ts index a086e8f83..3b7e59b88 100644 --- a/packages/web/hooks/useSystem.ts +++ b/packages/web/hooks/useSystem.ts @@ -3,6 +3,8 @@ import { useContextSelector } from 'use-context-selector'; export const useSystem = () => { const isPc = useContextSelector(useSystemStoreContext, (state) => state.isPc); + const isMac = + typeof window !== 'undefined' && window.navigator.userAgent.toLocaleLowerCase().includes('mac'); - return { isPc }; + return { isPc, isMac }; }; diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index af40b3bf1..d40b0ff36 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -63,6 +63,8 @@ "field_required": "Required", "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}", + "find_tip": "Find node ctrl f", + "find_tip_mac": "Find node ⌘ f", "foldAll": "Collapse all", "form_input_result": "User complete input result", "form_input_result_tip": "an object containing the complete result", @@ -123,18 +125,23 @@ "max_tokens": "Maximum Tokens", "mouse_priority": "Mouse first\n- Press the left button to drag the canvas\n- Hold down shift and left click to select batches", "new_context": "New Context", + "next": "Next", + "no_match_node": "No results", + "no_node_found": "No node was not found", "not_contains": "Does Not Contain", "only_the_reference_type_is_supported": "Only reference type is supported", "optional_value_type": "Optional Value Type", "optional_value_type_tip": "You can specify one or more data types. When dynamically adding fields, users can only select the configured types.", "pan_priority": "Touchpad first\n- Click to batch select\n- Move the canvas with two fingers", "pass_returned_object_as_output_to_next_nodes": "Pass the object returned in the code as output to the next nodes. The variable name needs to correspond to the return key.", + "please_enter_node_name": "Enter the node name", "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.global_file_input": "File links (deprecated)", "plugin_file_abandon_tip": "Plugin global file upload has been deprecated, please adjust it as soon as possible. \nRelated functions can be achieved through plug-in input and adding image type 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", + "previous": "Previous", "question_classification": "Classify", "question_optimization": "Query extension", "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", @@ -177,9 +184,9 @@ "text_content_extraction": "Text Extract", "text_to_extract": "Text to Extract", "these_variables_will_be_input_parameters_for_code_execution": "These variables will be input parameters for code execution", - "tool.tool_result": "Tool operation results", "to_add_node": "to add", "to_connect_node": "to connect", + "tool.tool_result": "Tool operation results", "tool_call_termination": "Stop ToolCall", "tool_custom_field": "Custom Tool", "tool_field": " Tool Field Parameter Configuration", diff --git a/packages/web/i18n/zh-CN/workflow.json b/packages/web/i18n/zh-CN/workflow.json index 7a358bcdb..6eb075039 100644 --- a/packages/web/i18n/zh-CN/workflow.json +++ b/packages/web/i18n/zh-CN/workflow.json @@ -63,6 +63,8 @@ "field_required": "必填", "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}", + "find_tip": "查找节点 ctrl f", + "find_tip_mac": "查找节点 ⌘ f", "foldAll": "全部折叠", "form_input_result": "用户完整输入结果", "form_input_result_tip": "一个包含完整结果的对象", @@ -123,18 +125,23 @@ "max_tokens": "最大 Tokens", "mouse_priority": "鼠标优先\n- 左键按下后可拖动画布\n- 按住 shift 后左键可批量选择", "new_context": "新的上下文", + "next": "下一个", + "no_match_node": "无结果", + "no_node_found": "未搜索到节点", "not_contains": "不包含", "only_the_reference_type_is_supported": "仅支持引用类型", "optional_value_type": "可选的数据类型", "optional_value_type_tip": "可以指定 1 个或多个数据类型,用户在动态添加字段时,仅可选择配置的类型", "pan_priority": "触摸板优先\n- 单击批量选择\n- 双指移动画布", "pass_returned_object_as_output_to_next_nodes": "将代码中 return 的对象作为输出,传递给后续的节点。变量名需要对应 return 的 key", + "please_enter_node_name": "请输入节点名称", "plugin.Instruction_Tip": "可以配置一段说明,以解释该插件的用途。每次使用插件前,会显示该段说明。支持标准 Markdown 语法。", "plugin.Instructions": "使用说明", "plugin.global_file_input": "文件链接(弃用)", "plugin_file_abandon_tip": "插件全局文件上传已弃用,请尽快调整。可以通过插件输入,添加图片类型输入来实现相关功能。", "plugin_input": "插件输入", "plugin_output_tool": "插件作为工具执行时,该字段是否作为工具响应结果", + "previous": "上一个", "question_classification": "问题分类", "question_optimization": "问题优化", "quote_content_placeholder": "可以自定义引用内容的结构,以更好的适配不同场景。可以使用一些变量来进行模板配置\n{{q}} - 主要内容\n{{a}} - 辅助数据\n{{source}} - 来源名\n{{sourceId}} - 来源ID\n{{index}} - 第 n 个引用", diff --git a/packages/web/i18n/zh-Hant/workflow.json b/packages/web/i18n/zh-Hant/workflow.json index 5a746e55e..7607ebd5b 100644 --- a/packages/web/i18n/zh-Hant/workflow.json +++ b/packages/web/i18n/zh-Hant/workflow.json @@ -63,6 +63,8 @@ "field_required": "必填", "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}", + "find_tip": "查找節點 ctrl f", + "find_tip_mac": "查找節點 ⌘ f", "foldAll": "全部折疊", "form_input_result": "使用者完整輸入結果", "form_input_result_tip": "一個包含完整結果的物件", @@ -123,18 +125,23 @@ "max_tokens": "最大 Token 數", "mouse_priority": "滑鼠優先\n- 按下左鍵拖曳畫布\n- 按住 Shift 鍵並點選左鍵可批次選取", "new_context": "新的脈絡", + "next": "下一個", + "no_match_node": "無結果", + "no_node_found": "未搜索到節點", "not_contains": "不包含", "only_the_reference_type_is_supported": "僅支援引用類型", "optional_value_type": "可選的資料類型", "optional_value_type_tip": "可以指定一或多個資料類型,使用者在動態新增欄位時,只能選擇已設定的類型", "pan_priority": "觸控板優先\n- 點選可批次選取\n- 使用兩指移動畫布", "pass_returned_object_as_output_to_next_nodes": "將程式碼中 return 的物件作為輸出,傳遞給後續的節點。變數名稱需要對應 return 的鍵值", + "please_enter_node_name": "請輸入節點名稱", "plugin.Instruction_Tip": "您可以設定一段說明來解釋這個外掛程式的用途。每次使用外掛程式前,都會顯示這段說明。支援標準 Markdown 語法。", "plugin.Instructions": "使用說明", "plugin.global_file_input": "檔案連結(已淘汰)", "plugin_file_abandon_tip": "外掛程式全域檔案上傳功能已淘汰,請儘速調整。您可以透過外掛程式輸入,新增圖片類型輸入來達成相關功能。", "plugin_input": "外掛程式輸入", "plugin_output_tool": "外掛程式作為工具執行時,這個欄位是否作為工具的回應結果", + "previous": "上一個", "question_classification": "問題分類", "question_optimization": "問題最佳化", "quote_content_placeholder": "可以自訂引用內容的結構,以便更好地適應不同場景。可以使用一些變數來設定範本\n{{q}} - 主要內容\n{{a}} - 輔助資料\n{{source}} - 來源名稱\n{{sourceId}} - 來源 ID\n{{index}} - 第 n 個引用", @@ -177,9 +184,9 @@ "text_content_extraction": "文字內容擷取", "text_to_extract": "要擷取的文字", "these_variables_will_be_input_parameters_for_code_execution": "這些變數會作為程式碼執行的輸入參數", - "tool.tool_result": "工具運行結果", "to_add_node": "添加節點", "to_connect_node": "連接節點", + "tool.tool_result": "工具運行結果", "tool_call_termination": "工具呼叫終止", "tool_custom_field": "自訂工具變數", "tool_field": "工具參數設定", diff --git a/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx b/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx index bfd0bd6a1..1df64a484 100644 --- a/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/Plugin/Header.tsx @@ -25,16 +25,20 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import SaveButton from '../Workflow/components/SaveButton'; import PublishHistories from '../PublishHistoriesSlider'; import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; import { WorkflowStatusContext } from '../WorkflowComponents/context/workflowStatusContext'; +import SaveButton from '../Workflow/components/SaveButton'; const Header = () => { const { t } = useTranslation(); const { isPc } = useSystem(); const router = useRouter(); - const { toast } = useToast(); + const { toast: backSaveToast } = useToast({ + containerStyle: { + mt: '60px' + } + }); const { appDetail, onSaveApp, currentTab } = useContextSelector(AppContext, (v) => v); const isV2Workflow = appDetail?.version === 'v2'; @@ -183,6 +187,7 @@ const Header = () => { size={'sm'} leftIcon={} variant={'whitePrimary'} + flexShrink={0} onClick={() => { const data = flowData2StoreDataAndCheck(); if (data) { @@ -211,12 +216,12 @@ const Header = () => { onBack, onOpenBackConfirm, isV2Workflow, - showHistoryModal, t, + showHistoryModal, loading, onClickSave, - flowData2StoreDataAndCheck, setShowHistoryModal, + flowData2StoreDataAndCheck, setWorkflowTestData ]); @@ -229,10 +234,11 @@ const Header = () => { setShowHistoryModal(false); }} past={past} - onSwitchTmpVersion={onSwitchTmpVersion} onSwitchCloudVersion={onSwitchCloudVersion} + onSwitchTmpVersion={onSwitchTmpVersion} /> )} + { await onClickSave({}); onCloseBackConfirm(); onBack(); - toast({ + backSaveToast({ status: 'success', title: t('app:saved_success'), position: 'top-right' diff --git a/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx b/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx index 337140b8b..1df64a484 100644 --- a/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx +++ b/projects/app/src/pageComponents/app/detail/Workflow/Header.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'next-i18next'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { useContextSelector } from 'use-context-selector'; -import { WorkflowContext } from '../WorkflowComponents/context'; +import { WorkflowContext, type WorkflowSnapshotsType } from '../WorkflowComponents/context'; import { AppContext, TabEnum } from '../context'; import RouteTab from '../RouteTab'; import { useRouter } from 'next/router'; @@ -25,10 +25,10 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import SaveButton from './components/SaveButton'; import PublishHistories from '../PublishHistoriesSlider'; import { WorkflowEventContext } from '../WorkflowComponents/context/workflowEventContext'; import { WorkflowStatusContext } from '../WorkflowComponents/context/workflowStatusContext'; +import SaveButton from '../Workflow/components/SaveButton'; const Header = () => { const { t } = useTranslation(); @@ -187,6 +187,7 @@ const Header = () => { size={'sm'} leftIcon={} variant={'whitePrimary'} + flexShrink={0} onClick={() => { const data = flowData2StoreDataAndCheck(); if (data) { @@ -215,12 +216,12 @@ const Header = () => { onBack, onOpenBackConfirm, isV2Workflow, - showHistoryModal, t, + showHistoryModal, loading, onClickSave, - flowData2StoreDataAndCheck, setShowHistoryModal, + flowData2StoreDataAndCheck, setWorkflowTestData ]); @@ -228,7 +229,7 @@ const Header = () => { <> {Render} {showHistoryModal && isV2Workflow && currentTab === TabEnum.appEdit && ( - onClose={() => { setShowHistoryModal(false); }} diff --git a/projects/app/src/pageComponents/app/detail/Workflow/components/SaveButton.tsx b/projects/app/src/pageComponents/app/detail/Workflow/components/SaveButton.tsx index d73f1a53b..add09b1cd 100644 --- a/projects/app/src/pageComponents/app/detail/Workflow/components/SaveButton.tsx +++ b/projects/app/src/pageComponents/app/detail/Workflow/components/SaveButton.tsx @@ -43,6 +43,7 @@ const SaveButton = ({ Trigger={ + + + + + + + ); +}; + +export default React.memo(SearchButton); diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/index.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/index.tsx index d205f8a53..4dd9d1ff1 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/index.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import ReactFlow, { type NodeProps, SelectionMode } from 'reactflow'; import { Box, IconButton, useDisclosure } from '@chakra-ui/react'; -import { SmallCloseIcon } from '@chakra-ui/icons'; import { EDGE_TYPE, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import dynamic from 'next/dynamic'; @@ -20,6 +19,8 @@ import ContextMenu from './components/ContextMenu'; import { WorkflowNodeEdgeContext, WorkflowInitContext } from '../context/workflowInitContext'; import { WorkflowEventContext } from '../context/workflowEventContext'; import NodeTemplatesPopover from './NodeTemplatesPopover'; +import SearchButton from '../../Workflow/components/SearchButton'; +import MyIcon from '@fastgpt/web/components/common/Icon'; const NodeSimple = dynamic(() => import('./nodes/NodeSimple')); const nodeTypes: Record = { @@ -113,20 +114,22 @@ const Workflow = () => { <> } - transform={isOpenTemplate ? '' : 'rotate(135deg)'} + icon={} transition={'0.2s ease'} aria-label={''} zIndex={1} - boxShadow={'2px 2px 6px #85b1ff'} + boxShadow={ + '0px 4px 10px 0px rgba(19, 51, 107, 0.20), 0px 0px 1px 0px rgba(19, 51, 107, 0.50)' + } onClick={() => { isOpenTemplate ? onCloseTemplate() : onOpenTemplate(); }} /> + diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 1bb2f98a2..4025b4017 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -36,6 +36,7 @@ import MyTag from '@fastgpt/web/components/common/Tag/index'; import MySelect from '@fastgpt/web/components/common/MySelect'; import { useCreation } from 'ahooks'; import { formatToolError } from '@fastgpt/global/core/app/utils'; +import HighlightText from '@fastgpt/web/components/common/String/HighlightText'; type Props = FlowNodeItemType & { children?: React.ReactNode | React.ReactNode[] | string; @@ -45,6 +46,7 @@ type Props = FlowNodeItemType & { w?: string | number; h?: string | number; selected?: boolean; + searchedText?: string; menuForbid?: { debug?: boolean; copy?: boolean; @@ -70,6 +72,7 @@ const NodeCard = (props: Props) => { h = 'full', nodeId, selected, + searchedText, menuForbid, isTool = false, isError = false, @@ -187,7 +190,12 @@ const NodeCard = (props: Props) => { h={'24px'} /> - {t(name as any)} +