feat(web): workflow add code node
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider, Space, Select } from 'antd';
|
||||
|
||||
interface OutputListProps {
|
||||
label: string;
|
||||
name: string;
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]',
|
||||
'object'
|
||||
]
|
||||
const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Space size={8}>
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add({ type: 'string' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-45!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'type']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-22!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 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>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default OutputList;
|
||||
@@ -0,0 +1,128 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, Space, Row, Col, Divider, Button, Tooltip } from 'antd'
|
||||
import { Node } from '@antv/x6'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import MappingList from '../MappingList'
|
||||
import Editor from '../../Editor'
|
||||
import OutputList from './OutputList'
|
||||
|
||||
interface MappingItem {
|
||||
name?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
interface CodeExecutionProps {
|
||||
options: Suggestion[]
|
||||
selectedNode: Node
|
||||
}
|
||||
|
||||
const codeTemplate = {
|
||||
python3: `def main(arg1: str, arg2: str):
|
||||
return {
|
||||
"result": arg1 + arg2,
|
||||
}`,
|
||||
javascript: `function main({arg1, arg2}) {
|
||||
return {
|
||||
result: arg1 + arg2
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const values = Form.useWatch([], form) || {}
|
||||
|
||||
const handleRefresh = () => {
|
||||
const code = form.getFieldValue('code') || ''
|
||||
const language = form.getFieldValue('language') || 'javascript'
|
||||
const currentInput = form.getFieldValue('input_variables') || []
|
||||
|
||||
// Get input_variables names to replace in code
|
||||
const inputNames = currentInput.map((item: MappingItem) => item.name).filter(Boolean).join(', ')
|
||||
|
||||
let newTemplate = code
|
||||
|
||||
if (language === 'javascript') {
|
||||
// Replace function parameters: function name({arg1, arg2}) or function name(arg1, arg2)
|
||||
newTemplate = code.replace(
|
||||
/function(\s+\w+\s*\(\s*)(\{?)([^})]*)\}?(\s*\))/,
|
||||
(_match: string, prefix: string, brace: string, _params: string, suffix: string) => {
|
||||
return `function${prefix}${brace}${inputNames}${brace ? '}' : ''}${suffix}`
|
||||
}
|
||||
)
|
||||
} else if (language === 'python3') {
|
||||
// Replace Python function parameters: def name(arg1, arg2):
|
||||
newTemplate = code.replace(
|
||||
/def(\s+\w+\s*\()([^)]*)(\))/,
|
||||
(_match: string, prefix: string, _params: string, suffix: string) => {
|
||||
return `def${prefix}${inputNames}${suffix}`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
form.setFieldValue('code', newTemplate)
|
||||
}
|
||||
const handleChangeLanguage = (value: string) => {
|
||||
form.setFieldValue('code', codeTemplate[value as keyof typeof codeTemplate])
|
||||
form.setFieldsValue({
|
||||
input_variables: [{ name: 'arg1' }, { name: 'arg2' }],
|
||||
code: codeTemplate[value as keyof typeof codeTemplate]
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="input_variables" noStyle>
|
||||
<MappingList
|
||||
label={t('workflow.config.code.input_variables')}
|
||||
name="input_variables"
|
||||
options={options}
|
||||
valueKey="variable"
|
||||
extra={<Tooltip title={t('workflow.config.code.refreshTip')}>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
className="rb:py-0! rb:px-1.5! rb:text-[12px]! rb:group"
|
||||
size="small"
|
||||
>
|
||||
<div onClick={handleRefresh} className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')] rb:group-hover:bg-[url('@/assets/images/refresh_hover.svg')]"></div>
|
||||
</Button>
|
||||
</Tooltip>}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="language" noStyle>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'PYTHON3', value: 'python3' },
|
||||
{ label: 'JAVASCRIPT', value: 'javascript' }
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:font-medium!"
|
||||
onChange={handleChangeLanguage}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="code" noStyle>
|
||||
<Editor size="small" language={values.language} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider />
|
||||
<Form.Item name="output_variables" noStyle>
|
||||
<OutputList
|
||||
label={t('workflow.config.code.output_variables')}
|
||||
name="output_variables"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeExecution
|
||||
@@ -144,6 +144,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
icon={block ? undefined : <PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
@@ -155,7 +156,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||
<AddButton block={true} />
|
||||
<AddButton block={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -196,6 +196,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||
filterBooleanType={true}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="mapping" noStyle>
|
||||
<MappingList name="mapping" options={options} />
|
||||
<MappingList label={t('workflow.config.jinja-render.mapping')} name="mapping" options={options} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="template">
|
||||
@@ -184,7 +184,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
title={t('workflow.config.jinja-render.template')}
|
||||
isArray={false}
|
||||
parentName="template"
|
||||
enableJinja2={true}
|
||||
language="jinja2"
|
||||
options={templateOptions}
|
||||
titleVariant="borderless"
|
||||
size="small"
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider } from 'antd';
|
||||
import { Button, Form, Input, Divider, Space } from 'antd';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
interface MappingListProps {
|
||||
label: string;
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
extra?: ReactNode;
|
||||
valueKey?: string;
|
||||
}
|
||||
const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueKey = 'value' }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
@@ -17,16 +20,19 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.jinja-render.mapping')}
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
<Space size={8}>
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
@@ -43,7 +49,7 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
name={[name, valueKey]}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { type FC, type ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import Editor from '../Editor'
|
||||
import Editor, { type LexicalEditorProps } from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string;
|
||||
options?: Suggestion[];
|
||||
title?: string | ReactNode;
|
||||
titleVariant?: 'outlined' | 'borderless';
|
||||
isArray?: boolean;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
language?: LexicalEditorProps['language'];
|
||||
onChange?: (value?: string) => void;
|
||||
size?: 'small' | 'default'
|
||||
}
|
||||
@@ -29,8 +29,8 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
options = [],
|
||||
language,
|
||||
size = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -81,13 +81,15 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
{typeof title === 'string'
|
||||
? <div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||
: title}
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
@@ -132,7 +134,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ const processNodeVariables = (
|
||||
if (p?.name) addVariable(variableList, addedKeys, `${dataNodeId}_${p.name}`, p.name, p.type || 'string', `${dataNodeId}.${p.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
|
||||
|
||||
case 'var-aggregator':
|
||||
if (config.group.defaultValue) {
|
||||
(config.group_variables.defaultValue || []).forEach((gv: any) => {
|
||||
@@ -106,6 +106,11 @@ const processNodeVariables = (
|
||||
if (cv.name?.trim()) addVariable(variableList, addedKeys, `${dataNodeId}_cycle_${cv.name}`, cv.name, cv.type || 'string', `${dataNodeId}.${cv.name}`, nodeData);
|
||||
});
|
||||
break;
|
||||
case 'code':
|
||||
(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);
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,9 +26,10 @@ import MemoryConfig from './MemoryConfig'
|
||||
import VariableList from './VariableList'
|
||||
import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList'
|
||||
import styles from './properties.module.css'
|
||||
import Editor from "../Editor";
|
||||
import Editor, { type LexicalEditorProps } from "../Editor";
|
||||
import RbSlider from './RbSlider'
|
||||
import JinjaRender from './JinjaRender'
|
||||
import CodeExecution from './CodeExecution'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -364,6 +365,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||
templateOptions={getFilteredVariableList(selectedNode?.data?.type, 'template')}
|
||||
/>
|
||||
: selectedNode?.data?.type === 'code'
|
||||
? <CodeExecution
|
||||
selectedNode={selectedNode}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, 'mapping')}
|
||||
/>
|
||||
: configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||
const config = configs[key] || {}
|
||||
|
||||
@@ -438,7 +444,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
language={config.language as LexicalEditorProps['language']}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
titleVariant={config.titleVariant}
|
||||
size="small"
|
||||
|
||||
@@ -87,4 +87,7 @@
|
||||
.properties :global(.ant-select .ant-select-arrow) {
|
||||
font-size: 10px;
|
||||
inset-inline-end: 6px;
|
||||
}
|
||||
.properties :global(.ant-input-sm) {
|
||||
padding: 3.6px 7px;
|
||||
}
|
||||
Reference in New Issue
Block a user