(null)
+ const { isExecuting, executionResult, handleRunWorkflow } =
+ useWorkflowExecution()
+
+ const {
+ blocks,
+ edges,
+ addBlock,
+ updateBlockPosition,
+ addEdge,
+ removeEdge,
+ canUndo,
+ canRedo,
+ undo,
+ redo,
+ } = useWorkflowStore()
+ const { addNotification } = useNotificationStore()
+ const { project } = useReactFlow()
+
+ // Convert blocks to ReactFlow nodes
+ const nodes = Object.values(blocks).map((block) => ({
+ id: block.id,
+ type: 'workflowBlock',
+ position: block.position,
+ selected: block.id === selectedBlockId,
+ dragHandle: '.workflow-drag-handle',
+ data: {
+ type: block.type,
+ config: getBlock(block.type),
+ name: block.name,
+ },
+ }))
+
+ const onNodesChange = useCallback(
+ (changes: any) => {
+ changes.forEach((change: any) => {
+ if (change.type === 'position' && change.position) {
+ updateBlockPosition(change.id, change.position)
+ }
+ })
+ },
+ [updateBlockPosition]
+ )
+
+ const onEdgesChange = useCallback(
+ (changes: any) => {
+ changes.forEach((change: any) => {
+ if (change.type === 'remove') {
+ removeEdge(change.id)
+ }
+ })
+ },
+ [removeEdge]
+ )
+
+ const onConnect = useCallback(
+ (connection: any) => {
+ addEdge({
+ ...connection,
+ id: crypto.randomUUID(),
+ type: 'custom',
+ })
+ },
+ [addEdge]
+ )
+
+ const onDrop = useCallback(
+ (event: React.DragEvent) => {
+ event.preventDefault()
+
+ try {
+ const data = JSON.parse(event.dataTransfer.getData('application/json'))
+ if (data.type === 'connectionBlock') return
+
+ const reactFlowBounds = event.currentTarget.getBoundingClientRect()
+ const position = project({
+ x: event.clientX - reactFlowBounds.left,
+ y: event.clientY - reactFlowBounds.top,
+ })
+
+ const blockConfig = getBlock(data.type)
+ if (!blockConfig) {
+ console.error('Invalid block type:', data.type)
+ return
+ }
+
+ const id = crypto.randomUUID()
+ const name = `${blockConfig.toolbar.title} ${
+ Object.values(blocks).filter((b) => b.type === data.type).length + 1
+ }`
+
+ addBlock(id, data.type, name, position)
+ } catch (err) {
+ console.error('Error dropping block:', err)
+ }
+ },
+ [project, blocks, addBlock]
+ )
+
+ const onNodeClick = useCallback((event: React.MouseEvent, node: any) => {
+ event.stopPropagation()
+ setSelectedBlockId(node.id)
+ setSelectedEdgeId(null)
+ }, [])
+
+ const onPaneClick = useCallback(() => {
+ setSelectedBlockId(null)
+ setSelectedEdgeId(null)
+ }, [])
+
+ const onEdgeClick = useCallback((event: React.MouseEvent, edge: any) => {
+ setSelectedEdgeId(edge.id)
+ }, [])
+
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ (event.key === 'Delete' || event.key === 'Backspace') &&
+ selectedEdgeId
+ ) {
+ removeEdge(selectedEdgeId)
+ setSelectedEdgeId(null)
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [selectedEdgeId, removeEdge])
+
+ useEffect(() => {
+ initializeStateLogger()
+ }, [])
+
+ const edgesWithSelection = edges.map((edge) => ({
+ ...edge,
+ data: {
+ selectedEdgeId,
+ onDelete: (edgeId: string) => {
+ removeEdge(edgeId)
+ setSelectedEdgeId(null)
+ },
+ },
+ }))
+
+ return (
+
+
+ e.preventDefault()}
+ fitView
+ maxZoom={1}
+ panOnScroll
+ defaultEdgeOptions={{ type: 'custom' }}
+ edgeTypes={edgeTypes}
+ proOptions={{ hideAttribution: true }}
+ connectionLineStyle={{
+ stroke: '#94a3b8',
+ strokeWidth: 2,
+ strokeDasharray: '5,5',
+ }}
+ connectionLineType={ConnectionLineType.SmoothStep}
+ onNodeClick={onNodeClick}
+ onPaneClick={onPaneClick}
+ onEdgeClick={onEdgeClick}
+ elementsSelectable={true}
+ selectNodesOnDrag={false}
+ nodesConnectable={true}
+ nodesDraggable={true}
+ draggable={false}
+ noWheelClassName="allow-scroll"
+ edgesFocusable={true}
+ edgesUpdatable={true}
+ >
+
+
+
+ )
+}
diff --git a/app/w/[id]/components/workflow-node/workflow-node.tsx b/app/w/[id]/components/workflow-node/workflow-node.tsx
new file mode 100644
index 0000000000..e194f45021
--- /dev/null
+++ b/app/w/[id]/components/workflow-node/workflow-node.tsx
@@ -0,0 +1,26 @@
+import { NodeProps } from 'reactflow'
+import { WorkflowBlock } from '../../../components/workflow-block/workflow-block'
+import { BlockConfig } from '../../../../../blocks/types'
+
+interface WorkflowNodeData {
+ type: string
+ config: BlockConfig
+ name: string
+}
+
+export const WorkflowNode = ({
+ data,
+ id,
+ xPos,
+ yPos,
+ selected,
+}: NodeProps) => (
+
+)
diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx
index 8ab503283b..d7ab7d3046 100644
--- a/app/w/[id]/workflow.tsx
+++ b/app/w/[id]/workflow.tsx
@@ -1,361 +1,12 @@
'use client'
-import { useCallback, useEffect, useState } from 'react'
-import ReactFlow, {
- Background,
- NodeProps,
- NodeTypes,
- EdgeTypes,
- Connection,
- addEdge,
- useNodesState,
- useEdgesState,
- useReactFlow,
- ReactFlowProvider,
- ConnectionLineType,
- BaseEdge,
- EdgeProps,
- getSmoothStepPath,
-} from 'reactflow'
+import { useEffect, useState } from 'react'
+import { ReactFlowProvider } from 'reactflow'
import 'reactflow/dist/style.css'
-import { getBlock } from '../../../blocks'
-import { WorkflowBlock } from '../components/workflow-block/workflow-block'
-import { BlockConfig } from '../../../blocks/types'
-import { useWorkflowStore } from '@/stores/workflow/workflow-store'
-import { initializeStateLogger } from '@/stores/workflow/state-logger'
-import { BlockState } from '@/stores/workflow/types'
-import { NotificationList } from '@/app/w/components/notifications/notifications'
-import { useNotificationStore } from '@/stores/notifications/notifications-store'
-import { executeWorkflow } from '@/lib/workflow'
-import { useWorkflowExecution } from '../hooks/use-workflow-execution'
import { useWorkflowRegistry } from '@/stores/workflow/workflow-registry'
import { useParams, useRouter } from 'next/navigation'
-import { X } from 'lucide-react'
+import { WorkflowCanvas } from './components/workflow-canvas/workflow-canvas'
-/**
- * Represents the data structure for a workflow node
- */
-interface WorkflowNodeData {
- type: string
- config: BlockConfig
- name: string
-}
-
-/**
- * Custom node component for rendering workflow blocks in the workflow editor
- */
-const WorkflowNode = ({
- data,
- id,
- xPos,
- yPos,
- selected,
-}: NodeProps) => (
-
-)
-
-/**
- * Custom edge component that renders an animated dashed line between nodes
- */
-const CustomEdge = (props: EdgeProps) => {
- const [edgePath] = getSmoothStepPath({
- sourceX: props.sourceX,
- sourceY: props.sourceY,
- targetX: props.targetX,
- targetY: props.targetY,
- })
-
- const midPoint = {
- x: (props.sourceX + props.targetX) / 2,
- y: (props.sourceY + props.targetY) / 2,
- }
-
- const isSelected = props.id === props.data?.selectedEdgeId
-
- return (
-
- {/* Invisible wider path for better click interaction */}
-
- {/* Visible animated path */}
-
-
-
-
- {/* Delete button - only show when edge is selected */}
- {isSelected && (
-
- {
- e.stopPropagation()
- props.data?.onDelete?.(props.id)
- }}
- >
-
-
-
- )}
-
- )
-}
-
-/**
- * Component type definitions for ReactFlow nodes and edges
- */
-const nodeTypes: NodeTypes = { workflowBlock: WorkflowNode }
-const edgeTypes: EdgeTypes = { custom: CustomEdge }
-
-/**
- * Main canvas component that handles the interactive workflow editor functionality
- * including drag and drop, node connections, and position updates
- */
-function WorkflowCanvas() {
- const [selectedBlockId, setSelectedBlockId] = useState(null)
- const [selectedEdgeId, setSelectedEdgeId] = useState(null)
- const { isExecuting, executionResult, handleRunWorkflow } =
- useWorkflowExecution()
-
- const {
- blocks,
- edges,
- addBlock,
- updateBlockPosition,
- addEdge,
- removeEdge,
- canUndo,
- canRedo,
- undo,
- redo,
- } = useWorkflowStore()
- const { addNotification } = useNotificationStore()
-
- const params = useParams()
-
- // Convert blocks to ReactFlow nodes using local selectedBlockId
- const nodes = Object.values(blocks).map((block) => ({
- id: block.id,
- type: 'workflowBlock',
- position: block.position,
- selected: block.id === selectedBlockId,
- dragHandle: '.workflow-drag-handle',
- data: {
- type: block.type,
- config: getBlock(block.type),
- name: block.name,
- },
- }))
-
- const { project } = useReactFlow()
-
- /**
- * Handles updating node positions when they are dragged
- */
- const onNodesChange = useCallback(
- (changes: any) => {
- changes.forEach((change: any) => {
- if (change.type === 'position' && change.position) {
- updateBlockPosition(change.id, change.position)
- }
- })
- },
- [updateBlockPosition]
- )
-
- /**
- * Handles edge removal when they are deleted
- */
- const onEdgesChange = useCallback(
- (changes: any) => {
- changes.forEach((change: any) => {
- if (change.type === 'remove') {
- removeEdge(change.id)
- }
- })
- },
- [removeEdge]
- )
-
- /**
- * Handles creating new connections between nodes
- */
- const onConnect = useCallback(
- (connection: any) => {
- addEdge({
- ...connection,
- id: crypto.randomUUID(),
- type: 'custom',
- })
- },
- [addEdge]
- )
-
- /**
- * Handles the drop event when a new block is dragged onto the canvas
- */
- const onDrop = useCallback(
- (event: React.DragEvent) => {
- event.preventDefault()
-
- try {
- const data = JSON.parse(event.dataTransfer.getData('application/json'))
-
- // Early return if this is a connection block drag
- if (data.type === 'connectionBlock') {
- return
- }
-
- const reactFlowBounds = event.currentTarget.getBoundingClientRect()
- const position = project({
- x: event.clientX - reactFlowBounds.left,
- y: event.clientY - reactFlowBounds.top,
- })
-
- const blockConfig = getBlock(data.type)
-
- if (!blockConfig) {
- console.error('Invalid block type:', data.type)
- return
- }
-
- const id = crypto.randomUUID()
- const name = `${blockConfig.toolbar.title} ${
- Object.values(blocks).filter((b) => b.type === data.type).length + 1
- }`
-
- addBlock(id, data.type, name, position)
- } catch (err) {
- console.error('Error dropping block:', err)
- }
- },
- [project, blocks, addBlock]
- )
-
- // Handler for node clicks
- const onNodeClick = useCallback((event: React.MouseEvent, node: any) => {
- event.stopPropagation()
- setSelectedBlockId(node.id)
- setSelectedEdgeId(null)
- }, [])
-
- // Handler for clicks on the empty canvas
- const onPaneClick = useCallback((event: React.MouseEvent) => {
- setSelectedBlockId(null)
- setSelectedEdgeId(null)
- }, [])
-
- // Add this new handler
- const onEdgeClick = useCallback((event: React.MouseEvent, edge: any) => {
- setSelectedEdgeId(edge.id)
- }, [])
-
- // Add keyboard event handler
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- if (
- (event.key === 'Delete' || event.key === 'Backspace') &&
- selectedEdgeId
- ) {
- removeEdge(selectedEdgeId)
- setSelectedEdgeId(null)
- }
- }
-
- window.addEventListener('keydown', handleKeyDown)
- return () => window.removeEventListener('keydown', handleKeyDown)
- }, [selectedEdgeId, removeEdge])
-
- useEffect(() => {
- initializeStateLogger()
- }, [])
-
- // Update the edges to include both selectedEdgeId and onDelete handler
- const edgesWithSelection = edges.map((edge) => ({
- ...edge,
- data: {
- selectedEdgeId,
- onDelete: (edgeId: string) => {
- removeEdge(edgeId)
- setSelectedEdgeId(null)
- },
- },
- }))
-
- return (
-
-
- e.preventDefault()}
- fitView
- maxZoom={1}
- panOnScroll
- defaultEdgeOptions={{ type: 'custom' }}
- edgeTypes={edgeTypes}
- proOptions={{ hideAttribution: true }}
- connectionLineStyle={{
- stroke: '#94a3b8',
- strokeWidth: 2,
- strokeDasharray: '5,5',
- }}
- connectionLineType={ConnectionLineType.SmoothStep}
- onNodeClick={onNodeClick}
- onPaneClick={onPaneClick}
- onEdgeClick={onEdgeClick}
- elementsSelectable={true}
- selectNodesOnDrag={false}
- nodesConnectable={true}
- nodesDraggable={true}
- draggable={false}
- noWheelClassName="allow-scroll"
- edgesFocusable={true}
- edgesUpdatable={true}
- >
-
-
-
- )
-}
-
-/**
- * Root workflow component that provides the ReactFlow context to the canvas
- */
export default function Workflow() {
const params = useParams()
const router = useRouter()
@@ -363,7 +14,6 @@ export default function Workflow() {
const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => {
- // First, initialize the registry and wait for it
if (typeof window !== 'undefined') {
const savedRegistry = localStorage.getItem('workflow-registry')
if (savedRegistry) {
@@ -374,10 +24,8 @@ export default function Workflow() {
}, [])
useEffect(() => {
- // Only proceed if we've initialized the registry
if (!isInitialized) return
- // Helper function to create a new workflow
const createInitialWorkflow = () => {
const id = crypto.randomUUID()
const newWorkflow = {
@@ -391,25 +39,21 @@ export default function Workflow() {
return id
}
- // Validate and handle workflow navigation
const validateAndNavigate = () => {
const workflowIds = Object.keys(workflows)
const currentId = params.id as string
- // No workflows exist - create one and navigate
if (workflowIds.length === 0) {
const newId = createInitialWorkflow()
router.replace(`/w/${newId}`)
return
}
- // Current workflow is invalid - navigate to first available
if (!workflows[currentId]) {
router.replace(`/w/${workflowIds[0]}`)
return
}
- // Valid workflow - set it as active
setActiveWorkflow(currentId)
}
@@ -424,7 +68,7 @@ export default function Workflow() {
])
if (!isInitialized) {
- return null // or a loading spinner
+ return null
}
return (