Merge pull request #1052 from SuanmoSuanyangTechnology/feature/single_node_run_zy
feat(web): single node run
This commit is contained in:
@@ -19,5 +19,8 @@ export default defineConfig([
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': false
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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<string, unknown>) => {
|
||||
return request.post(`/apps/${app_id}/workflow/nodes/${node_id}/run`, values)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 31</title>
|
||||
<g id="空间里层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
|
||||
<g id="应用管理-工作流-配置-开始" transform="translate(-1325, -24)" stroke="#171719" stroke-width="1.2">
|
||||
<g id="应用管理-工作流-配置-开始" transform="translate(-1325, -24)" stroke="#5B6167" stroke-width="1.2">
|
||||
<g id="运行" transform="translate(1318, 17)">
|
||||
<g id="编组-31" transform="translate(7, 7)">
|
||||
<path d="M4.5,3.55424764 L4.5,12.4457524 C4.5,12.9980371 4.94771525,13.4457524 5.5,13.4457524 C5.68741972,13.4457524 5.87106734,13.3930829 6.02999894,13.2937507 L13.1432027,8.8479983 C13.6115392,8.55528797 13.7539124,7.93833759 13.4612021,7.47000106 C13.3807214,7.34123193 13.2719718,7.2324824 13.1432027,7.1520017 L6.02999894,2.70624934 C5.56166241,2.41353901 4.94471203,2.55591217 4.6520017,3.0242487 C4.55266944,3.1831803 4.5,3.36682792 4.5,3.55424764 Z" id="路径-46"></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -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',
|
||||
|
||||
@@ -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: '情感引擎配置',
|
||||
|
||||
@@ -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<UploadProps, 'onChange'> {
|
||||
/** Custom file removal callback */
|
||||
onRemove?: (file: UploadFile) => boolean | void | Promise<boolean | void>;
|
||||
|
||||
featureConfig: FeaturesConfigForm['file_upload'];
|
||||
featureConfig?: FeaturesConfigForm['file_upload'];
|
||||
textType?: 'button' | 'text';
|
||||
block?: boolean;
|
||||
}
|
||||
@@ -184,7 +184,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
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;
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, (val: any) => 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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,9 +20,8 @@ const NodeTools: FC<{ node: Node }> = ({
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className={clsx("rb:absolute rb:p-1 rb:bg-white rb:-top-7.5 rb:right-0 rb:rounded-lg", {
|
||||
'rb:block': data.isSelected,
|
||||
'rb:hidden': !data.isSelected
|
||||
<Flex align="center" gap={8} className={clsx("rb:absolute rb:p-1! rb:bg-white rb:-top-7.5 rb:right-0 rb:rounded-lg", {
|
||||
'rb:hidden!': !data.isSelected
|
||||
})}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
@@ -36,7 +35,7 @@ const NodeTools: FC<{ node: Node }> = ({
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:hover:bg-[#F6F6F6] rb:rounded-sm rb:bg-cover rb:bg-[url(@/assets/images/common/dash.svg)]">
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<PortClickHandlerProps> = ({ graph }) => {
|
||||
if (gap < requiredSpace) {
|
||||
const shiftX = requiredSpace - gap;
|
||||
const visited = new Set<string>();
|
||||
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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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<PortClickHandlerProps> = ({ 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) => {
|
||||
|
||||
@@ -133,7 +133,7 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
|
||||
return option.dataType === currentType
|
||||
})}
|
||||
variant="borderless"
|
||||
variant="filled"
|
||||
size="small"
|
||||
className="select"
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Suggestion>
|
||||
extra?: Partial<Suggestion>,
|
||||
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[] = [];
|
||||
|
||||
@@ -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<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,9 +78,13 @@ const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
graphRef,
|
||||
chatVariables,
|
||||
blankClick
|
||||
blankClick,
|
||||
config,
|
||||
appId,
|
||||
handleSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm<NodeConfig>();
|
||||
const [configs, setConfigs] = useState<Record<string, NodeConfig>>({} as Record<string, NodeConfig>)
|
||||
const values = Form.useWatch([], form);
|
||||
@@ -530,11 +541,35 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className={clsx("rb:h-[calc(100vh-88px)] rb:w-90 rb:fixed rb:right-2.5 rb:top-18.5 rb:bottom-2.5 rb:z-1000", styles.properties)}>
|
||||
<RbCard
|
||||
title={t('workflow.nodeProperties')}
|
||||
extra={<Space>
|
||||
{!cannotRunNodes.includes(selectedNode?.data?.type) && <Popover content={t('workflow.singleRun')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
||||
<div
|
||||
className="rb:cursor-pointer rb:size-4 rb:hover:bg-[#F6F6F6] rb:rounded-sm rb:bg-cover rb:bg-[url('@/assets/images/workflow/run.svg')]"
|
||||
onClick={handleRun}
|
||||
></div>
|
||||
</Popover>}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
@@ -986,7 +1021,18 @@ const Properties: FC<PropertiesProps> = ({
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
{isRun && (
|
||||
<SingleNodeRun
|
||||
open={isRun}
|
||||
onClose={() => setIsRun(false)}
|
||||
selectedNode={selectedNode}
|
||||
appId={appId || config?.app_id || ''}
|
||||
variableList={variableList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Properties;
|
||||
@@ -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 (
|
||||
<Form.List name="context" initialValue={[JSON.stringify(defaultContextItem, null, 2)]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Flex vertical gap={8}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">{t('workflow.config.llm.context')}</div>
|
||||
<Button
|
||||
onClick={() => add(JSON.stringify(defaultContextItem, null, 2))}
|
||||
size="small"
|
||||
className="rb:text-[12px]! rb:rounded-sm!"
|
||||
>
|
||||
+ {t('common.add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
{fields.map(({ key, name }) => (
|
||||
<Flex vertical gap={4} key={key} className="rb:py-1! rb:bg-[#F6F6F6] rb:rounded-lg rb:text-[12px]">
|
||||
<Flex justify="space-between" align="center" className="rb:font-medium rb:px-2!">
|
||||
<span>JSON</span>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</Flex>
|
||||
<Form.Item name={name} noStyle>
|
||||
<CodeMirrorEditor
|
||||
language="json"
|
||||
size="small"
|
||||
variant="filled"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContextList
|
||||
134
web/src/views/Workflow/components/SingleNodeRun/FileVarInput.tsx
Normal file
134
web/src/views/Workflow/components/SingleNodeRun/FileVarInput.tsx
Normal file
@@ -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<FileVarInputProps> = ({ name, dataType, form }) => {
|
||||
const { t } = useTranslation()
|
||||
const uploadFileListModalRef = useRef<UploadFileListModalRef>(null)
|
||||
const [fileList, setFileList] = useState<any[]>([])
|
||||
|
||||
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 (
|
||||
<>
|
||||
<UploadFileListModal ref={uploadFileListModalRef} refresh={addFileList} />
|
||||
<Form.Item name={name} hidden noStyle />
|
||||
<Form.Item>
|
||||
<Row gutter={8}>
|
||||
<Col span={12}>
|
||||
<UploadFiles
|
||||
onChange={fileChange}
|
||||
block={true}
|
||||
textType="button"
|
||||
disabled={isSingle && fileList.length > 0}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Button block
|
||||
disabled={isSingle && fileList.length > 0}
|
||||
onClick={() => uploadFileListModalRef.current?.handleOpen()}>
|
||||
{t('memoryConversation.addRemoteFile')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{previewFileList.length > 0 && (
|
||||
<FileList wrap="wrap" fileList={previewFileList} onDelete={handleDelete} className="rb:mt-2!" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileVarInput
|
||||
341
web/src/views/Workflow/components/SingleNodeRun/index.tsx
Normal file
341
web/src/views/Workflow/components/SingleNodeRun/index.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-05-07 18:37:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-05-07 18:51:58
|
||||
*/
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Flex, Form, Input, InputNumber, Select, App, Checkbox } from 'antd'
|
||||
import { Node } from '@antv/x6'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { nodeRun } from '@/api/application'
|
||||
import CodeBlock from '@/components/Markdown/CodeBlock'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import styles from '../Properties/properties.module.css'
|
||||
import ContextList from './ContextList'
|
||||
import FileVarInput from './FileVarInput'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
|
||||
interface RunResult {
|
||||
status: 'completed' | 'failed' | 'running';
|
||||
node_id?: string;
|
||||
node_type?: string;
|
||||
inputs?: Record<string, any>;
|
||||
outputs?: any;
|
||||
token_usage?: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
elapsed_time?: number;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
interface SingleNodeRunProps {
|
||||
open: boolean;
|
||||
onClose: () => void
|
||||
selectedNode: Node
|
||||
appId: string
|
||||
variableList: Suggestion[]
|
||||
}
|
||||
|
||||
const SingleNodeRun: FC<SingleNodeRunProps> = ({ open, onClose, selectedNode, appId, variableList }) => {
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<RunResult | null>(null)
|
||||
|
||||
const [isAutoRun, setIsAutoRun] = useState(false)
|
||||
|
||||
const nodeData = selectedNode?.getData() || {}
|
||||
const nodeName = nodeData.name || t(`workflow.${nodeData.type}`)
|
||||
|
||||
const isLlm = nodeData.type === 'llm'
|
||||
const hasContext = isLlm && nodeData.config.context.defaultValue
|
||||
|
||||
// Recursively collect all {{nodeId.var}} references from nodeData, excluding conv. vars
|
||||
const extractVarRefs = (val: any, refs = new Set<string>()): Set<string> => {
|
||||
if (typeof val === 'string') {
|
||||
for (const m of val.matchAll(/\{\{([^}]+)\}\}/g))
|
||||
if (!m[1].startsWith('conv.') && m[1] !== 'context') {
|
||||
refs.add(m[1])
|
||||
}
|
||||
} else if (Array.isArray(val)) {
|
||||
val.forEach(v => extractVarRefs(v, refs))
|
||||
} else if (val && typeof val === 'object') {
|
||||
Object.values(val).forEach(v => extractVarRefs(v, refs))
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
const varRefs = extractVarRefs(nodeData)
|
||||
const visionInputRef = isLlm ? nodeData.config.vision_input?.defaultValue?.match(/\{\{([^}]+)\}\}/)?.[1] : undefined
|
||||
const contextInputRef = isLlm ? nodeData.config.context?.defaultValue?.match(/\{\{([^}]+)\}\}/)?.[1] : undefined
|
||||
const inputVars = variableList.filter(v => varRefs.has(v.value) && v.value !== visionInputRef && v.value !== contextInputRef)
|
||||
|
||||
|
||||
const handleRun = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const { inputs = {} } = values
|
||||
console.log('values', values)
|
||||
const params: Record<string, any> = {};
|
||||
Object.keys(inputs).forEach(key => {
|
||||
const value = inputs[key]
|
||||
|
||||
if (typeof value === 'object') {
|
||||
params[key] = value.map((file: any) => {
|
||||
if (file.url) {
|
||||
return file
|
||||
} else {
|
||||
return {
|
||||
type: file.type,
|
||||
transfer_method: 'local_file',
|
||||
upload_file_id: file.response.data.file_id
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
params[key] = value;
|
||||
}
|
||||
})
|
||||
setLoading(true)
|
||||
setResult({ status: 'running' })
|
||||
|
||||
if (hasContext) {
|
||||
const contextValues: string[] = form.getFieldValue('context') || []
|
||||
if (contextValues.length > 0) {
|
||||
params['context'] = contextValues.map(item => { try { return JSON.parse(item) } catch { return item } })
|
||||
}
|
||||
}
|
||||
|
||||
nodeRun(appId, nodeData.id, { inputs: params, stream: false })
|
||||
.then(res => {
|
||||
setResult(res as RunResult)
|
||||
})
|
||||
.catch(err => {
|
||||
setResult({ status: 'failed', error: err.message })
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = (val: string) => {
|
||||
copy(val)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
|
||||
const statusColor = result?.status === 'completed' ? '#369F21' : result?.status === 'failed' ? '#FF5D34' : '#5B6167'
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (nodeData?.type === 'iteration' || inputVars.length < 1 && !hasContext && !(isLlm && nodeData?.config?.vision?.defaultValue)) {
|
||||
setIsAutoRun(true)
|
||||
}
|
||||
}
|
||||
}, [open, inputVars, isLlm, hasContext, nodeData?.type, nodeData?.config?.vision?.defaultValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (isAutoRun) {
|
||||
handleRun()
|
||||
}
|
||||
}, [isAutoRun])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
// 与 Properties 完全相同的定位容器
|
||||
<div className={clsx('rb:h-[calc(100vh-88px)] rb:w-90 rb:absolute rb:right-0 rb:top-0 rb:bottom-2.5 rb:z-1002', styles.properties)}>
|
||||
{/* mask:仅覆盖 header 以下的区域,header 保持透明露出节点名 */}
|
||||
<div
|
||||
className="rb:absolute rb:inset-x-0 rb:bottom-0 rb:top-0 rb:rounded-xl rb:bg-[rgba(0,0,0,0.3)] rb:z-1002"
|
||||
/>
|
||||
|
||||
{/* SingleNodeRun 卡片,z-index 高于 mask */}
|
||||
<div className="rb:absolute rb:inset-x-0 rb:top-25.5 rb:bottom-0 rb:z-1003">
|
||||
<RbCard
|
||||
title={`${t('workflow.testRun')} ${nodeName}`}
|
||||
extra={
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')]"
|
||||
onClick={onClose}
|
||||
/>
|
||||
}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:font-[MiSans-Bold] rb:font-bold rb:min-h-[48px]!"
|
||||
className="rb:h-full! rb:hover:shadow-none!"
|
||||
bodyClassName="rb:overflow-y-auto! rb:h-[calc(100%-48px)]! rb:px-3! rb:pt-0! rb:pb-3!"
|
||||
>
|
||||
<Form form={form} layout="vertical" size="small" className="rb:mb-0!">
|
||||
<Flex vertical gap={12}>
|
||||
{/* Variables */}
|
||||
{nodeData?.type !== 'iteration' && inputVars.length > 0 && (
|
||||
<Flex vertical gap={8}>
|
||||
<div className="rb:text-[12px] rb:font-medium rb:text-[#5B6167]">{t('workflow.variables')}</div>
|
||||
{inputVars.map(v => (
|
||||
<Form.Item
|
||||
key={v.value}
|
||||
name={['inputs', v.value.replace('{{', '').replace('}}', '')]}
|
||||
label={v.dataType.includes('boolean')
|
||||
? null
|
||||
: <Flex gap={4} align="center" className="rb:text-[12px]">
|
||||
{v.nodeData?.icon && <div className={`rb:size-3.5 rb:bg-cover ${v.nodeData.icon}`} />}
|
||||
<span className="rb:font-medium">{v.nodeData?.name}</span>
|
||||
<span className="rb:text-[#5B6167]">/</span>
|
||||
<span className="rb:text-[#1677ff]">{v.label}</span>
|
||||
</Flex>
|
||||
}
|
||||
// rules={[{
|
||||
// required: ['knowledge-retrieval', 'loop'].includes(nodeData.type) && !v.dataType.includes('boolean'),
|
||||
// message: ['array[string]', 'array[number]'].includes(v.dataType) && Array.isArray(v.default) && v.default.length > 0 ? t('common.selectPlaceholder', { title: v.label }) : t('common.inputPlaceholder', { title: v.label })
|
||||
// }]}
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
{['array[string]', 'array[number]'].includes(v.dataType) && Array.isArray(v.default) && v.default.length > 0
|
||||
? <Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={v.default.map((item: string) => ({ label: item, value: item }))}
|
||||
/>
|
||||
: v.dataType.includes('string') && nodeData.type === 'knowledge-retrieval'
|
||||
? <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
/>
|
||||
: v.dataType.includes('string')
|
||||
? <Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
/>
|
||||
: v.dataType.includes('number')
|
||||
? <InputNumber
|
||||
size="small"
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(['retry', 'retry_interval'], value)}
|
||||
/>
|
||||
: v.dataType.includes('file')
|
||||
? <FileVarInput name={['inputs', v.value.replace('{{', '').replace('}}', '')]} dataType={v.dataType} form={form} />
|
||||
: v.dataType.includes('boolean')
|
||||
? <Checkbox>
|
||||
<Flex gap={4} align="center" className="rb:text-[12px]">
|
||||
{v.nodeData?.icon && <div className={`rb:size-3.5 rb:bg-cover ${v.nodeData.icon}`} />}
|
||||
<span className="rb:font-medium">{v.nodeData?.name}</span>
|
||||
<span className="rb:text-[#5B6167]">/</span>
|
||||
<span className="rb:text-[#1677ff]">{v.label}</span>
|
||||
</Flex>
|
||||
</Checkbox>
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
{/* Context */}
|
||||
{hasContext && <ContextList />}
|
||||
|
||||
{isLlm && nodeData?.config?.vision?.defaultValue && (() => {
|
||||
const ref = nodeData.config.vision_input?.defaultValue
|
||||
const visionVar = ref ? variableList.find(v => v.value === ref) : undefined
|
||||
const dataType = visionVar?.dataType ?? 'array[file]'
|
||||
|
||||
// if (!visionVar) return null
|
||||
console.log('visionVar', ref)
|
||||
return (
|
||||
<Form.Item
|
||||
name={['inputs', ref.replace('{{', '').replace('}}', '')]}
|
||||
label={t('workflow.config.llm.vision')}
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<FileVarInput name={['inputs', ref.replace('{{', '').replace('}}', '')]} dataType={dataType} form={form} />
|
||||
</Form.Item>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Run button */}
|
||||
{(!isAutoRun || result?.status) &&
|
||||
<Button type="primary" block onClick={handleRun} loading={!result?.status && loading} disabled={loading}>
|
||||
{result?.status ? t('workflow.reStartRun') : t('workflow.startRun')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
{/* Status row */}
|
||||
{result && (
|
||||
<div className="rb:rounded-lg rb:border rb:border-[#E8E8E8] rb:p-3 rb:bg-[#F6FFF4]">
|
||||
<Flex justify="space-between" align="start">
|
||||
<Flex vertical align="start" gap={2}>
|
||||
<span className="rb:text-[11px] rb:text-[#5B6167]">{t('workflow.status')}</span>
|
||||
<span className="rb:font-medium rb:text-[13px]" style={{ color: statusColor }}>
|
||||
{result.status?.toUpperCase()}
|
||||
</span>
|
||||
</Flex>
|
||||
<Flex vertical align="start" gap={2}>
|
||||
<span className="rb:text-[11px] rb:text-[#5B6167]">{t('workflow.elapsedTime')}</span>
|
||||
{result.elapsed_time != null && <span className="rb:font-medium rb:text-[13px]">{result.elapsed_time?.toFixed(3)}ms</span>}
|
||||
</Flex>
|
||||
<Flex vertical gap={2} align="start">
|
||||
<span className="rb:text-[11px] rb:text-[#5B6167]">{t('workflow.totalTokens')}</span>
|
||||
{!loading && <span className="rb:font-medium rb:text-[13px]">{ result?.token_usage?.total_tokens || 0} Tokens</span>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input / Output code blocks */}
|
||||
{result && (['inputs', 'process', 'outputs'] as const).map(key => {
|
||||
if (nodeData.type !== 'http-request' && key === 'process') return null
|
||||
const content = typeof result[key as keyof RunResult] === 'object' && result[key as keyof RunResult] ? JSON.stringify(result[key as keyof RunResult], null, 2) : result[key as keyof RunResult] ? result[key as keyof RunResult] : '{}'
|
||||
return (
|
||||
<div key={key} className="rb:bg-[#EBEBEB] rb:rounded-lg">
|
||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||
{t(`workflow.${key}_result`)}
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
onClick={() => handleCopy(content)}
|
||||
>{t('common.copy')}</Button>
|
||||
</div>
|
||||
<div className="rb:max-h-40 rb:overflow-auto">
|
||||
<CodeBlock
|
||||
size="small"
|
||||
value={content}
|
||||
needCopy={false}
|
||||
showLineNumbers={true}
|
||||
background="#EBEBEB"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Error */}
|
||||
{result?.error && (
|
||||
<RbAlert color="orange" className="rb:pb-0!">
|
||||
<Flex vertical className="rb:w-full!">
|
||||
<Flex align="center" justify="space-between">
|
||||
{t(`workflow.error`)}
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
onClick={() => handleCopy(result?.error || '')}
|
||||
>{t('common.copy')}</Button>
|
||||
</Flex>
|
||||
<Markdown className="rb:wrap-break-word!" content={result?.error || ''} />
|
||||
</Flex>
|
||||
</RbAlert>
|
||||
)}
|
||||
</Flex>
|
||||
</Form>
|
||||
</RbCard>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleNodeRun
|
||||
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:06:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-05-06 11:53:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-05-07 18:17:40
|
||||
*/
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import type { GroupMetadata, PortMetadata } from '@antv/x6/lib/model/port';
|
||||
@@ -16,6 +16,12 @@ import NoteNode from './components/Nodes/NoteNode';
|
||||
import { memoryConfigListUrl } from '@/api/memory';
|
||||
import type { NodeLibrary } from './types';
|
||||
|
||||
|
||||
export const cannotRunNodes = [
|
||||
'start',
|
||||
'end',
|
||||
'output',
|
||||
]
|
||||
/**
|
||||
* Workflow node library configuration
|
||||
* Defines all available node types, their icons, and configuration schemas
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:17:48
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-05-06 14:30:46
|
||||
* @Last Modified time: 2026-05-07 18:22:14
|
||||
*/
|
||||
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6';
|
||||
import { register as registerReactShape } from '@antv/x6-react-shape';
|
||||
@@ -488,6 +488,36 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
||||
}
|
||||
|
||||
// Check if loop/iteration nodes need add-node added
|
||||
const parentNodes = graphRef.current.getNodes().filter(node => {
|
||||
const type = node.getData()?.type;
|
||||
return type === 'loop' || type === 'iteration';
|
||||
});
|
||||
|
||||
parentNodes.forEach(parentNode => {
|
||||
const parentData = parentNode.getData();
|
||||
const allChildren = graphRef.current!.getNodes().filter(n => n.getData()?.cycle === parentData.id);
|
||||
const cycleStartNodes = allChildren.filter(n => n.getData()?.type === 'cycle-start');
|
||||
|
||||
// If only cycle-start exists, add add-node
|
||||
if (cycleStartNodes.length === 1 && allChildren.length === 1) {
|
||||
const cycleStartNode = cycleStartNodes[0];
|
||||
const bbox = cycleStartNode.getBBox();
|
||||
const addNode = graphRef.current!.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: bbox.x + 84,
|
||||
y: bbox.y + 4,
|
||||
data: { type: 'add-node', parentId: parentNode.id, cycle: parentData.id, label: t('workflow.addNode'), icon: '+' },
|
||||
});
|
||||
parentNode.addChild(addNode, { silent: true });
|
||||
graphRef.current!.addEdge({
|
||||
source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right' },
|
||||
target: { cell: addNode.id, port: addNode.getPorts().find(p => p.group === 'left')?.id || 'left' },
|
||||
...edgeAttrs,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
graphRef.current.centerContent()
|
||||
// Initialize after completion, display nodes in visible area
|
||||
if (nodes.length > 0 || edges.length > 0) {
|
||||
@@ -761,22 +791,11 @@ export const useWorkflowGraph = ({
|
||||
setSelectedNode(null)
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = graphRef.current?.getNodes();
|
||||
|
||||
nodes?.forEach(vo => {
|
||||
const data = vo.getData();
|
||||
if (data.isSelected) {
|
||||
vo.setData({
|
||||
...data,
|
||||
isSelected: false,
|
||||
}, { silent: true });
|
||||
}
|
||||
});
|
||||
clearNodeSelect()
|
||||
node.setData({
|
||||
...nodeData,
|
||||
isSelected: true,
|
||||
}, { silent: true });
|
||||
});
|
||||
clearEdgeSelect()
|
||||
if (nodeData.type !== 'notes') {
|
||||
setSelectedNode(node);
|
||||
@@ -805,7 +824,7 @@ export const useWorkflowGraph = ({
|
||||
node.setData({
|
||||
...data,
|
||||
isSelected: false,
|
||||
}, { silent: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
setSelectedNode(null);
|
||||
|
||||
@@ -118,6 +118,8 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
||||
parseEvent={parseEvent}
|
||||
config={config}
|
||||
chatVariables={chatVariables}
|
||||
appId={config?.app_id}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
}
|
||||
<Chat
|
||||
|
||||
Reference in New Issue
Block a user