feat(web): add loop node; add chat variable;

This commit is contained in:
zhaoying
2026-01-04 20:00:10 +08:00
parent 4e3b8870c5
commit a66fb9eade
29 changed files with 1453 additions and 279 deletions

View File

@@ -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>
)
}}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>