feat: 新增技能编排引擎和工作流构建器组件
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 统一Hands系统常量到单个源文件 refactor: 更新Hands中文名称和描述 fix: 修复技能市场在连接状态变化时重新加载 fix: 修复身份变更提案的错误处理逻辑 docs: 更新多个功能文档的验证状态和实现位置 docs: 更新Hands系统文档 test: 添加测试文件验证工作区路径
This commit is contained in:
324
desktop/src/components/WorkflowBuilder/WorkflowBuilder.tsx
Normal file
324
desktop/src/components/WorkflowBuilder/WorkflowBuilder.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Workflow Builder Component
|
||||
*
|
||||
* Visual workflow editor using React Flow for creating and editing
|
||||
* Pipeline DSL configurations.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useEffect } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
BackgroundVariant,
|
||||
Connection,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Node,
|
||||
Edge,
|
||||
NodeTypes,
|
||||
Panel,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
import { useWorkflowBuilderStore, nodePaletteItems, paletteCategories } from '../../store/workflowBuilderStore';
|
||||
import type { WorkflowNodeType, WorkflowNodeData } from '../../lib/workflow-builder/types';
|
||||
import { validateCanvas } from '../../lib/workflow-builder/yaml-converter';
|
||||
|
||||
// Import custom node components
|
||||
import { InputNode } from './nodes/InputNode';
|
||||
import { LlmNode } from './nodes/LlmNode';
|
||||
import { SkillNode } from './nodes/SkillNode';
|
||||
import { HandNode } from './nodes/HandNode';
|
||||
import { ConditionNode } from './nodes/ConditionNode';
|
||||
import { ParallelNode } from './nodes/ParallelNode';
|
||||
import { ExportNode } from './nodes/ExportNode';
|
||||
import { HttpNode } from './nodes/HttpNode';
|
||||
import { OrchestrationNode } from './nodes/OrchestrationNode';
|
||||
|
||||
import { NodePalette } from './NodePalette';
|
||||
import { PropertyPanel } from './PropertyPanel';
|
||||
import { WorkflowToolbar } from './WorkflowToolbar';
|
||||
|
||||
// =============================================================================
|
||||
// Node Types Configuration
|
||||
// =============================================================================
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
input: InputNode,
|
||||
llm: LlmNode,
|
||||
skill: SkillNode,
|
||||
hand: HandNode,
|
||||
condition: ConditionNode,
|
||||
parallel: ParallelNode,
|
||||
export: ExportNode,
|
||||
http: HttpNode,
|
||||
orchestration: OrchestrationNode,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Main Component
|
||||
// =============================================================================
|
||||
|
||||
export function WorkflowBuilderInternal() {
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const { screenToFlowPosition, fitView } = useReactFlow();
|
||||
|
||||
const {
|
||||
canvas,
|
||||
isDirty,
|
||||
selectedNodeId,
|
||||
validation,
|
||||
addNode,
|
||||
updateNode,
|
||||
deleteNode,
|
||||
addEdge: addStoreEdge,
|
||||
selectNode,
|
||||
saveWorkflow,
|
||||
validate,
|
||||
setDragging,
|
||||
} = useWorkflowBuilderStore();
|
||||
|
||||
// Local state for React Flow
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
// Sync canvas state with React Flow
|
||||
useEffect(() => {
|
||||
if (canvas) {
|
||||
setNodes(canvas.nodes.map(n => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
position: n.position,
|
||||
data: n.data,
|
||||
})));
|
||||
setEdges(canvas.edges.map(e => ({
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
type: e.type || 'default',
|
||||
animated: true,
|
||||
})));
|
||||
} else {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
}
|
||||
}, [canvas?.id]);
|
||||
|
||||
// Handle node changes (position, selection)
|
||||
const handleNodesChange = useCallback(
|
||||
(changes) => {
|
||||
onNodesChange(changes);
|
||||
|
||||
// Sync position changes back to store
|
||||
for (const change of changes) {
|
||||
if (change.type === 'position' && change.position) {
|
||||
const node = nodes.find(n => n.id === change.id);
|
||||
if (node) {
|
||||
// Position updates are handled by React Flow internally
|
||||
}
|
||||
}
|
||||
if (change.type === 'select') {
|
||||
selectNode(change.selected ? change.id : null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onNodesChange, nodes, selectNode]
|
||||
);
|
||||
|
||||
// Handle edge changes
|
||||
const handleEdgesChange = useCallback(
|
||||
(changes) => {
|
||||
onEdgesChange(changes);
|
||||
},
|
||||
[onEdgesChange]
|
||||
);
|
||||
|
||||
// Handle new connections
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
if (connection.source && connection.target) {
|
||||
addStoreEdge(connection.source, connection.target);
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
...connection,
|
||||
type: 'default',
|
||||
animated: true,
|
||||
},
|
||||
eds
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
[addStoreEdge, setEdges]
|
||||
);
|
||||
|
||||
// Handle node click
|
||||
const onNodeClick = useCallback(
|
||||
(_event: React.MouseEvent, node: Node) => {
|
||||
selectNode(node.id);
|
||||
},
|
||||
[selectNode]
|
||||
);
|
||||
|
||||
// Handle pane click (deselect)
|
||||
const onPaneClick = useCallback(() => {
|
||||
selectNode(null);
|
||||
}, [selectNode]);
|
||||
|
||||
// Handle drag over for palette items
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}, []);
|
||||
|
||||
// Handle drop from palette
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const type = event.dataTransfer.getData('application/reactflow') as WorkflowNodeType;
|
||||
if (!type) return;
|
||||
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
addNode(type, position);
|
||||
},
|
||||
[screenToFlowPosition, addNode]
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Delete selected node
|
||||
if ((event.key === 'Delete' || event.key === 'Backspace') && selectedNodeId) {
|
||||
deleteNode(selectedNodeId);
|
||||
}
|
||||
|
||||
// Save workflow
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
saveWorkflow();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedNodeId, deleteNode, saveWorkflow]);
|
||||
|
||||
if (!canvas) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full bg-gray-50">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-4">No workflow loaded</p>
|
||||
<button
|
||||
onClick={() => useWorkflowBuilderStore.getState().createNewWorkflow('New Workflow')}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Create New Workflow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Node Palette */}
|
||||
<NodePalette
|
||||
categories={paletteCategories}
|
||||
onDragStart={(type) => {
|
||||
setDragging(true);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setDragging(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
<WorkflowToolbar
|
||||
workflowName={canvas.name}
|
||||
isDirty={isDirty}
|
||||
validation={validation}
|
||||
onSave={saveWorkflow}
|
||||
onValidate={validate}
|
||||
/>
|
||||
|
||||
<div ref={reactFlowWrapper} className="flex-1">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
snapToGrid
|
||||
snapGrid={[15, 15]}
|
||||
defaultEdgeOptions={{
|
||||
animated: true,
|
||||
type: 'smoothstep',
|
||||
}}
|
||||
>
|
||||
<Controls />
|
||||
<MiniMap
|
||||
nodeColor={(node) => {
|
||||
switch (node.type) {
|
||||
case 'input':
|
||||
return '#10b981';
|
||||
case 'llm':
|
||||
return '#8b5cf6';
|
||||
case 'skill':
|
||||
return '#f59e0b';
|
||||
case 'hand':
|
||||
return '#ef4444';
|
||||
case 'export':
|
||||
return '#3b82f6';
|
||||
default:
|
||||
return '#6b7280';
|
||||
}
|
||||
}}
|
||||
maskColor="rgba(0, 0, 0, 0.1)"
|
||||
/>
|
||||
<Background variant={BackgroundVariant.Dots} gap={20} size={1} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Property Panel */}
|
||||
{selectedNodeId && (
|
||||
<PropertyPanel
|
||||
nodeId={selectedNodeId}
|
||||
nodeData={nodes.find(n => n.id === selectedNodeId)?.data as WorkflowNodeData}
|
||||
onUpdate={(data) => updateNode(selectedNodeId, data)}
|
||||
onDelete={() => deleteNode(selectedNodeId)}
|
||||
onClose={() => selectNode(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export with provider
|
||||
export function WorkflowBuilder() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowBuilderInternal />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowBuilder;
|
||||
Reference in New Issue
Block a user