Added dynamic handles; added conditional block; changed function block

This commit is contained in:
Emir Karabeg
2025-02-04 18:39:13 -08:00
parent e430bee5ea
commit 22e695eec1
6 changed files with 153 additions and 21 deletions

View File

@@ -91,7 +91,7 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
<Switch
blockId={blockId}
subBlockId={config.id}
title={config.title}
title={config.title ?? ''}
/>
)
case 'tool-input':
@@ -101,7 +101,7 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
<CheckboxList
blockId={blockId}
subBlockId={config.id}
title={config.title}
title={config.title ?? ''}
options={config.options as { label: string; id: string }[]}
layout={config.layout}
/>

View File

@@ -5,9 +5,10 @@ import { Handle, Position } from 'reactflow'
import { cn } from '@/lib/utils'
import { ActionBar } from './components/action-bar/action-bar'
import { ConnectionBlocks } from './components/connection-blocks/connection-blocks'
import { useState } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useWorkflowStore } from '@/stores/workflow/store'
import { Badge } from '@/components/ui/badge'
import { useUpdateNodeInternals } from 'reactflow'
interface WorkflowBlockProps {
id: string
@@ -18,6 +19,11 @@ interface WorkflowBlockProps {
selected?: boolean
}
interface SubBlockPosition {
id: string
top: number
}
export function WorkflowBlock({
id,
type,
@@ -37,6 +43,60 @@ export function WorkflowBlock({
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
const updateBlockName = useWorkflowStore((state) => state.updateBlockName)
const blockRef = useRef<HTMLDivElement>(null)
const [subBlockPositions, setSubBlockPositions] = useState<
SubBlockPosition[]
>([])
const updateNodeInternals = useUpdateNodeInternals()
// Calculate subblock positions when the component mounts or updates
useEffect(() => {
const calculatePositions = () => {
if (!blockRef.current) return
const positions: SubBlockPosition[] = []
const blockRect = blockRef.current.getBoundingClientRect()
workflow.subBlocks
.filter((block) => block.outputHandle)
.forEach((block) => {
const subBlockElement = blockRef.current?.querySelector(
`[data-subblock-id="${block.id}"]`
)
if (subBlockElement) {
const subBlockRect = subBlockElement.getBoundingClientRect()
positions.push({
id: block.id,
// Move handle 25px up from the bottom
top: subBlockRect.bottom - blockRect.top - 25,
})
}
})
setSubBlockPositions(positions)
updateNodeInternals(id)
}
// Calculate initial positions
calculatePositions()
// Use ResizeObserver to detect size changes and recalculate
const resizeObserver = new ResizeObserver(() => {
calculatePositions()
})
if (blockRef.current) {
resizeObserver.observe(blockRef.current)
}
// Recalculate on window resize
window.addEventListener('resize', calculatePositions)
return () => {
resizeObserver.disconnect()
window.removeEventListener('resize', calculatePositions)
}
}, [workflow.subBlocks, id, updateNodeInternals])
function groupSubBlocks(subBlocks: SubBlockConfig[]) {
const rows: SubBlockConfig[][] = []
@@ -87,6 +147,7 @@ export function WorkflowBlock({
return (
<Card
ref={blockRef}
className={cn(
'w-[320px] shadow-md select-none group relative cursor-default',
!isEnabled && 'shadow-sm'
@@ -155,6 +216,7 @@ export function WorkflowBlock({
className={`space-y-1 ${
subBlock.layout === 'half' ? 'flex-1' : 'w-full'
}`}
data-subblock-id={subBlock.id}
>
<SubBlock
blockId={id}
@@ -167,18 +229,40 @@ export function WorkflowBlock({
))}
</div>
<Handle
type="source"
position={horizontalHandles ? Position.Right : Position.Bottom}
className={cn(
'!w-3.5 !h-3.5',
'!bg-white !rounded-full !border !border-gray-200',
'!opacity-0 group-hover:!opacity-100',
'!transition-opacity !duration-200 !cursor-crosshair',
'hover:!border-blue-500',
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]'
)}
/>
{/* Main output handle */}
{subBlockPositions.length === 0 && (
<Handle
type="source"
position={horizontalHandles ? Position.Right : Position.Bottom}
className={cn(
'!w-3.5 !h-3.5',
'!bg-white !rounded-full !border !border-gray-200',
'!opacity-0 group-hover:!opacity-100',
'!transition-opacity !duration-200 !cursor-crosshair',
'hover:!border-blue-500',
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]'
)}
/>
)}
{/* Subblock output handles */}
{subBlockPositions.map((position) => (
<Handle
key={position.id}
type="source"
position={Position.Right}
id={`output-${position.id}`}
style={{ top: position.top }}
className={cn(
'!w-3.5 !h-3.5',
'!bg-white !rounded-full !border !border-gray-200',
'!opacity-0 group-hover:!opacity-100',
'!transition-opacity !duration-200 !cursor-crosshair',
'hover:!border-blue-500',
'!right-[-7px]'
)}
/>
))}
</Card>
)
}

View File

@@ -0,0 +1,46 @@
import { ConditionalIcon } from '@/components/icons'
import { BlockConfig } from '../types'
import { CodeExecutionOutput } from '@/tools/function/execute'
export const ConditionBlock: BlockConfig<CodeExecutionOutput> = {
type: 'condition',
toolbar: {
title: 'Condition',
description: 'Add a condition',
bgColor: '#FF972F',
icon: ConditionalIcon,
category: 'blocks',
},
tools: {
access: ['function_execute']
},
workflow: {
inputs: {
code: { type: 'string', required: true }
},
outputs: {
response: {
type: {
result: 'any',
stdout: 'string'
}
}
},
subBlocks: [
{
id: 'if',
title: 'if',
type: 'code',
layout: 'full',
outputHandle: true
},
{
id: 'elseIf',
title: 'else if',
type: 'code',
layout: 'full',
outputHandle: true
}
],
},
}

View File

@@ -7,7 +7,7 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
toolbar: {
title: 'Function',
description: 'Add custom logic',
bgColor: '#FF8D2F',
bgColor: '#FF402F',
icon: CodeIcon,
category: 'blocks',
},
@@ -29,10 +29,8 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
subBlocks: [
{
id: 'code',
title: 'Code',
type: 'code',
layout: 'full',
placeholder: 'Enter your code here...'
}
],
},

View File

@@ -10,6 +10,7 @@ import { JinaBlock } from './blocks/jina'
import { TranslateBlock } from './blocks/translate'
import { SlackMessageBlock } from './blocks/slack'
import { GitHubBlock } from './blocks/github'
import { ConditionBlock } from './blocks/condition'
// Export blocks for ease of use
export {
@@ -21,7 +22,8 @@ export {
JinaBlock,
TranslateBlock,
SlackMessageBlock,
GitHubBlock
GitHubBlock,
ConditionBlock
}
// Registry of all block configurations
@@ -34,7 +36,8 @@ const blocks: Record<string, BlockConfig> = {
jina_reader: JinaBlock,
translate: TranslateBlock,
slack_message: SlackMessageBlock,
github_repo_info: GitHubBlock
github_repo_info: GitHubBlock,
condition: ConditionBlock
}
// Build a reverse mapping of tools to block types

View File

@@ -38,7 +38,7 @@ export interface ParamConfig {
export interface SubBlockConfig {
id: string
title: string
title?: string
type: SubBlockType
layout?: SubBlockLayout
options?: string[] | { label: string; id: string }[]
@@ -48,6 +48,7 @@ export interface SubBlockConfig {
placeholder?: string
password?: boolean
connectionDroppable?: boolean
outputHandle?: boolean
hidden?: boolean
value?: (params: Record<string, any>) => string
}