diff --git a/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 4ca5ac107..e13d34156 100644 --- a/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -26,7 +26,7 @@ export function ConnectionBlocks({ id: connection.id, name: connection.name, outputType: connection.outputType, - sourceBlockId: blockId, + sourceBlockId: connection.id, }, }) ) diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx index 27d4ead57..5e668e453 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx @@ -4,7 +4,10 @@ import { cn } from '@/lib/utils' import { useState, useRef } from 'react' import { SubBlockConfig } from '@/blocks/types' import { formatDisplayText } from '@/components/ui/formatted-text' -import { EnvVarDropdown, checkEnvVarTrigger } from '@/components/ui/env-var-dropdown' +import { + EnvVarDropdown, + checkEnvVarTrigger, +} from '@/components/ui/env-var-dropdown' import { TagDropdown, checkTagTrigger } from '@/components/ui/tag-dropdown' import { useWorkflowStore } from '@/stores/workflow/store' @@ -30,6 +33,9 @@ export function LongInput({ const [cursorPosition, setCursorPosition] = useState(0) const textareaRef = useRef(null) const overlayRef = useRef(null) + const [activeSourceBlockId, setActiveSourceBlockId] = useState( + null + ) // Handle input changes const handleChange = (e: React.ChangeEvent) => { @@ -37,12 +43,12 @@ export function LongInput({ const newCursorPosition = e.target.selectionStart ?? 0 setValue(newValue) setCursorPosition(newCursorPosition) - + // Check for environment variables trigger const envVarTrigger = checkEnvVarTrigger(newValue, newCursorPosition) setShowEnvVars(envVarTrigger.show) setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '') - + // Check for tag trigger const tagTrigger = checkTagTrigger(newValue, newCursorPosition) setShowTags(tagTrigger.show) @@ -71,12 +77,16 @@ export function LongInput({ if (data.type !== 'connectionBlock') return // Get current cursor position or append to end - const dropPosition = textareaRef.current?.selectionStart ?? value?.toString().length ?? 0 - + const dropPosition = + textareaRef.current?.selectionStart ?? value?.toString().length ?? 0 + // Insert '<' at drop position to trigger the dropdown const currentValue = value?.toString() ?? '' - const newValue = currentValue.slice(0, dropPosition) + '<' + currentValue.slice(dropPosition) - + const newValue = + currentValue.slice(0, dropPosition) + + '<' + + currentValue.slice(dropPosition) + // Focus the textarea first textareaRef.current?.focus() @@ -86,6 +96,11 @@ export function LongInput({ setCursorPosition(dropPosition + 1) setShowTags(true) + // Pass the source block ID from the dropped connection + if (data.connectionData?.sourceBlockId) { + setActiveSourceBlockId(data.connectionData.sourceBlockId) + } + // Set cursor position after state updates setTimeout(() => { if (textareaRef.current) { @@ -152,10 +167,12 @@ export function LongInput({ visible={showTags} onSelect={setValue} blockId={blockId} + activeSourceBlockId={activeSourceBlockId} inputValue={value?.toString() ?? ''} cursorPosition={cursorPosition} onClose={() => { setShowTags(false) + setActiveSourceBlockId(null) }} /> diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx index 7fd47f636..d75e05eb5 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/short-input.tsx @@ -39,6 +39,9 @@ export function ShortInput({ const [cursorPosition, setCursorPosition] = useState(0) const inputRef = useRef(null) const overlayRef = useRef(null) + const [activeSourceBlockId, setActiveSourceBlockId] = useState( + null + ) // Use either controlled or uncontrolled value const value = propValue !== undefined ? propValue : storeValue @@ -120,6 +123,11 @@ export function ShortInput({ setCursorPosition(dropPosition + 1) setShowTags(true) + // Pass the source block ID from the dropped connection + if (data.connectionData?.sourceBlockId) { + setActiveSourceBlockId(data.connectionData.sourceBlockId) + } + // Set cursor position after state updates setTimeout(() => { if (inputRef.current) { @@ -206,10 +214,12 @@ export function ShortInput({ visible={showTags} onSelect={handleEnvVarSelect} blockId={blockId} + activeSourceBlockId={activeSourceBlockId} inputValue={value?.toString() ?? ''} cursorPosition={cursorPosition} onClose={() => { setShowTags(false) + setActiveSourceBlockId(null) }} /> diff --git a/components/ui/tag-dropdown.tsx b/components/ui/tag-dropdown.tsx index 0817a1acc..739e923ea 100644 --- a/components/ui/tag-dropdown.tsx +++ b/components/ui/tag-dropdown.tsx @@ -6,6 +6,7 @@ interface TagDropdownProps { visible: boolean onSelect: (newValue: string) => void blockId: string + activeSourceBlockId: string | null className?: string inputValue: string cursorPosition: number @@ -16,6 +17,7 @@ export const TagDropdown: React.FC = ({ visible, onSelect, blockId, + activeSourceBlockId, className, inputValue, cursorPosition, @@ -26,80 +28,127 @@ export const TagDropdown: React.FC = ({ // Get available tags from workflow state const blocks = useWorkflowStore((state) => state.blocks) const edges = useWorkflowStore((state) => state.edges) - + + // Extract search term from input + const searchTerm = useMemo(() => { + const textBeforeCursor = inputValue.slice(0, cursorPosition) + const match = textBeforeCursor.match(/<([^>]*)$/) + return match ? match[1].toLowerCase() : '' + }, [inputValue, cursorPosition]) + // Get source block and compute tags const { tags } = useMemo(() => { - const sourceEdge = edges.find(edge => edge.target === blockId) - const sourceBlock = sourceEdge ? blocks[sourceEdge.source] : null - - if (!sourceBlock) { - return { tags: [] } - } + // If we have an active source block ID from a drop, use that specific block only + if (activeSourceBlockId) { + const sourceBlock = blocks[activeSourceBlockId] + if (!sourceBlock) return { tags: [] } - // Get all available output paths recursively - const getOutputPaths = (obj: any, prefix = ''): string[] => { - // If we're at a primitive type or null, return the current path - if (typeof obj !== 'object' || obj === null) { - return prefix ? [prefix] : [] + const getOutputPaths = (obj: any, prefix = ''): string[] => { + if (typeof obj !== 'object' || obj === null) { + return prefix ? [prefix] : [] + } + + if ('type' in obj) { + return getOutputPaths(obj.type, prefix) + } + + return Object.entries(obj).flatMap(([key, value]) => { + const newPrefix = prefix ? `${prefix}.${key}` : key + return getOutputPaths(value, newPrefix) + }) } - // If we have a type field, this is a block output definition - if ('type' in obj) { - return getOutputPaths(obj.type, prefix) + const outputPaths = getOutputPaths(sourceBlock.outputs) + const blockName = sourceBlock.name || sourceBlock.type + + return { + tags: outputPaths.map( + (path) => `${blockName.replace(/\s+/g, '').toLowerCase()}.${path}` + ), + } + } + + // Otherwise, show tags from all incoming connections + const sourceEdges = edges.filter((edge) => edge.target === blockId) + const sourceTags = sourceEdges.flatMap((edge) => { + const sourceBlock = blocks[edge.source] + if (!sourceBlock) return [] + + const getOutputPaths = (obj: any, prefix = ''): string[] => { + if (typeof obj !== 'object' || obj === null) { + return prefix ? [prefix] : [] + } + + if ('type' in obj) { + return getOutputPaths(obj.type, prefix) + } + + return Object.entries(obj).flatMap(([key, value]) => { + const newPrefix = prefix ? `${prefix}.${key}` : key + return getOutputPaths(value, newPrefix) + }) } - // Otherwise, traverse the object's properties - return Object.entries(obj).flatMap(([key, value]) => { - const newPrefix = prefix ? `${prefix}.${key}` : key - return getOutputPaths(value, newPrefix) - }) - } + const outputPaths = getOutputPaths(sourceBlock.outputs) + const blockName = sourceBlock.name || sourceBlock.type - // Get all output paths starting from the outputs object - const outputPaths = getOutputPaths(sourceBlock.outputs) - const blockName = sourceBlock.name || sourceBlock.type + return outputPaths.map( + (path) => `${blockName.replace(/\s+/g, '').toLowerCase()}.${path}` + ) + }) - // Format tags with block name and output paths - return { - tags: outputPaths.map(path => `${blockName.replace(/\s+/g, '').toLowerCase()}.${path}`) - } - }, [blocks, edges, blockId]) + return { tags: sourceTags } + }, [blocks, edges, blockId, activeSourceBlockId]) + + // Filter tags based on search term + const filteredTags = useMemo(() => { + if (!searchTerm) return tags + return tags.filter((tag) => tag.toLowerCase().includes(searchTerm)) + }, [tags, searchTerm]) + + // Reset selection when filtered results change + useEffect(() => { + setSelectedIndex(0) + }, [searchTerm]) // Handle tag selection const handleTagSelect = (tag: string) => { const textBeforeCursor = inputValue.slice(0, cursorPosition) const textAfterCursor = inputValue.slice(cursorPosition) - + // Find the position of the last '<' before cursor const lastOpenBracket = textBeforeCursor.lastIndexOf('<') if (lastOpenBracket === -1) return - - const newValue = textBeforeCursor.slice(0, lastOpenBracket) + - '<' + tag + '>' + - textAfterCursor - + + const newValue = + textBeforeCursor.slice(0, lastOpenBracket) + + '<' + + tag + + '>' + + textAfterCursor + onSelect(newValue) onClose?.() } // Handle keyboard navigation const handleKeyDown = (e: KeyboardEvent) => { - if (!visible || tags.length === 0) return + if (!visible || filteredTags.length === 0) return switch (e.key) { case 'ArrowDown': e.preventDefault() - setSelectedIndex(prev => - prev < tags.length - 1 ? prev + 1 : prev + setSelectedIndex((prev) => + prev < filteredTags.length - 1 ? prev + 1 : prev ) break case 'ArrowUp': e.preventDefault() - setSelectedIndex(prev => prev > 0 ? prev - 1 : prev) + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev)) break case 'Enter': e.preventDefault() - handleTagSelect(tags[selectedIndex]) + handleTagSelect(filteredTags[selectedIndex]) break case 'Escape': e.preventDefault() @@ -114,7 +163,7 @@ export const TagDropdown: React.FC = ({ window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) } - }, [visible, selectedIndex, tags]) + }, [visible, selectedIndex, filteredTags]) // Don't render if not visible or no tags if (!visible || tags.length === 0) return null @@ -122,46 +171,57 @@ export const TagDropdown: React.FC = ({ return (
- {tags.map((tag, index) => ( - - ))} + {filteredTags.length === 0 ? ( +
+ No matching tags found +
+ ) : ( + filteredTags.map((tag, index) => ( + + )) + )}
) } // Helper function to check for '<' trigger -export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => { +export const checkTagTrigger = ( + text: string, + cursorPosition: number +): { show: boolean } => { if (cursorPosition >= 1) { const textBeforeCursor = text.slice(0, cursorPosition) const lastOpenBracket = textBeforeCursor.lastIndexOf('<') const lastCloseBracket = textBeforeCursor.lastIndexOf('>') - + // Show if we have an unclosed '<' that's not part of a completed tag - if (lastOpenBracket !== -1 && (lastCloseBracket === -1 || lastCloseBracket < lastOpenBracket)) { + if ( + lastOpenBracket !== -1 && + (lastCloseBracket === -1 || lastCloseBracket < lastOpenBracket) + ) { return { show: true } } } return { show: false } -} - +}