feat(web): add loop node; add chat variable;
This commit is contained in:
@@ -9,8 +9,8 @@ import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void;
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>;
|
||||
onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; comparison_operator: string; right: string; }[] }>) => void;
|
||||
options: Suggestion[];
|
||||
name: string;
|
||||
selectedNode?: any;
|
||||
@@ -221,7 +221,7 @@ const CaseList: FC<CaseListProps> = ({
|
||||
onClick={() => addCondition()}
|
||||
size="small"
|
||||
>
|
||||
+ 添加条件
|
||||
+ {t('workflow.config.addCase')}
|
||||
</Button>
|
||||
{caseFields.length > 1 && <DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
@@ -229,7 +229,8 @@ const CaseList: FC<CaseListProps> = ({
|
||||
/>}
|
||||
</Space>
|
||||
</div>
|
||||
{conditionFields?.length > 1 && <>
|
||||
{conditionFields?.length > 1 &&
|
||||
<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
@@ -238,50 +239,56 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{conditionFields.map((conditionField, conditionIndex) => (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
{conditionFields.map((conditionField, conditionIndex) => {
|
||||
const currentOperator = value?.[caseIndex]?.expressions?.[conditionIndex]?.comparison_operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
|
||||
return (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'comparison_operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!hideRightField && (
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
placeholder="包含"
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -24,7 +24,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName }) => {
|
||||
return (
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-[#F8F9FB]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-1">
|
||||
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface Case {
|
||||
logical_operator: 'and' | 'or';
|
||||
expressions: Array<{ left: string; comparison_operator: string; right: string; }>
|
||||
}
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Case;
|
||||
onChange?: (value: Case) => void;
|
||||
options: Suggestion[];
|
||||
parentName: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
addBtnText?: string;
|
||||
}
|
||||
const operatorList = [
|
||||
"empty",
|
||||
"not_empty",
|
||||
"contains",
|
||||
"not_contains",
|
||||
"startwith",
|
||||
"endwith",
|
||||
"eq",
|
||||
"ne",
|
||||
"lt",
|
||||
"le",
|
||||
"gt",
|
||||
"ge"
|
||||
]
|
||||
|
||||
const ConditionList: FC<CaseListProps> = ({
|
||||
value,
|
||||
options,
|
||||
parentName,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChangeLogicalOperator = () => {
|
||||
if (!value) return;
|
||||
onChange && onChange({
|
||||
logical_operator: value.logical_operator === 'and' ? 'or' : 'and',
|
||||
expressions: value.expressions || []
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[parentName, 'expressions']}>
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
<div className="rb:relative">
|
||||
{fields.map((field, index) => {
|
||||
const currentOperator = value?.expressions?.[index]?.comparison_operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:mb-3">
|
||||
{index > 0 && (<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{value?.logical_operator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6">
|
||||
<Row gutter={8} align="middle">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder="输入值"
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Form.Item name={[field.name, 'comparison_operator']} noStyle>
|
||||
<Select
|
||||
placeholder="包含"
|
||||
options={operatorList.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.if-else.${key}`)
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{!hideRightField && (
|
||||
<Col span={24}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
<Editor options={options} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({ left: '', comparison_operator: '', right: '' })}
|
||||
className="rb:w-full rb:ml-6 rb:mt-2"
|
||||
icon={<span>+</span>}
|
||||
>
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConditionList
|
||||
@@ -0,0 +1,172 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Row, Col, Input } from 'antd'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface CycleVar {
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
input_type: string;
|
||||
}
|
||||
|
||||
interface CycleVarsListProps {
|
||||
value?: CycleVar[];
|
||||
onChange?: (value: CycleVar[]) => void;
|
||||
options: Suggestion[];
|
||||
parentName: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
'array[string]',
|
||||
'array[number]',
|
||||
'array[boolean]',
|
||||
'array[object]'
|
||||
]
|
||||
|
||||
const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
value = [],
|
||||
options,
|
||||
parentName,
|
||||
onChange,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
// 获取循环节点的子节点变量
|
||||
const getChildNodeVariables = () => {
|
||||
if (!selectedNode || !graphRef?.current || selectedNode.getData()?.type !== 'loop') {
|
||||
return options;
|
||||
}
|
||||
|
||||
const loopNodeId = selectedNode.getData()?.id;
|
||||
const childNodes = graphRef.current.getNodes().filter((node: any) =>
|
||||
node.getData()?.cycle === loopNodeId
|
||||
);
|
||||
|
||||
const childVariables: Suggestion[] = [];
|
||||
childNodes.forEach((childNode: any) => {
|
||||
const childData = childNode.getData();
|
||||
if (childData?.config) {
|
||||
Object.keys(childData.config).forEach(key => {
|
||||
if (childData.config[key]?.defaultValue) {
|
||||
childVariables.push({
|
||||
key: `${childData.id}.${key}`,
|
||||
label: `${childData.name || childData.type}.${key}`,
|
||||
type: 'output',
|
||||
dataType: 'string',
|
||||
value: `{{${childData.id}.${key}}}`,
|
||||
nodeData: childData
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...options, ...childVariables];
|
||||
};
|
||||
|
||||
const availableOptions = getChildNodeVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
||||
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" />
|
||||
</div>
|
||||
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...field }, index) => {
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-3 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white">
|
||||
<Row gutter={8} align="middle" className="rb:mb-2">
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input placeholder="变量名" size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder="Constant"
|
||||
options={[
|
||||
{ label: 'Constant', value: 'constant' },
|
||||
{ label: 'Variable', value: 'variable' }
|
||||
]}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
// 重置 value 字段
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name={[name, 'value']} noStyle>
|
||||
{currentInputType === 'variable' ? (
|
||||
<VariableSelect
|
||||
placeholder="选择变量"
|
||||
options={availableOptions}
|
||||
/>
|
||||
) : (
|
||||
<Input.TextArea
|
||||
placeholder="输入值"
|
||||
rows={3}
|
||||
className="rb:w-full"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })}
|
||||
className="rb:w-full"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
添加变量
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CycleVarsList
|
||||
@@ -26,7 +26,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
const labelRender: LabelRender = (props) => {
|
||||
const { value } = props
|
||||
const filterOption = options.find(vo => vo.value === value)
|
||||
const filterOption = options.find(vo => `{{${vo.value}}}` === value)
|
||||
|
||||
if (filterOption) {
|
||||
return (
|
||||
@@ -62,8 +62,10 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({
|
||||
label: suggestions[0].nodeData.name,
|
||||
options: suggestions.map(s => ({ label: s.label, value: s.value }))
|
||||
options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` }))
|
||||
}));
|
||||
|
||||
console.log('groupedOptions', groupedOptions)
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -18,6 +18,8 @@ import CaseList from './CaseList'
|
||||
import HttpRequest from './HttpRequest';
|
||||
import MappingList from './MappingList'
|
||||
import CategoryList from './CategoryList'
|
||||
import ConditionList from './ConditionList'
|
||||
import CycleVarsList from './CycleVarsList'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -27,10 +29,12 @@ interface PropertiesProps {
|
||||
deleteEvent: () => void;
|
||||
copyEvent: () => void;
|
||||
parseEvent: () => void;
|
||||
config?: any;
|
||||
}
|
||||
const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
graphRef,
|
||||
config,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
@@ -255,6 +259,25 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
// Add conversation variables from global config
|
||||
const conversationVariables = config?.variables || [];
|
||||
|
||||
conversationVariables.forEach((variable: any) => {
|
||||
const key = `CONVERSATION_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `conversation.${variable.name}`,
|
||||
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
|
||||
group: 'CONVERSATION'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return variableList;
|
||||
}, [selectedNode, graphRef]);
|
||||
|
||||
@@ -417,7 +440,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
)
|
||||
}
|
||||
if (config.type === 'caseList') {
|
||||
console.log('key', key)
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CaseList
|
||||
@@ -440,6 +462,16 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'cycleVarsList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
@@ -473,15 +505,20 @@ const Properties: FC<PropertiesProps> = ({
|
||||
: config.type === 'variableList'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={variableList.map(vo => ({
|
||||
...vo,
|
||||
value: `{{${vo.value}}}`
|
||||
}))}
|
||||
options={variableList}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList parentName={key} />
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
parentName={key}
|
||||
options={variableList}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
Reference in New Issue
Block a user