From 7b43e591721fa5b2beabf140cd9376fe57596bff Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 7 May 2026 18:40:41 +0800 Subject: [PATCH 1/2] feat(web): single node run --- web/eslint.config.js | 3 + web/src/api/application.ts | 6 +- web/src/assets/images/workflow/run.svg | 2 +- web/src/i18n/en.ts | 9 + web/src/i18n/zh.ts | 10 + .../Conversation/components/FileUpload.tsx | 6 +- .../components/UploadFileListModal.tsx | 4 +- .../Workflow/components/CheckList/index.tsx | 6 +- .../Editor/plugin/AutocompletePlugin.tsx | 3 +- .../Workflow/components/Nodes/NodeTools.tsx | 7 +- .../Workflow/components/PortClickHandler.tsx | 39 +- .../Properties/CycleVarsList/index.tsx | 2 +- .../Properties/MemoryConfig/index.tsx | 2 - .../Properties/hooks/useVariableList.ts | 23 +- .../Workflow/components/Properties/index.tsx | 52 ++- .../components/SingleNodeRun/ContextList.tsx | 74 ++++ .../components/SingleNodeRun/FileVarInput.tsx | 134 +++++++ .../components/SingleNodeRun/index.tsx | 343 ++++++++++++++++++ web/src/views/Workflow/constant.ts | 10 +- .../views/Workflow/hooks/useWorkflowGraph.ts | 49 ++- web/src/views/Workflow/index.tsx | 2 + 21 files changed, 724 insertions(+), 62 deletions(-) create mode 100644 web/src/views/Workflow/components/SingleNodeRun/ContextList.tsx create mode 100644 web/src/views/Workflow/components/SingleNodeRun/FileVarInput.tsx create mode 100644 web/src/views/Workflow/components/SingleNodeRun/index.tsx diff --git a/web/eslint.config.js b/web/eslint.config.js index b19330b1..48de4ccb 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -19,5 +19,8 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, + linterOptions: { + 'eslint@typescript-eslint/no-explicit-any': false + } }, ]) diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 6965f363..563d1353 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 13:59:45 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-24 15:48:30 + * @Last Modified time: 2026-05-06 15:09:49 */ import { request } from '@/utils/request' import type { ApplicationModalData } from '@/views/ApplicationManagement/types' @@ -178,4 +178,8 @@ export const getAppLogDetail = (app_id: string, conversation_id: string) => { // Reset agent model config to default export const resetAppModelConfig = (app_id: string) => { return request.get(`/apps/${app_id}/model/parameters/default`) +} +// Single node test run +export const nodeRun = (app_id: string, node_id: string, values: Record) => { + return request.post(`/apps/${app_id}/workflow/nodes/${node_id}/run`, values) } \ No newline at end of file diff --git a/web/src/assets/images/workflow/run.svg b/web/src/assets/images/workflow/run.svg index 5d320106..d686607b 100644 --- a/web/src/assets/images/workflow/run.svg +++ b/web/src/assets/images/workflow/run.svg @@ -2,7 +2,7 @@ 编组 31 - + diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 4b53403e..83efb1ab 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2536,6 +2536,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re input_result: 'Input', output_result: 'Output', process_result: 'Data Processing', + inputs_result: 'Input', + outputs_result: 'Output', error: 'Error Message', loopNum: ' loops', iterationNum: ' iterations', @@ -2546,6 +2548,13 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re output_cycle_vars: 'Final Loop Variables', }, sureReplace: 'Confirm Replace', + testRun: 'Test Run', + variables: 'Variables', + startRun: 'Start Run', + reStartRun: 'Restart Run', + status: 'Status', + elapsedTime: 'Elapsed Time', + totalTokens: 'Total Tokens', checkList: 'Check List', checkListDesc: 'Ensure all issues are resolved before publishing', checkListEmpty: 'No issues found', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 9ec419ca..5447d9b4 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2501,6 +2501,8 @@ export const zh = { input_result: '输入', output_result: '输出', process_result: '数据处理', + inputs_result: '输入', + outputs_result: '输出', error: '错误信息', loopNum: '个循环', iterationNum: '个迭代', @@ -2511,6 +2513,13 @@ export const zh = { output_cycle_vars: '最终循环变量', }, sureReplace: '确认替换', + testRun: '测试运行', + variables: '变量', + startRun: '开始运行', + reStartRun: '重新运行', + status: '状态', + elapsedTime: '运行时间', + totalTokens: '总 TOKEN 数', checkList: '检查清单', checkListDesc: '发布前确保所有问题均已解决', checkListEmpty: '没有发现问题', @@ -2555,6 +2564,7 @@ export const zh = { variableSelect: { empty: '暂无变量', }, + singleRun: '运行此节点', }, emotionEngine: { emotionEngineConfig: '情感引擎配置', diff --git a/web/src/views/Conversation/components/FileUpload.tsx b/web/src/views/Conversation/components/FileUpload.tsx index c8715fd2..7d53aa3e 100644 --- a/web/src/views/Conversation/components/FileUpload.tsx +++ b/web/src/views/Conversation/components/FileUpload.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:42 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-02 18:29:48 + * @Last Modified time: 2026-04-21 10:22:41 */ /** * File Upload Component @@ -56,7 +56,7 @@ interface UploadFilesProps extends Omit { /** Custom file removal callback */ onRemove?: (file: UploadFile) => boolean | void | Promise; - featureConfig: FeaturesConfigForm['file_upload']; + featureConfig?: FeaturesConfigForm['file_upload']; textType?: 'button' | 'text'; block?: boolean; } @@ -184,7 +184,7 @@ const UploadFiles = forwardRef(({ audio: 'audio_max_size_mb', } const maxSizeKey = categoryMap[mimePrefix] ?? 'document_max_size_mb' - const maxSize = (featureConfig[maxSizeKey] as number) ?? fileSize + const maxSize = (featureConfig?.[maxSizeKey] as number) ?? fileSize const fileSizeMB = file.size / 1024 / 1024 const isLtMaxSize = fileSizeMB < maxSize; diff --git a/web/src/views/Conversation/components/UploadFileListModal.tsx b/web/src/views/Conversation/components/UploadFileListModal.tsx index 82a14f8e..8875298d 100644 --- a/web/src/views/Conversation/components/UploadFileListModal.tsx +++ b/web/src/views/Conversation/components/UploadFileListModal.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-06 21:09:47 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-23 17:49:42 + * @Last Modified time: 2026-04-21 10:22:45 */ /** * Upload File List Modal Component @@ -31,7 +31,7 @@ const FormItem = Form.Item; interface UploadFileListModalProps { /** Callback to refresh parent component with new file list */ refresh: (fileList?: any[]) => void; - featureConfig: FeaturesConfigForm['file_upload'] + featureConfig?: FeaturesConfigForm['file_upload'] } /** diff --git a/web/src/views/Workflow/components/CheckList/index.tsx b/web/src/views/Workflow/components/CheckList/index.tsx index 5ba13212..4154bc37 100644 --- a/web/src/views/Workflow/components/CheckList/index.tsx +++ b/web/src/views/Workflow/components/CheckList/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-04-09 18:58:21 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-20 10:39:17 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-05-07 18:35:54 */ import { useState, useCallback, useEffect, useRef, type FC } from 'react' import { Popover, Flex } from 'antd' @@ -60,7 +60,7 @@ const specialValidators: Record boolean> = { return val.some(c => !c?.expressions?.length || c.expressions.some((expr: any) => !isExprSet(expr))) }, // question-classifier.categories: every category must have a value - 'question-classifier.categories': (val: any[]) => !Array.isArray(val) || !val.some(c => c?.class_name && String(c.class_name).trim()), + 'question-classifier.categories': (val: any[]) => !Array.isArray(val) || !val.every(c => c?.class_name && String(c.class_name).trim()), // var-aggregator.group_variables: must be non-empty array 'var-aggregator.group_variables': (val: any[]) => !Array.isArray(val) || !val.length, // assigner.assignments: every item needs variable_selector + operation; value required unless operation is 'clear' diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index a5ea9771..a60bc250 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2025-12-23 16:22:51 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-13 14:00:07 + * @Last Modified time: 2026-05-06 15:06:03 */ import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react'; import { createPortal } from 'react-dom'; @@ -27,6 +27,7 @@ export interface Suggestion { disabled?: boolean; // Flag for disabled state children?: Suggestion[]; // Sub-variables (e.g. file fields) parentLabel?: string; // Parent variable label (for child display) + default?: any; } // Autocomplete plugin for variable suggestions triggered by '/' character diff --git a/web/src/views/Workflow/components/Nodes/NodeTools.tsx b/web/src/views/Workflow/components/Nodes/NodeTools.tsx index 4948f35d..8bd3307c 100644 --- a/web/src/views/Workflow/components/Nodes/NodeTools.tsx +++ b/web/src/views/Workflow/components/Nodes/NodeTools.tsx @@ -20,9 +20,8 @@ const NodeTools: FC<{ node: Node }> = ({ } } return ( -
= ({
-
+ ) } diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index e339e117..215bbc12 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-09 18:30:28 - * @Last Modified by: mikey.zhaopeng - * @Last Modified time: 2026-05-06 11:46:02 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-05-07 18:38:06 */ import { useEffect, useState } from 'react'; import { Flex, Popover } from 'antd'; @@ -176,17 +176,30 @@ const PortClickHandler: React.FC = ({ graph }) => { if (gap < requiredSpace) { const shiftX = requiredSpace - gap; const visited = new Set(); - const shiftDownstream = (cell: any) => { + const isCycleContainer = (type: string) => type === 'loop' || type === 'iteration'; + const shiftDownstream = (cell: any, shiftChildren: boolean = false) => { if (visited.has(cell.id)) return; visited.add(cell.id); const pos = cell.getPosition(); cell.setPosition(pos.x + shiftX, pos.y); + if (shiftChildren) { + const cellType = cell.getData()?.type; + if (isCycleContainer(cellType)) { + const cycleId = cell.getData()?.id; + const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === cycleId); + childNodes.forEach((child: any) => { + const childPos = child.getPosition(); + child.setPosition(childPos.x + shiftX, childPos.y); + }); + } + } graph.getConnectedEdges(cell, { outgoing: true }).forEach((e: any) => { const tCell = graph.getCellById(e.getTargetCellId()); - if (tCell?.isNode()) shiftDownstream(tCell); + if (tCell?.isNode()) shiftDownstream(tCell, shiftChildren); }); }; - shiftDownstream(edgeInsertion.targetCell); + const targetCellType = edgeInsertion.targetCell.getData()?.type; + shiftDownstream(edgeInsertion.targetCell, isCycleContainer(targetCellType)); } } else if (addNodePosition) { newX = addNodePosition.x; @@ -241,7 +254,7 @@ const PortClickHandler: React.FC = ({ graph }) => { if (sourceNodeData.cycle) { const parentNode = graph.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle); - if (parentNode) parentNode.addChild(newNode, { silent: true }); + if (parentNode) parentNode.addChild(newNode); } if (edgeInsertion) { @@ -285,8 +298,8 @@ const PortClickHandler: React.FC = ({ graph }) => { y: parentBBox.y + 70 + 4, data: { type: 'add-node', label: t('workflow.addNode'), icon: '+', parentId: id, cycle: id }, }); - newNode.addChild(cycleStartNode, { silent: true }); - newNode.addChild(addNodePlaceholder, { silent: true }); + newNode.addChild(cycleStartNode); + newNode.addChild(addNodePlaceholder); const innerEdge = graph.addEdge({ source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find((p: any) => p.group === 'right')?.id || 'right' }, target: { cell: addNodePlaceholder.id, port: addNodePlaceholder.getPorts().find((p: any) => p.group === 'left')?.id || 'left' }, @@ -295,11 +308,11 @@ const PortClickHandler: React.FC = ({ graph }) => { addedCells.push(cycleStartNode, addNodePlaceholder, innerEdge); } - // Adjust loop node size when child node is added via port within loop node - const cycleId = sourceNodeData.cycle; - if (cycleId) { - adjustCycleContainerSize(graph, cycleId); - } + // Adjust loop node size when child node is added via port within loop node + const cycleId = sourceNodeData.cycle; + if (cycleId) { + adjustCycleContainerSize(graph, cycleId); + } // toFront const bringCycleChildrenToFront = (cycleContainerId: string) => { diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index ce37743b..076c903b 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -133,7 +133,7 @@ const CycleVarsList: FC = ({ return option.dataType === currentType })} - variant="borderless" + variant="filled" size="small" className="select" /> diff --git a/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx b/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx index edac612d..9f68b212 100644 --- a/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx +++ b/web/src/views/Workflow/components/Properties/MemoryConfig/index.tsx @@ -13,8 +13,6 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({ const { t } = useTranslation() const form = Form.useFormInstance(); const values = Form.useWatch([], form) || {} - - console.log('MemoryConfig', values) const handleChangeEnable = (value: boolean) => { if (value) { diff --git a/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts b/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts index f979d924..d8e172a4 100644 --- a/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts +++ b/web/src/views/Workflow/components/Properties/hooks/useVariableList.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-01-19 17:00:26 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-13 10:44:17 + * @Last Modified time: 2026-05-07 18:36:58 */ /** * useVariableList Hook @@ -97,14 +97,15 @@ const addVariable = ( dataType: string, value: string, nodeData: any, - extra?: Partial + extra?: Partial, + defaultValue?: any ) => { if (!keys.has(key)) { keys.add(key); const children = dataType === 'file' ? buildFileChildren(key, value, nodeData, label) : undefined; - list.push({ key, label, type: 'variable', dataType, value, nodeData, children, ...extra }); + list.push({ key, label, type: 'variable', dataType, value, nodeData, children, default: defaultValue, ...extra }); } }; @@ -153,7 +154,7 @@ const processNodeVariables = ( case 'start': // Add start node variables [...(config?.variables?.defaultValue ?? []), ...(config?.variables?.value ?? [])].forEach((v: any) => { - if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${v.name}`, v.name, v.type, `${dataNodeId}.${v.name}`, nodeData); + if (v?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${v.name}`, v.name, v.type, `${dataNodeId}.${v.name}`, nodeData, undefined, v.defaultValue ?? v.default); }); // Add system variables config?.variables?.sys?.forEach((v: any) => { @@ -164,7 +165,7 @@ const processNodeVariables = ( case 'parameter-extractor': // Add extracted parameters (config?.params?.defaultValue || []).forEach((p: any) => { - if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData); + if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData, undefined, p.defaultValue ?? p.default); }); break; @@ -178,7 +179,7 @@ const processNodeVariables = ( const fv = variableList.find(v => `{{${v.value}}}` === gv.value[0]); if (fv) dt = fv.dataType; } - addVariable(variableList, addedKeys, `${dataNodeId}_${gv.key}`, gv.key, dt, `${dataNodeId}.${gv.key}`, nodeData); + addVariable(variableList, addedKeys, `${dataNodeId}_${gv.key}`, gv.key, dt, `${dataNodeId}.${gv.key}`, nodeData, undefined, gv.defaultValue ?? gv.default); } }); } else { @@ -205,14 +206,14 @@ const processNodeVariables = ( case 'loop': // Add loop cycle variables (config.cycle_vars.defaultValue || []).forEach((cv: any) => { - if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData); + if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData, undefined, cv.defaultValue ?? cv.default); }); break; case 'code': // Add code node output variables (config.output_variables.defaultValue || []).forEach((cv: any) => { - if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData); + if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData, undefined, cv.defaultValue ?? cv.default); }); break; } @@ -321,13 +322,13 @@ export const getChildNodeVariables = ( // Add parameter-extractor variables if (type === 'parameter-extractor') { (nodeData.config?.params?.defaultValue || []).forEach((p: any) => { - if (p?.name) addVariable(list, keys, `${nodeId}_${p.name}`, p.name, p.type || 'string', `${nodeId}.${p.name}`, nodeData); + if (p?.name) addVariable(list, keys, `${nodeId}_${p.name}`, p.name, p.type || 'string', `${nodeId}.${p.name}`, nodeData, undefined, p.defaultValue ?? p.default); }); } // Add code node variables if (type === 'code') { (nodeData.config?.output_variables?.defaultValue || []).forEach((p: any) => { - if (p?.name) addVariable(list, keys, `${nodeId}_${p.name}`, p.name, p.type || 'string', `${nodeId}.${p.name}`, nodeData); + if (p?.name) addVariable(list, keys, `${nodeId}_${p.name}`, p.name, p.type || 'string', `${nodeId}.${p.name}`, nodeData, undefined, p.defaultValue ?? p.default); }); } }); @@ -393,7 +394,7 @@ export const useVariableList = ( const relevantIds = [...getPreviousNodes(selectedNode.id), ...childIds, ...(parentLoop ? getPreviousNodes(parentLoop.id) : [])]; // Add chat variables - chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' })); + chatVariables?.forEach(v => addVariable(list, keys, `CONVERSATION_${v.name}`, v.name, v.type, `conv.${v.name}`, { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' }, { group: 'CONVERSATION' }, v.defaultValue ?? v.default)); // Process each relevant node: deferred types last (they depend on prior variables) const deferredIds: string[] = []; diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index f1fb6258..a604c348 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -2,13 +2,13 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:39:59 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-21 20:27:33 + * @Last Modified time: 2026-05-07 18:36:31 */ import { type FC, useEffect, useState, useMemo } from "react"; import clsx from 'clsx' import { useTranslation } from 'react-i18next' import { Graph, Node } from '@antv/x6'; -import { Form, Input, Select, InputNumber, Switch, Flex, Space, Dropdown, type MenuProps, Button } from 'antd'; +import { Form, Input, Select, InputNumber, Switch, Flex, Space, Dropdown, type MenuProps, Button, App, Popover } from 'antd'; import type { NodeConfig, NodeProperties, ChatVariable } from '../../types' import CustomSelect from "@/components/CustomSelect"; @@ -28,6 +28,7 @@ import ToolConfig from './ToolConfig' import MemoryConfig from './MemoryConfig' import VariableList from './VariableList' import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList' +import { useWorkflowStore } from '@/store/workflow' import styles from './properties.module.css' import Editor, { type LexicalEditorProps } from "../Editor"; import RbSlider from '@/components/RbSlider' @@ -39,6 +40,8 @@ import ModelConfig from './ModelConfig' import ModelSelect from '@/components/ModelSelect' import ListOperator from './ListOperator' import MappingList from "./MappingList"; +import SingleNodeRun from '../SingleNodeRun' +import { cannotRunNodes } from '../../constant' /** * Props for Properties component @@ -58,8 +61,12 @@ interface PropertiesProps { parseEvent: () => void; /** Workflow configuration */ config?: any; + /** App ID for node run */ + appId?: string; /** Chat variables */ chatVariables: ChatVariable[]; + /** Function to save workflow configuration */ + handleSave: (flag?: boolean) => Promise; } /** @@ -71,9 +78,13 @@ const Properties: FC = ({ selectedNode, graphRef, chatVariables, - blankClick + blankClick, + config, + appId, + handleSave, }) => { const { t } = useTranslation() + const { message } = App.useApp() const [form] = Form.useForm(); const [configs, setConfigs] = useState>({} as Record) const values = Form.useWatch([], form); @@ -530,11 +541,35 @@ const Properties: FC = ({ } } + const [isRun, setIsRun] = useState(false); + const { getCheckResults } = useWorkflowStore() + const handleRun = () => { + handleSave?.(false) + .then(() => { + if (appId) { + const nodeResult = getCheckResults(appId).find(r => r.id === selectedNode.id) + const configErrors = nodeResult?.errors.filter(e => e.key !== 'notConnected') ?? [] + if (configErrors.length) { + message.error(configErrors[0].message) + return + } + } + setIsRun(true) + }) + } + return ( + <>
+ {!cannotRunNodes.includes(selectedNode?.data?.type) && +
+
} = ({
} + + {isRun && ( + setIsRun(false)} + selectedNode={selectedNode} + appId={appId || config?.app_id || ''} + variableList={variableList} + /> + )} + ); }; export default Properties; \ No newline at end of file diff --git a/web/src/views/Workflow/components/SingleNodeRun/ContextList.tsx b/web/src/views/Workflow/components/SingleNodeRun/ContextList.tsx new file mode 100644 index 00000000..30cb8a0f --- /dev/null +++ b/web/src/views/Workflow/components/SingleNodeRun/ContextList.tsx @@ -0,0 +1,74 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-05-07 18:37:15 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-05-07 18:37:15 + */ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Button, Flex, Form } from 'antd' + +import CodeMirrorEditor from '@/components/CodeMirrorEditor' + +const defaultContextItem = { + "content": "", + "title": "", + "url": "", + "icon": "", + "metadata": { + "dataset_id": "", + "dataset_name": "", + "document_id": [], + "document_name": "", + "document_data_source_type": "", + "segment_id": "", + "segment_position": "", + "segment_word_count": "", + "segment_hit_count": "", + "segment_index_node_hash": "", + "score": "" + } +} + +const ContextList: FC = () => { + const { t } = useTranslation() + + return ( + + {(fields, { add, remove }) => ( + + +
{t('workflow.config.llm.context')}
+ +
+ {fields.map(({ key, name }) => ( + + + JSON +
remove(name)} + >
+
+ + + +
+ ))} +
+ )} +
+ ) +} + +export default ContextList diff --git a/web/src/views/Workflow/components/SingleNodeRun/FileVarInput.tsx b/web/src/views/Workflow/components/SingleNodeRun/FileVarInput.tsx new file mode 100644 index 00000000..61208d19 --- /dev/null +++ b/web/src/views/Workflow/components/SingleNodeRun/FileVarInput.tsx @@ -0,0 +1,134 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-05-07 18:37:23 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-05-07 18:37:23 + */ +import { type FC, useState, useRef, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { Button, Form, Row, Col } from 'antd' +import type { FormInstance } from 'antd' +import UploadFiles, { transform_file_type } from '@/views/Conversation/components/FileUpload' +import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal' +import type { UploadFileListModalRef } from '@/views/Conversation/types' +import FileList from '@/components/Chat/FileList' +import { getFileInfoByUrl } from '@/api/fileStorage' + +interface FileVarInputProps { + name: string | string[] + dataType: string + form: FormInstance +} + +const FileVarInput: FC = ({ name, dataType, form }) => { + const { t } = useTranslation() + const uploadFileListModalRef = useRef(null) + const [fileList, setFileList] = useState([]) + + const isSingle = dataType === 'file' + + const setFormFileValue = (updated: any[]) => { + form.setFieldValue(name, isSingle ? (updated[0] ?? null) : updated) + } + + const fileChange = (file?: any) => { + const fileObj = file ? { + ...file, + type: file.type, + transfer_method: 'local_file', + upload_file_id: file.response?.data?.file_id, + } : undefined + if (isSingle) { + const updated = [fileObj] + setFileList(updated) + setTimeout(() => setFormFileValue(updated), 0) + return + } + setFileList(prev => { + const index = prev.findIndex((item: any) => item.uid === fileObj.uid) + const updated = index > -1 + ? prev.map((item, i) => i === index ? fileObj : item) + : [...prev, fileObj] + setTimeout(() => setFormFileValue(updated), 0) + return updated + }) + } + + const addFileList = (list?: any[]) => { + if (!list?.length) return + const uploadingList = list.map(f => ({ ...f, status: 'uploading' })) + setFileList(prev => { + const updated = isSingle ? [uploadingList[0]] : [...prev, ...uploadingList] + setTimeout(() => setFormFileValue(updated), 0) + return updated + }); + (isSingle ? [uploadingList[0]] : uploadingList).forEach(file => { + getFileInfoByUrl(file.url) + .then((res) => { + const { file_name, file_size, content_type } = res as { file_name: string; file_size: number; content_type: string } + setFileList(prev => { + const updated = prev.map(f => + f.uid === file.uid + ? { ...f, status: 'done', name: file_name, size: file_size, type: transform_file_type[content_type] || content_type } + : f + ) + setFormFileValue(updated) + return updated + }) + }) + .catch(() => { + setFileList(prev => { + const updated = prev.map(f => f.uid === file.uid ? { ...f, status: 'error' } : f) + setFormFileValue(updated) + return updated + }) + }) + }) + } + + const previewFileList = useMemo(() => fileList.map(file => ({ + ...file, + url: file.thumbUrl || file.url || (file.originFileObj ? URL.createObjectURL(file.originFileObj) : undefined) + })), [fileList]) + + const handleDelete = (file: any) => { + const updated = fileList.filter(item => + item.thumbUrl && file.thumbUrl ? item.thumbUrl !== file.thumbUrl + : item.url && file.url ? item.url !== file.url + : item.uid !== file.uid + ) + setFileList(updated) + setFormFileValue(updated) + } + + return ( + <> + +