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

View File

@@ -5,9 +5,10 @@ import { Handle, Position } from 'reactflow'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { ActionBar } from './components/action-bar/action-bar' import { ActionBar } from './components/action-bar/action-bar'
import { ConnectionBlocks } from './components/connection-blocks/connection-blocks' 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 { useWorkflowStore } from '@/stores/workflow/store'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { useUpdateNodeInternals } from 'reactflow'
interface WorkflowBlockProps { interface WorkflowBlockProps {
id: string id: string
@@ -18,6 +19,11 @@ interface WorkflowBlockProps {
selected?: boolean selected?: boolean
} }
interface SubBlockPosition {
id: string
top: number
}
export function WorkflowBlock({ export function WorkflowBlock({
id, id,
type, type,
@@ -37,6 +43,60 @@ export function WorkflowBlock({
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('') const [editedName, setEditedName] = useState('')
const updateBlockName = useWorkflowStore((state) => state.updateBlockName) 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[]) { function groupSubBlocks(subBlocks: SubBlockConfig[]) {
const rows: SubBlockConfig[][] = [] const rows: SubBlockConfig[][] = []
@@ -87,6 +147,7 @@ export function WorkflowBlock({
return ( return (
<Card <Card
ref={blockRef}
className={cn( className={cn(
'w-[320px] shadow-md select-none group relative cursor-default', 'w-[320px] shadow-md select-none group relative cursor-default',
!isEnabled && 'shadow-sm' !isEnabled && 'shadow-sm'
@@ -155,6 +216,7 @@ export function WorkflowBlock({
className={`space-y-1 ${ className={`space-y-1 ${
subBlock.layout === 'half' ? 'flex-1' : 'w-full' subBlock.layout === 'half' ? 'flex-1' : 'w-full'
}`} }`}
data-subblock-id={subBlock.id}
> >
<SubBlock <SubBlock
blockId={id} blockId={id}
@@ -167,18 +229,40 @@ export function WorkflowBlock({
))} ))}
</div> </div>
<Handle {/* Main output handle */}
type="source" {subBlockPositions.length === 0 && (
position={horizontalHandles ? Position.Right : Position.Bottom} <Handle
className={cn( type="source"
'!w-3.5 !h-3.5', position={horizontalHandles ? Position.Right : Position.Bottom}
'!bg-white !rounded-full !border !border-gray-200', className={cn(
'!opacity-0 group-hover:!opacity-100', '!w-3.5 !h-3.5',
'!transition-opacity !duration-200 !cursor-crosshair', '!bg-white !rounded-full !border !border-gray-200',
'hover:!border-blue-500', '!opacity-0 group-hover:!opacity-100',
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]' '!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> </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: { toolbar: {
title: 'Function', title: 'Function',
description: 'Add custom logic', description: 'Add custom logic',
bgColor: '#FF8D2F', bgColor: '#FF402F',
icon: CodeIcon, icon: CodeIcon,
category: 'blocks', category: 'blocks',
}, },
@@ -29,10 +29,8 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
subBlocks: [ subBlocks: [
{ {
id: 'code', id: 'code',
title: 'Code',
type: 'code', type: 'code',
layout: 'full', layout: 'full',
placeholder: 'Enter your code here...'
} }
], ],
}, },

View File

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

View File

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