feat(web): workflow support lexical editor

This commit is contained in:
zhaoying
2025-12-23 16:22:51 +08:00
parent 7d40d06b69
commit 26263bdcf0
11 changed files with 607 additions and 54 deletions

View File

@@ -1,13 +1,20 @@
import { type FC } from 'react';
import { type FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Graph, Node } from '@antv/x6';
import Editor from '../Editor'
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
interface TextareaProps {
isArray?: boolean;
parentName?: string;
label?: string;
placeholder?: string;
value?: string;
onChange?: (value?: string) => void;
selectedNode?: Node | null;
graphRef?: React.MutableRefObject<Graph | undefined>;
}
const roleOptions = [
// { label: 'SYSTEM', value: 'SYSTEM' },
@@ -15,11 +22,92 @@ const roleOptions = [
{ label: 'ASSISTANT', value: 'ASSISTANT' },
]
const MessageEditor: FC<TextareaProps> = ({
isArray = true,
parentName = 'messages',
placeholder,
selectedNode,
graphRef,
}) => {
const { t } = useTranslation()
const form = Form.useFormInstance();
const values = form.getFieldsValue()
const suggestions = useMemo(() => {
if (!selectedNode || !graphRef?.current) return [];
const suggestions: Suggestion[] = [];
const graph = graphRef.current;
const edges = graph.getEdges();
const nodes = graph.getNodes();
// Find all connected previous nodes (recursive)
const getAllPreviousNodes = (nodeId: string, visited = new Set<string>()): string[] => {
if (visited.has(nodeId)) return [];
visited.add(nodeId);
const directPrevious = edges
.filter(edge => edge.getTargetCellId() === nodeId)
.map(edge => edge.getSourceCellId());
const allPrevious = [...directPrevious];
directPrevious.forEach(prevNodeId => {
allPrevious.push(...getAllPreviousNodes(prevNodeId, visited));
});
return allPrevious;
};
const allPreviousNodeIds = getAllPreviousNodes(selectedNode.id);
console.log('allPreviousNodeIds', allPreviousNodeIds)
allPreviousNodeIds.forEach(nodeId => {
const node = nodes.find(n => n.id === nodeId);
if (!node) return;
const nodeData = node.getData();
switch(nodeData.type) {
case 'start':
const list = [
...(nodeData.config?.variables?.defaultValue ?? []),
...(nodeData.config?.variables?.value ?? [])
]
list.forEach((variable: any) => {
suggestions.push({
key: `${nodeId}_${variable.name}`,
label: variable.name,
type: 'variable',
dataType: variable.type,
value: `${nodeId}.${variable.name}`,
nodeData: nodeData,
});
});
nodeData.config?.variables?.sys.forEach((variable: any) => {
suggestions.push({
key: `${nodeId}_${variable.name}`,
label: variable.name,
type: 'variable',
dataType: variable.type,
value: `sys.${variable.name}`,
nodeData: nodeData,
});
});
break
case 'llm':
suggestions.push({
key: `${nodeId}_output`,
label: 'output',
type: 'variable',
dataType: 'String',
value: `${nodeId}.output`,
nodeData: nodeData,
});
break
}
});
return suggestions;
}, [selectedNode, graphRef]);
const handleAdd = (add: FormListOperation['add']) => {
const list = values[parentName];
@@ -30,58 +118,74 @@ const MessageEditor: FC<TextareaProps> = ({
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>
{isArray
? <Form.List name={parentName}>
{(fields, { add, remove }) => (
<Space size={12} direction="vertical" className="rb:w-full">
{fields.map(({ key, name, ...restField }) => {
const currentRole = values[parentName]?.[key].role || 'USER'
return (
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
<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
>
<Editor placeholder={placeholder} suggestions={suggestions} />
</Form.Item>
</Space>
)
})}
<Form.Item>
<Button type="dashed" onClick={() => handleAdd(add)} block icon={<PlusOutlined />}>
+{t('workflow.addMessage')}
</Button>
</Form.Item>
</Space >
)}
</Form.List>
:
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
<Row>
<Col span={12}>
{t('workflow.answerDesc')}
</Col>
</Row>
<Form.Item
name={parentName}
noStyle
>
<Editor placeholder={placeholder} suggestions={suggestions} />
</Form.Item>
</Space>
}
</div>
);
};

View File

@@ -21,6 +21,7 @@ interface PropertiesProps {
}
const Properties: FC<PropertiesProps> = ({
selectedNode,
graphRef,
}) => {
const { t } = useTranslation()
const { modal } = App.useApp()
@@ -60,9 +61,11 @@ const Properties: FC<PropertiesProps> = ({
};
useEffect(() => {
console.log('values', values)
if (values && selectedNode) {
const { id, ...rest } = values
Object.keys(values).forEach(key => {
if (selectedNode.data?.config[key]) {
selectedNode.data.config[key].defaultValue = values[key]
@@ -181,7 +184,14 @@ const Properties: FC<PropertiesProps> = ({
if (selectedNode.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
return (
<Form.Item key={key} name={key}>
<MessageEditor />
<MessageEditor selectedNode={selectedNode} graphRef={graphRef} />
</Form.Item>
)
}
if (selectedNode.data?.type === 'end' && key === 'output') {
return (
<Form.Item key={key} name={key}>
<MessageEditor isArray={false} parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
</Form.Item>
)
}