/* * @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; 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 = ({ 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(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()): Set => { 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 = {}; 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 完全相同的定位容器
{/* mask:仅覆盖 header 以下的区域,header 保持透明露出节点名 */}
{/* SingleNodeRun 卡片,z-index 高于 mask */}
} 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!" >
{/* Variables */} {nodeData?.type !== 'iteration' && inputVars.length > 0 && (
{t('workflow.variables')}
{inputVars.map(v => ( {v.nodeData?.icon &&
} {v.nodeData?.name} / {v.label} } // 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 ? : v.dataType.includes('number') ? form.setFieldValue(['retry', 'retry_interval'], value)} /> : v.dataType.includes('file') ? : v.dataType.includes('boolean') ? {v.nodeData?.icon &&
} {v.nodeData?.name} / {v.label} : null } ))} )} {/* Context */} {hasContext && } {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 ( ) })()} {/* Run button */} {(!isAutoRun || result?.status) && } {/* Status row */} {result && (
{t('workflow.status')} {result.status?.toUpperCase()} {t('workflow.elapsedTime')} {result.elapsed_time != null && {result.elapsed_time?.toFixed(3)}ms} {t('workflow.totalTokens')} {!loading && { result?.token_usage?.total_tokens || 0} Tokens}
)} {/* 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 (
{t(`workflow.${key}_result`)}
) })} {/* Error */} {result?.error && ( {t(`workflow.error`)} )}
) } export default SingleNodeRun