feat(web): Add Workflow
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { type FC } from 'react';
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
interface TextareaProps {
|
||||
parentName?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (value?: string) => void;
|
||||
}
|
||||
const roleOptions = [
|
||||
// { label: 'SYSTEM', value: 'SYSTEM' },
|
||||
{ label: 'USER', value: 'USER' },
|
||||
{ label: 'ASSISTANT', value: 'ASSISTANT' },
|
||||
]
|
||||
const MessageEditor: FC<TextareaProps> = ({
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
}) => {
|
||||
const form = Form.useFormInstance();
|
||||
const values = form.getFieldsValue()
|
||||
|
||||
const handleAdd = (add: FormListOperation['add']) => {
|
||||
const list = values[parentName];
|
||||
const lastRole = list[list.length - 1].role
|
||||
|
||||
add({
|
||||
role: lastRole === 'USER' ? 'ASSISTANT' : 'USER',
|
||||
content: undefined
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const currentRole = values[parentName]?.[key].role || 'USER'
|
||||
|
||||
return (
|
||||
<Space key={key} 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
|
||||
{...restField}
|
||||
name={[name, 'role']}
|
||||
noStyle
|
||||
>
|
||||
{currentRole === 'SYSTEM'
|
||||
? <Input disabled />
|
||||
:
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{currentRole !== 'SYSTEM' && <Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</div>
|
||||
</Col>}
|
||||
</Row>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'content']}
|
||||
noStyle
|
||||
>
|
||||
<Input.TextArea placeholder={placeholder} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)
|
||||
})}
|
||||
<Form.Item className="rb:mt-3!">
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block icon={<PlusOutlined />}>
|
||||
Add field
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageEditor;
|
||||
@@ -0,0 +1,180 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Checkbox, Tag } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { StartVariableItem, VariableEditModalRef } from '../../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import SortableList from '@/components/SortableList'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface VariableEditModalProps {
|
||||
refresh: (values: StartVariableItem) => void;
|
||||
}
|
||||
|
||||
const types = [
|
||||
'string',
|
||||
'number',
|
||||
'boolean',
|
||||
// 'array',
|
||||
// 'object'
|
||||
]
|
||||
const variableType = {
|
||||
string: 'string',
|
||||
number: 'number',
|
||||
boolean: 'boolean',
|
||||
// array: 'array',
|
||||
// object: 'object',
|
||||
}
|
||||
|
||||
const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<StartVariableItem>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<StartVariableItem | null>(null)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setEditVo(null)
|
||||
};
|
||||
|
||||
const handleOpen = (variable?: StartVariableItem) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
setEditVo(variable || null)
|
||||
form.setFieldsValue(variable)
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields().then((values) => {
|
||||
refresh({
|
||||
...(editVo || {}),
|
||||
...values,
|
||||
})
|
||||
handleClose()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
// 变量类型改变时,更新初始化其他字段值
|
||||
const handleChangeType = (value: StartVariableItem['type']) => {
|
||||
if (value) {
|
||||
form.setFieldsValue({
|
||||
type: value,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
max_length: undefined,
|
||||
default: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.start.addVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* 变量类型 */}
|
||||
<FormItem
|
||||
name="type"
|
||||
label={t('workflow.config.start.variableType')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.start.${key}`),
|
||||
}))}
|
||||
onChange={handleChangeType}
|
||||
labelRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
|
||||
optionRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* 变量名称 */}
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('workflow.config.start.variableName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('workflow.config.start.invalidVariableName') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
{/* 显示名称 */}
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('workflow.config.start.description')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
{/* 最大长度 */}
|
||||
{['string'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="max_length"
|
||||
label={t('workflow.config.start.max_length')}
|
||||
>
|
||||
<InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 默认值 */}
|
||||
{['string', 'number', 'boolean'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="default"
|
||||
label={t('workflow.config.start.default')}
|
||||
>
|
||||
{['string'].includes(values.type) && <Input placeholder={t('common.enter')} />}
|
||||
{['number'].includes(values.type) && <InputNumber placeholder={t('common.enter')} style={{ width: '100%' }} />}
|
||||
{['boolean'].includes(values.type) && <Select placeholder={t('common.pleaseSelect')} options={[{ value: true, label: t('workflow.config.start.defaultChecked') }, { value: false, label: t('workflow.config.start.notDefaultChecked') }]} />}
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 选项 */}
|
||||
{['array'].includes(values?.type) && (
|
||||
<FormItem
|
||||
name="options"
|
||||
label={t('workflow.config.start.options')}
|
||||
>
|
||||
<SortableList />
|
||||
</FormItem>
|
||||
)}
|
||||
{/* 是否必填 */}
|
||||
<FormItem
|
||||
name="required"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>{t('workflow.config.start.required')}</Checkbox>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default VariableEditModal;
|
||||
231
web/src/views/Workflow/components/Properties/index.tsx
Normal file
231
web/src/views/Workflow/components/Properties/index.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { type FC, useEffect, useState, useRef } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App } from 'antd'
|
||||
|
||||
import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef } from '../../types'
|
||||
import Empty from '@/components/Empty';
|
||||
import emptyIcon from '@/assets/images/workflow/empty.png'
|
||||
import CustomSelect from "@/components/CustomSelect";
|
||||
import VariableEditModal from './VariableEditModal';
|
||||
import MessageEditor from './MessageEditor'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
setSelectedNode: (node: Node | null) => void;
|
||||
graphRef: React.MutableRefObject<Graph | undefined>;
|
||||
blankClick: () => void;
|
||||
deleteEvent: () => void;
|
||||
copyEvent: () => void;
|
||||
parseEvent: () => void;
|
||||
}
|
||||
const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
const [form] = Form.useForm<NodeConfig>();
|
||||
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
|
||||
const values = Form.useWatch([], form);
|
||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNode && form) {
|
||||
const { type = 'default', name = '', config } = selectedNode.getData() || {}
|
||||
const initialValue: Record<string, any> = {}
|
||||
Object.keys(config || {}).forEach(key => {
|
||||
if (config && config[key] && 'defaultValue' in config[key]) {
|
||||
initialValue[key] = config[key].defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
form.setFieldsValue({
|
||||
type,
|
||||
id: selectedNode.id,
|
||||
name,
|
||||
...initialValue,
|
||||
})
|
||||
setConfigs(config || {})
|
||||
}
|
||||
}, [selectedNode, form])
|
||||
|
||||
const updateNodeLabel = (newLabel: string) => {
|
||||
if (selectedNode && form) {
|
||||
const nodeData = selectedNode.data as NodeProperties;
|
||||
selectedNode.setAttrByPath('text/text', `${nodeData.icon} ${newLabel}`);
|
||||
selectedNode.setData({ ...selectedNode.data, name: newLabel });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (values && selectedNode) {
|
||||
const { id, ...rest } = values
|
||||
|
||||
Object.keys(values).forEach(key => {
|
||||
if (selectedNode.data.config[key]) {
|
||||
selectedNode.data.config[key].defaultValue = values[key]
|
||||
}
|
||||
})
|
||||
|
||||
selectedNode?.setData({ ...selectedNode.data, ...rest })
|
||||
}
|
||||
}, [values, selectedNode])
|
||||
|
||||
const handleAddVariable = () => {
|
||||
variableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEditVariable = (index: number, vo: StartVariableItem) => {
|
||||
variableModalRef.current?.handleOpen(vo)
|
||||
setEditIndex(index)
|
||||
}
|
||||
const handleRefreshVariable = (value: StartVariableItem) => {
|
||||
if (!selectedNode) return
|
||||
if (editIndex !== null) {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
defaultValue[editIndex] = value
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue]
|
||||
} else {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue, value]
|
||||
}
|
||||
selectedNode?.setData({ ...selectedNode.data})
|
||||
|
||||
setConfigs({ ...selectedNode.data.config})
|
||||
}
|
||||
const handleDeleteVariable = (index: number, vo: StartVariableItem) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
defaultValue.splice(index, 1)
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue]
|
||||
|
||||
selectedNode?.setData({ ...selectedNode.data })
|
||||
|
||||
setConfigs({ ...selectedNode.data.config })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-3">{t('workflow.nodeProperties')}</div>
|
||||
{!selectedNode
|
||||
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} />
|
||||
: <Form form={form} layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-y-auto">
|
||||
<Form.Item name="name" label={t('workflow.nodeName')}>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
onChange={(e) => {
|
||||
updateNodeLabel(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{configs && Object.keys(configs).length > 0 && Object.keys(configs).map((key) => {
|
||||
const config = configs[key] || {}
|
||||
|
||||
if (selectedNode.data.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
|
||||
<div className="rb:leading-5">
|
||||
{t(`workflow.config.${selectedNode.data.type}.${key}`)}
|
||||
</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddVariable}>+{t('application.addVariables')}</Button>
|
||||
</div>
|
||||
|
||||
<Space size={4} direction="vertical" className="rb:w-full">
|
||||
{Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) =>
|
||||
<div key={`${vo.name}}-${index}`} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md rb:group rb:cursor-pointer">
|
||||
<span>{vo.name}·{vo.description}</span>
|
||||
|
||||
<div className="rb:group-hover:hidden rb:flex rb:items-center rb:gap-1">
|
||||
{vo.required && <span>{t('workflow.config.start.required')}</span>}
|
||||
{vo.type}
|
||||
</div>
|
||||
<Space className="rb:hidden! rb:group-hover:flex!">
|
||||
<div
|
||||
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditVariable(index, vo)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteVariable(index, vo)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<Divider size="small" />
|
||||
{config.sys?.map((vo, index) =>
|
||||
<div key={index} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div>
|
||||
<span>sys.{vo.name}</span>
|
||||
</div>
|
||||
{vo.type}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedNode.data.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
if (config.type === 'define') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
label={t(`workflow.config.${selectedNode.data.type}.${key}`)}
|
||||
>
|
||||
{config.type === 'input'
|
||||
? <Input placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'textarea'
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
? <InputNumber />
|
||||
: config.type === 'slider'
|
||||
? <Slider min={config.min} max={config.max} step={config.step} />
|
||||
: config.type === 'customSelect'
|
||||
? <CustomSelect
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
hasAll={false}
|
||||
valueKey={config.valueKey}
|
||||
labelKey={config.labelKey}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
</Form>
|
||||
}
|
||||
|
||||
<VariableEditModal
|
||||
ref={variableModalRef}
|
||||
refresh={handleRefreshVariable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Properties;
|
||||
Reference in New Issue
Block a user