diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx new file mode 100644 index 0000000000..924862de58 --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/condition-input.tsx @@ -0,0 +1,455 @@ +import { useEffect, useRef, useState } from 'react' +import { ChevronDown, ChevronUp, Plus, Trash } from 'lucide-react' +import { highlight, languages } from 'prismjs' +import 'prismjs/components/prism-javascript' +import 'prismjs/themes/prism.css' +import Editor from 'react-simple-code-editor' +import { Handle, Position, useUpdateNodeInternals } from 'reactflow' +import { Button } from '@/components/ui/button' +import { EnvVarDropdown, checkEnvVarTrigger } from '@/components/ui/env-var-dropdown' +import { TagDropdown, checkTagTrigger } from '@/components/ui/tag-dropdown' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' +import { useSubBlockValue } from '../hooks/use-sub-block-value' + +interface ConditionalBlock { + id: string + title: string + value: string +} + +interface ConditionInputProps { + blockId: string + subBlockId: string + isConnecting: boolean +} + +export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionInputProps) { + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + const [lineCount, setLineCount] = useState(1) + const [showTags, setShowTags] = useState(false) + const [showEnvVars, setShowEnvVars] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const [cursorPosition, setCursorPosition] = useState(0) + const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const editorRef = useRef(null) + const [visualLineHeights, setVisualLineHeights] = useState<{ [key: string]: number[] }>({}) + const updateNodeInternals = useUpdateNodeInternals() + + // Initialize conditional blocks with empty values + const [conditionalBlocks, setConditionalBlocks] = useState([ + { id: crypto.randomUUID(), title: 'if', value: '' }, + ]) + + // Sync store value with conditional blocks on initial load + useEffect(() => { + if (storeValue !== null) { + try { + const parsedValue = JSON.parse(storeValue.toString()) + if (Array.isArray(parsedValue)) { + setConditionalBlocks(parsedValue) + } + } catch { + // If the store value isn't valid JSON, initialize with default block + setConditionalBlocks([{ id: crypto.randomUUID(), title: 'if', value: '' }]) + } + } + }, []) + + // Update store whenever conditional blocks change + useEffect(() => { + setStoreValue(JSON.stringify(conditionalBlocks)) + updateNodeInternals(`${blockId}-${subBlockId}`) + }, [conditionalBlocks, blockId, subBlockId]) + + // Update block value + const updateBlockValue = (blockId: string, newValue: string) => { + setConditionalBlocks((blocks) => + blocks.map((block) => (block.id === blockId ? { ...block, value: newValue } : block)) + ) + } + + // Update the line counting logic to be block-specific + useEffect(() => { + if (!editorRef.current) return + + const calculateVisualLines = () => { + const preElement = editorRef.current?.querySelector('pre') + if (!preElement) return + + const newVisualLineHeights: { [key: string]: number[] } = {} + + conditionalBlocks.forEach((block) => { + const lines = block.value.split('\n') + const blockVisualHeights: number[] = [] + + // Create a hidden container with the same width as the editor + const container = document.createElement('div') + container.style.cssText = ` + position: absolute; + visibility: hidden; + width: ${preElement.clientWidth}px; + font-family: ${window.getComputedStyle(preElement).fontFamily}; + font-size: ${window.getComputedStyle(preElement).fontSize}; + padding: 12px; + white-space: pre-wrap; + word-break: break-word; + ` + document.body.appendChild(container) + + // Process each line + lines.forEach((line) => { + const lineDiv = document.createElement('div') + + if (line.includes('<') && line.includes('>')) { + const parts = line.split(/(<[^>]+>)/g) + parts.forEach((part) => { + const span = document.createElement('span') + span.textContent = part + if (part.startsWith('<') && part.endsWith('>')) { + span.style.color = 'rgb(153, 0, 85)' + } + lineDiv.appendChild(span) + }) + } else { + lineDiv.textContent = line || ' ' + } + + container.appendChild(lineDiv) + + const actualHeight = lineDiv.getBoundingClientRect().height + const lineUnits = Math.ceil(actualHeight / 21) + blockVisualHeights.push(lineUnits) + + container.removeChild(lineDiv) + }) + + document.body.removeChild(container) + newVisualLineHeights[block.id] = blockVisualHeights + }) + + setVisualLineHeights(newVisualLineHeights) + } + + calculateVisualLines() + + const resizeObserver = new ResizeObserver(calculateVisualLines) + resizeObserver.observe(editorRef.current) + + return () => resizeObserver.disconnect() + }, [conditionalBlocks]) + + // Modify the line numbers rendering to be block-specific + const renderLineNumbers = (blockId: string) => { + const numbers: JSX.Element[] = [] + let lineNumber = 1 + const blockHeights = visualLineHeights[blockId] || [] + + blockHeights.forEach((height) => { + for (let i = 0; i < height; i++) { + numbers.push( +
0 && 'invisible')} + > + {lineNumber} +
+ ) + } + lineNumber++ + }) + + return numbers + } + + // Handle drops from connection blocks + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + try { + const data = JSON.parse(e.dataTransfer.getData('application/json')) + if (data.type !== 'connectionBlock') return + + // Get current cursor position from the textarea + const textarea = editorRef.current?.querySelector('textarea') + const dropPosition = + textarea?.selectionStart ?? conditionalBlocks.map((block) => block.value).join('\n').length + + // Insert '<' at drop position to trigger the dropdown + const newValue = + conditionalBlocks + .map((block) => block.value) + .join('\n') + .slice(0, dropPosition) + + '<' + + conditionalBlocks + .map((block) => block.value) + .join('\n') + .slice(dropPosition) + + updateBlockValue(data.connectionData?.sourceBlockId || '', newValue) + setCursorPosition(dropPosition + 1) + setShowTags(true) + + if (data.connectionData?.sourceBlockId) { + setActiveSourceBlockId(data.connectionData.sourceBlockId) + } + + // Set cursor position after state updates + setTimeout(() => { + if (textarea) { + textarea.selectionStart = dropPosition + 1 + textarea.selectionEnd = dropPosition + 1 + textarea.focus() + } + }, 0) + } catch (error) { + console.error('Failed to parse drop data:', error) + } + } + + // Handle tag selection + const handleTagSelect = (newValue: string) => { + updateBlockValue(activeSourceBlockId || '', newValue) + setShowTags(false) + setActiveSourceBlockId(null) + } + + // Handle environment variable selection + const handleEnvVarSelect = (newValue: string) => { + updateBlockValue(activeSourceBlockId || '', newValue) + setShowEnvVars(false) + } + + // Update block titles based on position + const updateBlockTitles = (blocks: ConditionalBlock[]): ConditionalBlock[] => { + return blocks.map((block, index) => ({ + ...block, + title: index === 0 ? 'if' : index === blocks.length - 1 ? 'else' : 'else if', + })) + } + + // Update these functions to use updateBlockTitles + const addBlock = (afterId: string) => { + const blockIndex = conditionalBlocks.findIndex((block) => block.id === afterId) + const newBlock = { id: crypto.randomUUID(), title: '', value: '' } + + const newBlocks = [...conditionalBlocks] + newBlocks.splice(blockIndex + 1, 0, newBlock) + setConditionalBlocks(updateBlockTitles(newBlocks)) + } + + const removeBlock = (id: string) => { + if (conditionalBlocks.length === 1) return + setConditionalBlocks((blocks) => updateBlockTitles(blocks.filter((block) => block.id !== id))) + } + + const moveBlock = (id: string, direction: 'up' | 'down') => { + const blockIndex = conditionalBlocks.findIndex((block) => block.id === id) + if ( + (direction === 'up' && blockIndex === 0) || + (direction === 'down' && blockIndex === conditionalBlocks.length - 1) + ) + return + + const newBlocks = [...conditionalBlocks] + const targetIndex = direction === 'up' ? blockIndex - 1 : blockIndex + 1 + ;[newBlocks[blockIndex], newBlocks[targetIndex]] = [ + newBlocks[targetIndex], + newBlocks[blockIndex], + ] + setConditionalBlocks(updateBlockTitles(newBlocks)) + } + + return ( +
+ {conditionalBlocks.map((block, index) => ( +
+
+ {block.title} + +
+ + + + + Add Block + + +
+ + + + + Move Up + + + + + + + Move Down + +
+ + + + + + Delete Condition + +
+
+
e.preventDefault()} + onDrop={handleDrop} + > + {/* Line numbers */} + + +
+ {block.value.length === 0 && ( +
+ {' === true'} +
+ )} + { + updateBlockValue(block.id, newCode) + + // Check for tag trigger and environment variable trigger + const textarea = editorRef.current?.querySelector('textarea') + if (textarea) { + const pos = textarea.selectionStart + setCursorPosition(pos) + + const tagTrigger = checkTagTrigger(newCode, pos) + setShowTags(tagTrigger.show) + if (!tagTrigger.show) { + setActiveSourceBlockId(null) + } + + const envVarTrigger = checkEnvVarTrigger(newCode, pos) + setShowEnvVars(envVarTrigger.show) + setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '') + } + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + setShowTags(false) + setShowEnvVars(false) + } + }} + highlight={(code) => highlight(code, languages.javascript, 'javascript')} + padding={12} + style={{ + fontFamily: 'inherit', + minHeight: '46px', + lineHeight: '21px', + }} + className="focus:outline-none" + textareaClassName="focus:outline-none focus:ring-0 bg-transparent" + /> + + {showEnvVars && ( + { + setShowEnvVars(false) + setSearchTerm('') + }} + /> + )} + + {showTags && ( + { + setShowTags(false) + setActiveSourceBlockId(null) + }} + /> + )} +
+
+
+ ))} +
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index af8e178122..8b33c0b253 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -2,6 +2,7 @@ import { Label } from '@/components/ui/label' import { SubBlockConfig } from '../../../../../../../blocks/types' import { CheckboxList } from './components/checkbox-list' import { Code } from './components/code' +import { ConditionInput } from './components/condition-input' import { Dropdown } from './components/dropdown' import { LongInput } from './components/long-input' import { ShortInput } from './components/short-input' @@ -89,6 +90,10 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { layout={config.layout} /> ) + case 'condition-input': + return ( + + ) default: return null } diff --git a/app/w/[id]/components/workflow-block/workflow-block.tsx b/app/w/[id]/components/workflow-block/workflow-block.tsx index f19845d41e..780553f345 100644 --- a/app/w/[id]/components/workflow-block/workflow-block.tsx +++ b/app/w/[id]/components/workflow-block/workflow-block.tsx @@ -26,7 +26,6 @@ interface SubBlockPosition { export function WorkflowBlock({ id, type, config, name, selected }: WorkflowBlockProps) { const { toolbar, workflow } = config - // Dragging connection state const [isConnecting, setIsConnecting] = useState(false) const isEnabled = useWorkflowStore((state) => state.blocks[id]?.enabled ?? true) const horizontalHandles = useWorkflowStore( @@ -36,62 +35,12 @@ export function WorkflowBlock({ id, type, config, name, selected }: WorkflowBloc const [editedName, setEditedName] = useState('') const updateBlockName = useWorkflowStore((state) => state.updateBlockName) const blockRef = useRef(null) - const [subBlockPositions, setSubBlockPositions] = useState([]) const updateNodeInternals = useUpdateNodeInternals() - // Add a small delay to ensure DOM is ready + // Add effect to update node internals when handles change useEffect(() => { - const calculatePositions = () => { - if (!blockRef.current) return - - // Add setTimeout to ensure styles are applied - setTimeout(() => { - const positions: SubBlockPosition[] = [] - const blockRect = blockRef.current?.getBoundingClientRect() - - if (!blockRect) return - - 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, - top: subBlockRect.bottom - blockRect.top - 25, - }) - } - }) - - setSubBlockPositions(positions) - updateNodeInternals(id) - }, 0) - } - - // Calculate initial positions with a slight delay - const initialTimer = setTimeout(calculatePositions, 50) - - // 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 () => { - clearTimeout(initialTimer) - resizeObserver.disconnect() - window.removeEventListener('resize', calculatePositions) - } - }, [workflow.subBlocks, id, updateNodeInternals]) + updateNodeInternals(id) + }, [id, horizontalHandles]) function groupSubBlocks(subBlocks: SubBlockConfig[]) { // Filter out hidden subblocks @@ -157,6 +106,7 @@ export function WorkflowBlock({ id, type, config, name, selected }: WorkflowBloc
@@ -208,7 +162,6 @@ export function WorkflowBlock({ id, type, config, name, selected }: WorkflowBloc
@@ -217,11 +170,12 @@ export function WorkflowBlock({ id, type, config, name, selected }: WorkflowBloc ))}
- {/* Main output handle */} - {subBlockPositions.length === 0 && ( + {/* Main output handle - only render if not a condition block */} + {type !== 'condition' && ( )} - - {/* Subblock output handles */} - {subBlockPositions.map((position) => ( - - ))} ) } diff --git a/blocks/blocks/condition.ts b/blocks/blocks/condition.ts index e09852afd4..c231a460bf 100644 --- a/blocks/blocks/condition.ts +++ b/blocks/blocks/condition.ts @@ -28,9 +28,8 @@ export const ConditionBlock: BlockConfig = { }, subBlocks: [ { - id: 'if', - title: 'if', - type: 'code', + id: 'conditions', + type: 'condition-input', layout: 'full', }, ], diff --git a/blocks/types.ts b/blocks/types.ts index 0c27626610..5fa81845cb 100644 --- a/blocks/types.ts +++ b/blocks/types.ts @@ -19,7 +19,7 @@ export type SubBlockType = | 'switch' | 'tool-input' | 'checkbox-list' - + | 'condition-input' export type SubBlockLayout = 'full' | 'half' // Tool output type utilities @@ -75,7 +75,6 @@ export interface SubBlockConfig { placeholder?: string password?: boolean connectionDroppable?: boolean - outputHandle?: boolean hidden?: boolean value?: (params: Record) => string minimizable?: boolean