mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
Added dynamic handles; added conditional block; changed function block
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
46
blocks/blocks/condition.ts
Normal file
46
blocks/blocks/condition.ts
Normal 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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...'
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user