feat(web): 完成循环节点的基本配置
This commit is contained in:
@@ -28,6 +28,7 @@ export const sourceNodeNotFoundError = () => new CheckError(200, '连线起始
|
|||||||
export const targetNodeNotFoundError = () => new CheckError(201, '连线目标节点未找到')
|
export const targetNodeNotFoundError = () => new CheckError(201, '连线目标节点未找到')
|
||||||
export const nodeToSelfError = () => new CheckError(203, '节点不能直连自身')
|
export const nodeToSelfError = () => new CheckError(203, '节点不能直连自身')
|
||||||
export const hasCycleError = () => new CheckError(204, '禁止流程循环')
|
export const hasCycleError = () => new CheckError(204, '禁止流程循环')
|
||||||
|
export const differentParent = () => new CheckError(205, '子流程禁止连接外部节点')
|
||||||
|
|
||||||
const hasCycle = (sourceNode: Node, targetNode: Node, nodes: Node[], edges: Edge[], visited = new Set<string>()) => {
|
const hasCycle = (sourceNode: Node, targetNode: Node, nodes: Node[], edges: Edge[], visited = new Set<string>()) => {
|
||||||
if (visited.has(targetNode.id)) return false
|
if (visited.has(targetNode.id)) return false
|
||||||
@@ -48,6 +49,10 @@ export const checkAddConnection: (connection: Connection, nodes: Node[], edges:
|
|||||||
throw targetNodeNotFoundError()
|
throw targetNodeNotFoundError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isEqual(sourceNode.parentId, targetNode.parentId)) {
|
||||||
|
throw differentParent()
|
||||||
|
}
|
||||||
|
|
||||||
// 禁止流程出现环,必须是有向无环图
|
// 禁止流程出现环,必须是有向无环图
|
||||||
if (isEqual(sourceNode.id, targetNode.id)) {
|
if (isEqual(sourceNode.id, targetNode.id)) {
|
||||||
throw nodeToSelfError()
|
throw nodeToSelfError()
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import styled from 'styled-components'
|
|||||||
import '@xyflow/react/dist/style.css'
|
import '@xyflow/react/dist/style.css'
|
||||||
import {commonInfo} from '../../util/amis.tsx'
|
import {commonInfo} from '../../util/amis.tsx'
|
||||||
import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx'
|
import {checkAddConnection, checkAddNode, checkSave} from './FlowChecker.tsx'
|
||||||
|
import {useNodeDrag} from './Helper.tsx'
|
||||||
import {NodeRegistry, NodeRegistryMap} from './NodeRegistry.tsx'
|
import {NodeRegistry, NodeRegistryMap} from './NodeRegistry.tsx'
|
||||||
import {useContextStore} from './store/ContextStore.ts'
|
import {useContextStore} from './store/ContextStore.ts'
|
||||||
import {useDataStore} from './store/DataStore.ts'
|
import {useDataStore} from './store/DataStore.ts'
|
||||||
import {useFlowStore} from './store/FlowStore.ts'
|
import {useFlowStore} from './store/FlowStore.ts'
|
||||||
import type {FlowEditorProps} from './types.ts'
|
import {flowDotColor, type FlowEditorProps} from './types.ts'
|
||||||
|
|
||||||
const FlowableDiv = styled.div`
|
const FlowableDiv = styled.div`
|
||||||
.react-flow__node.selectable {
|
.react-flow__node.selectable {
|
||||||
@@ -75,10 +76,17 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
setInputSchema(props.inputSchema)
|
setInputSchema(props.inputSchema)
|
||||||
}, [props.graphData])
|
}, [props.graphData])
|
||||||
|
|
||||||
|
const {
|
||||||
|
onNodeDragStart,
|
||||||
|
onNodeDrag,
|
||||||
|
onNodeDragEnd,
|
||||||
|
} = useNodeDrag([props.graphData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlowableDiv className="h-full w-full">
|
<FlowableDiv className="h-full w-full">
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
className="rounded-xl"
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
@@ -97,6 +105,9 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
}}
|
}}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
nodeTypes={arrToMap(Object.keys(NodeRegistryMap), key => NodeRegistryMap[key]!.component)}
|
nodeTypes={arrToMap(Object.keys(NodeRegistryMap), key => NodeRegistryMap[key]!.component)}
|
||||||
|
onNodeDragStart={onNodeDragStart}
|
||||||
|
onNodeDrag={onNodeDrag}
|
||||||
|
onNodeDragStop={onNodeDragEnd}
|
||||||
>
|
>
|
||||||
<Panel position="top-right">
|
<Panel position="top-right">
|
||||||
<Space className="toolbar">
|
<Space className="toolbar">
|
||||||
@@ -176,7 +187,12 @@ function FlowEditor(props: FlowEditorProps) {
|
|||||||
</Panel>
|
</Panel>
|
||||||
<Controls/>
|
<Controls/>
|
||||||
<MiniMap/>
|
<MiniMap/>
|
||||||
<Background variant={BackgroundVariant.Cross} gap={20} size={3}/>
|
<Background
|
||||||
|
variant={BackgroundVariant.Cross}
|
||||||
|
gap={20}
|
||||||
|
size={3}
|
||||||
|
color={flowDotColor}
|
||||||
|
/>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</FlowableDiv>
|
</FlowableDiv>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import {type Edge, getIncomers, type Node} from '@xyflow/react'
|
import {type Edge, getIncomers, type Node} from '@xyflow/react'
|
||||||
import type {Option} from 'amis/lib/Schema'
|
import type {Option} from 'amis/lib/Schema'
|
||||||
import {find, has, isEmpty, isEqual, unique} from 'licia'
|
import {find, has, isEmpty, isEqual, max, min, unique} from 'licia'
|
||||||
|
import {type DependencyList, type MouseEvent as ReactMouseEvent, useCallback, useRef} from 'react'
|
||||||
import Queue from 'yocto-queue'
|
import Queue from 'yocto-queue'
|
||||||
|
import {useFlowStore} from './store/FlowStore.ts'
|
||||||
import type {InputFormOptions, InputFormOptionsGroup} from './types.ts'
|
import type {InputFormOptions, InputFormOptionsGroup} from './types.ts'
|
||||||
|
|
||||||
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
export const getAllIncomerNodeById: (id: string, nodes: Node[], edges: Edge[]) => string[] = (id, nodes, edges) => {
|
||||||
@@ -82,3 +84,43 @@ export const generateAllIncomerOutputVariablesFormOptions: (id: string, inputSch
|
|||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理循环节点的边界问题
|
||||||
|
export const useNodeDrag = (deps: DependencyList) => {
|
||||||
|
const currentPosition = useRef({x: 0, y: 0} as { x: number, y: number })
|
||||||
|
const {setNode, getNodeById} = useFlowStore()
|
||||||
|
|
||||||
|
const onNodeDragStart = useCallback(() => {
|
||||||
|
}, deps)
|
||||||
|
const onNodeDrag = useCallback((event: ReactMouseEvent, node: Node) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (node.parentId) {
|
||||||
|
let parentNode = getNodeById(node.parentId)
|
||||||
|
if (parentNode) {
|
||||||
|
let newPosition = {
|
||||||
|
x: max(min(node.position.x, (parentNode.measured?.width ?? 0) - (node.measured?.width ?? 0) - 28), 28),
|
||||||
|
y: max(min(node.position.y, (parentNode.measured?.height ?? 0) - (node.measured?.height ?? 0) - 28), 90),
|
||||||
|
}
|
||||||
|
setNode({
|
||||||
|
...node,
|
||||||
|
position: newPosition,
|
||||||
|
})
|
||||||
|
currentPosition.current = newPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
const onNodeDragEnd = useCallback((_event: ReactMouseEvent, node: Node) => {
|
||||||
|
if (node.parentId) {
|
||||||
|
setNode({
|
||||||
|
...node,
|
||||||
|
position: currentPosition.current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
|
||||||
|
return {
|
||||||
|
onNodeDragStart,
|
||||||
|
onNodeDrag,
|
||||||
|
onNodeDragEnd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {has, isEmpty} from 'licia'
|
import {has, isEmpty} from 'licia'
|
||||||
import type {JSX} from 'react'
|
|
||||||
import {getAllIncomerNodeOutputVariables} from './Helper.tsx'
|
import {getAllIncomerNodeOutputVariables} from './Helper.tsx'
|
||||||
import CodeNode from './node/CodeNode.tsx'
|
import CodeNode from './node/CodeNode.tsx'
|
||||||
import KnowledgeNode from './node/KnowledgeNode.tsx'
|
import KnowledgeNode from './node/KnowledgeNode.tsx'
|
||||||
import LlmNode from './node/LlmNode.tsx'
|
import LlmNode from './node/LlmNode.tsx'
|
||||||
|
import LoopNode from './node/LoopNode.tsx'
|
||||||
import OutputNode from './node/OutputNode.tsx'
|
import OutputNode from './node/OutputNode.tsx'
|
||||||
import SwitchNode from './node/SwitchNode.tsx'
|
import SwitchNode from './node/SwitchNode.tsx'
|
||||||
import TemplateNode from './node/TemplateNode.tsx'
|
import TemplateNode from './node/TemplateNode.tsx'
|
||||||
import type {NodeChecker} from './types.ts'
|
import type {NodeChecker, NodeDefine} from './types.ts'
|
||||||
|
|
||||||
const inputSingleVariableChecker: (field: string) => NodeChecker = field => {
|
const inputSingleVariableChecker: (field: string) => NodeChecker = field => {
|
||||||
return (id, inputSchema, nodes, edges, data) => {
|
return (id, inputSchema, nodes, edges, data) => {
|
||||||
@@ -54,16 +54,6 @@ const inputMultiVariableChecker: NodeChecker = (id, inputSchema, nodes, edges, d
|
|||||||
return {error: false}
|
return {error: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeDefine = {
|
|
||||||
key: string,
|
|
||||||
group: string,
|
|
||||||
name: string,
|
|
||||||
icon: JSX.Element,
|
|
||||||
description: string,
|
|
||||||
component: any,
|
|
||||||
checkers: NodeChecker[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NodeRegistry: NodeDefine[] = [
|
export const NodeRegistry: NodeDefine[] = [
|
||||||
{
|
{
|
||||||
key: 'llm-node',
|
key: 'llm-node',
|
||||||
@@ -110,6 +100,15 @@ export const NodeRegistry: NodeDefine[] = [
|
|||||||
component: SwitchNode,
|
component: SwitchNode,
|
||||||
checkers: [],
|
checkers: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'loop-node',
|
||||||
|
group: '逻辑节点',
|
||||||
|
name: '循环',
|
||||||
|
icon: <i className="fa fa-repeat"/>,
|
||||||
|
description: '实现循环执行流程',
|
||||||
|
component: LoopNode,
|
||||||
|
checkers: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'output-node',
|
key: 'output-node',
|
||||||
group: '输出节点',
|
group: '输出节点',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons'
|
import {CopyFilled, DeleteFilled, EditFilled} from '@ant-design/icons'
|
||||||
import {type Edge, Handle, type Node, type NodeProps, NodeToolbar, Position} from '@xyflow/react'
|
import {type Edge, Handle, type Node, type NodeProps, NodeResizeControl, NodeToolbar, Position} from '@xyflow/react'
|
||||||
import type {Schema} from 'amis'
|
import {type ClassName, classnames, type Schema} from 'amis'
|
||||||
import {Button, Card, Drawer, Space, Tooltip} from 'antd'
|
import {Button, Drawer, Space, Tooltip} from 'antd'
|
||||||
import {type JSX, useCallback, useState} from 'react'
|
import {type CSSProperties, type JSX, useCallback, useState} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
|
import {amisRender, commonInfo, horizontalFormOptions} from '../../../util/amis.tsx'
|
||||||
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
|
import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
|
||||||
@@ -95,20 +95,16 @@ export function outputsFormColumns(editable: boolean = false, required: boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AmisNodeProps = {
|
type AmisNodeProps = {
|
||||||
|
className: ClassName,
|
||||||
|
style?: CSSProperties,
|
||||||
nodeProps: NodeProps
|
nodeProps: NodeProps
|
||||||
extraNodeDescription?: JSX.Element
|
extraNodeDescription?: JSX.Element
|
||||||
handler: JSX.Element
|
handler: JSX.Element
|
||||||
columnSchema?: () => Schema[]
|
columnSchema?: () => Schema[]
|
||||||
|
resize?: { minWidth: number, minHeight: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
const AmisNodeContainerDiv = styled.div`
|
const AmisNodeContainerDiv = styled.div`
|
||||||
.ant-card {
|
|
||||||
.ant-card-actions {
|
|
||||||
& > li {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const StartNodeHandler = () => {
|
export const StartNodeHandler = () => {
|
||||||
@@ -128,11 +124,18 @@ export const NormalNodeHandler = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const nodeClassName = (name: string) => {
|
||||||
|
return `flow-node flow-node-${name}`
|
||||||
|
}
|
||||||
|
|
||||||
const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
|
const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
|
||||||
|
className,
|
||||||
|
style,
|
||||||
nodeProps,
|
nodeProps,
|
||||||
extraNodeDescription,
|
extraNodeDescription,
|
||||||
handler,
|
handler,
|
||||||
columnSchema,
|
columnSchema,
|
||||||
|
resize,
|
||||||
}) => {
|
}) => {
|
||||||
const {removeNode} = useFlowStore()
|
const {removeNode} = useFlowStore()
|
||||||
const {getDataById, setDataById, removeDataById} = useDataStore()
|
const {getDataById, setDataById, removeDataById} = useDataStore()
|
||||||
@@ -236,7 +239,7 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
|
|||||||
removeDataById(id)
|
removeDataById(id)
|
||||||
}, [])
|
}, [])
|
||||||
return (
|
return (
|
||||||
<AmisNodeContainerDiv className="w-64">
|
<AmisNodeContainerDiv className={classnames(className, 'w-64')} style={style}>
|
||||||
<Drawer
|
<Drawer
|
||||||
title="节点编辑"
|
title="节点编辑"
|
||||||
open={editDrawerOpen}
|
open={editDrawerOpen}
|
||||||
@@ -278,17 +281,27 @@ const AmisNode: (props: AmisNodeProps) => JSX.Element = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</NodeToolbar>
|
</NodeToolbar>
|
||||||
<Card
|
<div className="node-card h-full flex flex-col bg-white rounded-md border border-gray-100 border-solid">
|
||||||
className="node-card"
|
<div
|
||||||
title={nodeName}
|
className="node-card-header items-center flex justify-between p-2 border-t-0 border-l-0 border-r-0 border-b border-gray-100 border-solid">
|
||||||
extra={<span className="text-gray-300 text-xs">{id}</span>}
|
<span className="font-bold">{nodeName}</span>
|
||||||
size="small"
|
<span className="text-gray-300 text-sm">{id}</span>
|
||||||
>
|
</div>
|
||||||
<div className="card-description p-2 text-secondary text-sm">
|
<div className="node-card-description flex flex-col flex-1 p-2 text-secondary text-sm">
|
||||||
|
<div className="node-card-description-node">
|
||||||
{nodeDescription}
|
{nodeDescription}
|
||||||
|
</div>
|
||||||
|
<div className="node-card-description-extra flex-1 mt-1">
|
||||||
{extraNodeDescription}
|
{extraNodeDescription}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
{resize ? <>
|
||||||
|
<NodeResizeControl
|
||||||
|
minWidth={resize.minWidth}
|
||||||
|
minHeight={resize.minHeight}
|
||||||
|
/>
|
||||||
|
</> : undefined}
|
||||||
{handler}
|
{handler}
|
||||||
</AmisNodeContainerDiv>
|
</AmisNodeContainerDiv>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, {useCallback, useEffect} from 'react'
|
|||||||
import {useContextStore} from '../store/ContextStore.ts'
|
import {useContextStore} from '../store/ContextStore.ts'
|
||||||
import {useDataStore} from '../store/DataStore.ts'
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
||||||
|
|
||||||
const languageMap: Record<string, string> = {
|
const languageMap: Record<string, string> = {
|
||||||
'javascript': 'Javascript',
|
'javascript': 'Javascript',
|
||||||
@@ -62,6 +62,7 @@ const CodeNode = (props: NodeProps) => {
|
|||||||
], [props.id])
|
], [props.id])
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
|
className={nodeClassName('code')}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
extraNodeDescription={
|
extraNodeDescription={
|
||||||
nodeData?.type
|
nodeData?.type
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {commonInfo} from '../../../util/amis.tsx'
|
|||||||
import {useContextStore} from '../store/ContextStore.ts'
|
import {useContextStore} from '../store/ContextStore.ts'
|
||||||
import {useDataStore} from '../store/DataStore.ts'
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
||||||
|
|
||||||
const KnowledgeNode = (props: NodeProps) => {
|
const KnowledgeNode = (props: NodeProps) => {
|
||||||
const {getNodes, getEdges} = useFlowStore()
|
const {getNodes, getEdges} = useFlowStore()
|
||||||
@@ -79,6 +79,7 @@ const KnowledgeNode = (props: NodeProps) => {
|
|||||||
], [props.id])
|
], [props.id])
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
|
className={nodeClassName('knowledge')}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
columnSchema={columnsSchema}
|
columnSchema={columnsSchema}
|
||||||
handler={<NormalNodeHandler/>}
|
handler={<NormalNodeHandler/>}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, {useCallback, useEffect} from 'react'
|
|||||||
import {useContextStore} from '../store/ContextStore.ts'
|
import {useContextStore} from '../store/ContextStore.ts'
|
||||||
import {useDataStore} from '../store/DataStore.ts'
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
||||||
|
|
||||||
const modelMap: Record<string, string> = {
|
const modelMap: Record<string, string> = {
|
||||||
qwen3: 'Qwen3',
|
qwen3: 'Qwen3',
|
||||||
@@ -57,6 +57,7 @@ const LlmNode = (props: NodeProps) => {
|
|||||||
], [props.id])
|
], [props.id])
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
|
className={nodeClassName('llm')}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
extraNodeDescription={
|
extraNodeDescription={
|
||||||
nodeData?.model
|
nodeData?.model
|
||||||
|
|||||||
46
service-web/client/src/components/flow/node/LoopNode.tsx
Normal file
46
service-web/client/src/components/flow/node/LoopNode.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {Background, BackgroundVariant, type NodeProps} from '@xyflow/react'
|
||||||
|
import {classnames} from 'amis'
|
||||||
|
import React from 'react'
|
||||||
|
import {flowBackgroundColor, flowDotColor} from '../types.ts'
|
||||||
|
import AmisNode, {nodeClassName, NormalNodeHandler} from './AmisNode.tsx'
|
||||||
|
|
||||||
|
const LoopNode = (props: NodeProps) => {
|
||||||
|
return (
|
||||||
|
<AmisNode
|
||||||
|
className={classnames('w-full', 'h-full', nodeClassName('loop'))}
|
||||||
|
style={{
|
||||||
|
minWidth: '256px',
|
||||||
|
minHeight: '110px'
|
||||||
|
}}
|
||||||
|
nodeProps={props}
|
||||||
|
extraNodeDescription={
|
||||||
|
<div
|
||||||
|
className="nodrag relative h-full w-full"
|
||||||
|
style={{
|
||||||
|
minHeight: '8rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Background
|
||||||
|
id={`loop-background-${props.id}`}
|
||||||
|
className="rounded-xl"
|
||||||
|
variant={BackgroundVariant.Cross}
|
||||||
|
gap={20}
|
||||||
|
size={3}
|
||||||
|
style={{
|
||||||
|
zIndex: 0,
|
||||||
|
}}
|
||||||
|
color={flowDotColor}
|
||||||
|
bgColor={flowBackgroundColor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
handler={<NormalNodeHandler/>}
|
||||||
|
resize={{
|
||||||
|
minWidth: 256,
|
||||||
|
minHeight: 208,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(LoopNode)
|
||||||
@@ -4,7 +4,7 @@ import {generateAllIncomerOutputVariablesFormOptions} from '../Helper.tsx'
|
|||||||
import {useContextStore} from '../store/ContextStore.ts'
|
import {useContextStore} from '../store/ContextStore.ts'
|
||||||
import {useDataStore} from '../store/DataStore.ts'
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import AmisNode, {EndNodeHandler} from './AmisNode.tsx'
|
import AmisNode, {EndNodeHandler, nodeClassName} from './AmisNode.tsx'
|
||||||
|
|
||||||
const OutputNode = (props: NodeProps) => {
|
const OutputNode = (props: NodeProps) => {
|
||||||
const {getNodes, getEdges} = useFlowStore()
|
const {getNodes, getEdges} = useFlowStore()
|
||||||
@@ -33,6 +33,7 @@ const OutputNode = (props: NodeProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
|
className={nodeClassName('output')}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
columnSchema={columnsSchema}
|
columnSchema={columnsSchema}
|
||||||
handler={<EndNodeHandler/>}
|
handler={<EndNodeHandler/>}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Handle, type NodeProps, Position} from '@xyflow/react'
|
import {Handle, type NodeProps, Position} from '@xyflow/react'
|
||||||
import {Tag} from 'antd'
|
import {Tag} from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import AmisNode from './AmisNode.tsx'
|
import AmisNode, {nodeClassName} from './AmisNode.tsx'
|
||||||
|
|
||||||
const cases = [
|
const cases = [
|
||||||
{
|
{
|
||||||
@@ -18,6 +18,7 @@ const cases = [
|
|||||||
const SwitchNode = (props: NodeProps) => {
|
const SwitchNode = (props: NodeProps) => {
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
|
className={nodeClassName('switch')}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
extraNodeDescription={
|
extraNodeDescription={
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, {useCallback, useEffect} from 'react'
|
|||||||
import {useContextStore} from '../store/ContextStore.ts'
|
import {useContextStore} from '../store/ContextStore.ts'
|
||||||
import {useDataStore} from '../store/DataStore.ts'
|
import {useDataStore} from '../store/DataStore.ts'
|
||||||
import {useFlowStore} from '../store/FlowStore.ts'
|
import {useFlowStore} from '../store/FlowStore.ts'
|
||||||
import AmisNode, {inputsFormColumns, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
import AmisNode, {inputsFormColumns, nodeClassName, NormalNodeHandler, outputsFormColumns} from './AmisNode.tsx'
|
||||||
|
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
default: '默认',
|
default: '默认',
|
||||||
@@ -75,6 +75,7 @@ const TemplateNode = (props: NodeProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AmisNode
|
<AmisNode
|
||||||
|
className={nodeClassName('template')}
|
||||||
nodeProps={props}
|
nodeProps={props}
|
||||||
extraNodeDescription={
|
extraNodeDescription={
|
||||||
nodeData?.type
|
nodeData?.type
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const useFlowStore = create<{
|
|||||||
addNode: (node: Node) => void,
|
addNode: (node: Node) => void,
|
||||||
removeNode: (id: string) => void,
|
removeNode: (id: string) => void,
|
||||||
setNodes: (nodes: Node[]) => void,
|
setNodes: (nodes: Node[]) => void,
|
||||||
|
setNode: (node: Node) => void,
|
||||||
|
|
||||||
edges: Edge[],
|
edges: Edge[],
|
||||||
getEdges: () => Edge[],
|
getEdges: () => Edge[],
|
||||||
@@ -42,6 +43,16 @@ export const useFlowStore = create<{
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
setNodes: nodes => set({nodes}),
|
setNodes: nodes => set({nodes}),
|
||||||
|
setNode: node => {
|
||||||
|
set({
|
||||||
|
nodes: get().nodes.map(n => {
|
||||||
|
if (isEqual(node.id, n.id)) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
edges: [],
|
edges: [],
|
||||||
getEdges: () => get().edges,
|
getEdges: () => get().edges,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type {Edge, Node} from '@xyflow/react'
|
import type {Edge, Node} from '@xyflow/react'
|
||||||
|
import type {JSX} from 'react'
|
||||||
|
|
||||||
|
export const flowBackgroundColor = "#fafafa"
|
||||||
|
export const flowDotColor = "#dedede"
|
||||||
|
|
||||||
export type InputFormOptions = {
|
export type InputFormOptions = {
|
||||||
label: string
|
label: string
|
||||||
@@ -24,3 +28,13 @@ export type FlowEditorProps = {
|
|||||||
graphData: GraphData,
|
graphData: GraphData,
|
||||||
onGraphDataChange: (graphData: GraphData) => void,
|
onGraphDataChange: (graphData: GraphData) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NodeDefine = {
|
||||||
|
key: string,
|
||||||
|
group: string,
|
||||||
|
name: string,
|
||||||
|
icon: JSX.Element,
|
||||||
|
description: string,
|
||||||
|
component: any,
|
||||||
|
checkers: NodeChecker[],
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import type {GraphData} from '../components/flow/types.ts'
|
|||||||
|
|
||||||
function Test() {
|
function Test() {
|
||||||
// language=JSON
|
// language=JSON
|
||||||
const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 92\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 130\n },\n "selected": false,\n "dragging": false\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 130\n },\n "selected": true,\n "dragging": false\n }\n ],\n "edges": [\n {\n "source": "MzEitlOusl",\n "sourceHandle": "source",\n "target": "bivXSpiLaI",\n "targetHandle": "target",\n "id": "xy-edge__MzEitlOuslsource-bivXSpiLaItarget"\n }\n ],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n }\n }\n}'))
|
const [graphData] = useState<GraphData>(JSON.parse('{\n "nodes": [\n {\n "id": "QxNrkChBWQ",\n "type": "loop-node",\n "position": {\n "x": 742,\n "y": 119\n },\n "data": {},\n "measured": {\n "width": 458,\n "height": 368\n },\n "selected": true,\n "dragging": false,\n "width": 458,\n "height": 368,\n "resizing": false\n },\n {\n "id": "MzEitlOusl",\n "type": "llm-node",\n "position": {\n "x": 47,\n "y": 92\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false,\n "extent": "parent",\n "parentId": "QxNrkChBWQ"\n },\n {\n "id": "bivXSpiLaI",\n "type": "code-node",\n "position": {\n "x": 381,\n "y": 181\n },\n "data": {},\n "measured": {\n "width": 256,\n "height": 110\n },\n "selected": false,\n "dragging": false\n }\n ],\n "edges": [],\n "data": {\n "MzEitlOusl": {\n "node": {\n "name": "大模型",\n "description": "使用大模型对话"\n },\n "outputs": {\n "text": {\n "type": "string"\n }\n },\n "model": "qwen3",\n "systemPrompt": "你是个好人",\n "finished": true\n },\n "bivXSpiLaI": {\n "node": {\n "name": "代码执行",\n "description": "执行自定义的处理代码"\n },\n "outputs": {\n "result": {\n "type": "string"\n }\n },\n "type": "javascript",\n "content": "console.log(\'hello\')",\n "inputs": {\n "text": {\n "variable": "MzEitlOusl.text"\n }\n },\n "finished": true\n },\n "QxNrkChBWQ": {\n "node": {\n "name": "循环",\n "description": "实现循环执行流程"\n },\n "finished": true\n }\n }\n}'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
|
|||||||
Reference in New Issue
Block a user