<template>
|
<div class="relative h-full w-full" id="main-canvas" @drop="handleOnDrop" @dragover="handleOnDragOver">
|
<VueFlow v-model="elements" :node-types="nodeTypes" :connection-mode="ConnectionMode.Strict">
|
<template #node-start="startNodeProps">
|
<StartNode
|
ref="nodeRef"
|
v-bind="startNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(startNodeProps.id, ref)"
|
/>
|
</template>
|
<template #node-condition="conditionNodeProps">
|
<ConditionNode
|
ref="nodeRef"
|
v-bind="conditionNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(conditionNodeProps.id, ref)"
|
/>
|
</template>
|
<template #node-output_msg="outputNodeProps">
|
<OutputNode
|
ref="nodeRef"
|
v-bind="outputNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(outputNodeProps.id, ref)"
|
/>
|
</template>
|
|
<template #node-end="endNodeProps">
|
<EndNode ref="nodeRef" v-bind="endNodeProps" :isViewMode="isViewMode" />
|
</template>
|
|
<template #edge-custom="customEdgeProps">
|
<CustomEdge v-bind="customEdgeProps" :isViewMode="isViewMode" />
|
</template>
|
|
<template #node-agent="agentNodeProps">
|
<AgentNode
|
ref="nodeRef"
|
v-bind="agentNodeProps"
|
:agentNames="agentNames"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(agentNodeProps.id, ref)"
|
/>
|
</template>
|
<template #node-func="funcNodeProps">
|
<FuncNode ref="nodeRef" v-bind="funcNodeProps" :funcNames="funcNames" :isViewMode="isViewMode" />
|
</template>
|
|
<template #node-code="codeNodeProps">
|
<CodeNode
|
ref="nodeRef"
|
v-bind="codeNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(codeNodeProps.id, ref)"
|
/>
|
</template>
|
<template #node-python_code="pythonCodeNodeProps">
|
<PythonCodeNode
|
ref="nodeRef"
|
v-bind="pythonCodeNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(pythonCodeNodeProps.id, ref)"
|
/>
|
</template>
|
<template #node-n8n="n8nNodeProps">
|
<N8nNode
|
:workflowList="n8nWorkflowList"
|
ref="nodeRef"
|
v-bind="n8nNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(n8nNodeProps.id, ref)"
|
/>
|
</template>
|
<template #node-text_resource="textResourceNodeProps">
|
<TextResourceNode
|
ref="nodeRef"
|
v-bind="textResourceNodeProps"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(textResourceNodeProps.id, ref)"
|
/>
|
</template>
|
|
<template #node-analysis="analysisNodeProps">
|
<AnalysisNode ref="nodeRef" v-bind="analysisNodeProps" :llmInfoList="llmInfoList" :isViewMode="isViewMode" />
|
</template>
|
|
<template #node-LLM="llmNodeProps">
|
<LLMNode
|
v-bind="llmNodeProps"
|
:llmInfoList="llmInfoList"
|
:isViewMode="isViewMode"
|
@register="(ref) => registerNodeRef(llmNodeProps.id, ref)"
|
/>
|
</template>
|
<Controls :showInteractive="false" />
|
<Background />
|
<MiniMap pannable zoomable />
|
</VueFlow>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { Background } from '@vue-flow/background';
|
import { Controls } from '@vue-flow/controls';
|
import { MiniMap } from '@vue-flow/minimap';
|
|
import type { Dimensions, Elements, FlowProps } from '@vue-flow/core';
|
import { ConnectionMode, MarkerType, useVueFlow, VueFlow } from '@vue-flow/core';
|
import { nextTick, onMounted, ref, watch } from 'vue';
|
import { Test_data } from './testData';
|
import CustomEdge from './ui/edges/CustomEdge.vue';
|
import AgentNode from './ui/nodes/AgentNode.vue';
|
import AnalysisNode from './ui/nodes/AnalysisNode.vue';
|
import CodeNode from './ui/nodes/CodeNode.vue';
|
import ConditionNode from './ui/nodes/ConditionNode.vue';
|
import EndNode from './ui/nodes/EndNode.vue';
|
import FuncNode from './ui/nodes/FuncNode.vue';
|
import LLMNode from './ui/nodes/LLMNode.vue';
|
import OutputNode from './ui/nodes/OutputNode.vue';
|
import PythonCodeNode from './ui/nodes/PythonCodeNode.vue';
|
import StartNode from './ui/nodes/StartNode.vue';
|
import TextResourceNode from './ui/nodes/TextResourceNode.vue';
|
import { NodeType, nodeTypeMap } from './vueFlowEnum';
|
import { VueFlowHelper } from './VueFlowHelper';
|
import N8nNode from './ui/nodes/N8nNode.vue';
|
import { GetN8nWorkflowList } from '/@/api/n8n';
|
const props = defineProps(['flowJson', 'agentNames', 'funcNames', 'llmInfoList', 'isViewMode']);
|
|
const n8nWorkflowList = ref([]);
|
const getN8nWorkflowList = async () => {
|
const res = await GetN8nWorkflowList({
|
active: true,
|
});
|
n8nWorkflowList.value = res?.data ?? [];
|
};
|
|
const nodeTypes = {};
|
const elements = ref<Elements>();
|
const {
|
findNode,
|
nodes,
|
addNodes,
|
addEdges,
|
project,
|
vueFlowRef,
|
onConnect,
|
setNodes,
|
setEdges,
|
setViewport,
|
fitView,
|
setInteractive,
|
} = useVueFlow(
|
Object.keys(props.flowJson).length > 0
|
? props.flowJson
|
: ({
|
nodes: [
|
{
|
id: '1',
|
type: 'start',
|
data: VueFlowHelper.getDefaultData(NodeType.Start),
|
|
position: { x: 25, y: 400 },
|
},
|
],
|
edges: [],
|
} as FlowProps)
|
);
|
|
onConnect((params) => {
|
addEdges({
|
id: VueFlowHelper.genGeometryId(),
|
|
source: params.source,
|
target: params.target,
|
sourceHandle: params.sourceHandle,
|
targetHandle: params.targetHandle,
|
type: 'custom',
|
markerEnd: {
|
type: MarkerType.ArrowClosed,
|
width: 40,
|
height: 70,
|
},
|
});
|
|
// addEdges());
|
});
|
const initFlowStatus = () => {
|
fitView();
|
|
setInteractive(!props.isViewMode);
|
};
|
onMounted(async () => {
|
getN8nWorkflowList();
|
setTimeout(() => {
|
initFlowStatus();
|
}, 30);
|
});
|
|
function handleOnDrop(event: DragEvent) {
|
const type = event.dataTransfer?.getData('application/vueflow') as NodeType;
|
if (type === 'workflow') {
|
const { nodes, edges, position, zoom } = Test_data;
|
const [x = 0, y = 0] = position;
|
setNodes(nodes);
|
setEdges(edges);
|
setViewport({ x, y, zoom: zoom || 0 });
|
return;
|
}
|
|
const { left, top } = vueFlowRef.value!.getBoundingClientRect();
|
|
const position = project({
|
x: event.clientX - left,
|
y: event.clientY - top,
|
});
|
|
const newNode = {
|
id: VueFlowHelper.genGeometryId(),
|
type,
|
position,
|
label: nodeTypeMap[type],
|
data: VueFlowHelper.getDefaultData(type),
|
};
|
|
addNodes([newNode]);
|
|
nextTick(() => {
|
const node = findNode(newNode.id);
|
const stop = watch(
|
() => node!.dimensions,
|
(dimensions: Dimensions) => {
|
if (dimensions.width > 0 && dimensions.height > 0 && node) {
|
node.position = {
|
x: node.position.x - node.dimensions.width / 2,
|
y: node.position.y - node.dimensions.height / 2,
|
};
|
stop();
|
}
|
},
|
{ deep: true, flush: 'post' }
|
);
|
});
|
}
|
function handleOnDragOver(event: DragEvent) {
|
event.preventDefault();
|
|
if (event.dataTransfer) {
|
event.dataTransfer.dropEffect = 'move';
|
}
|
}
|
|
const nodeRefs = ref(new Map());
|
|
const registerNodeRef = (nodeId: string, ref: any) => {
|
nodeRefs.value.set(nodeId, ref);
|
};
|
|
const validateForm = async () => {
|
const validPromises = [];
|
for (const nodeRef of nodeRefs.value.values()) {
|
if (nodeRef?.validateForm) {
|
validPromises.push(nodeRef.validateForm());
|
}
|
}
|
|
const results = await Promise.all(validPromises);
|
return results;
|
};
|
|
defineExpose({
|
validateForm,
|
});
|
</script>
|
<style lang="scss">
|
@import '@vue-flow/core/dist/style.css';
|
@import '@vue-flow/core/dist/theme-default.css';
|
@import '@vue-flow/controls/dist/style.css';
|
@import '@vue-flow/minimap/dist/style.css';
|
@import '@vue-flow/node-resizer/dist/style.css';
|
|
#main-canvas {
|
--vf-handle: #2563eb;
|
--handle-size: 13px;
|
--resize-handle-size: 8px;
|
.vue-flow__handle {
|
width: var(--handle-size);
|
height: var(--handle-size);
|
}
|
|
// .vue-flow__resize-control.handle {
|
// width: var(--resize-handle-size);
|
// height: var(--resize-handle-size);
|
// // visibility: hidden;
|
// }
|
}
|
</style>
|
|
<style lang="scss" scoped></style>
|