-
Notifications
You must be signed in to change notification settings - Fork 3.3k
refactor(tool-input): subblock-first rendering, component extraction, bug fixes #3207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
d236cc8
refactor(tool-input): eliminate SyncWrappers, add canonical toggle an…
waleedlatif1 a0ebe08
fix(tool-input): restore optional indicator, fix folder selector and …
waleedlatif1 8af5617
add sibling values to subblock context since subblock store isn't rel…
waleedlatif1 a29afd2
cleanup
waleedlatif1 c43f502
fix(tool-input): render uncovered tool params alongside subblocks
waleedlatif1 41ed859
fix(tool-input): auto-refresh workflow inputs after redeploy
waleedlatif1 a25b26e
fix(tool-input): correct workflow selector visibility and tighten (op…
waleedlatif1 3e17627
fix(tool-input): align (optional) text to baseline instead of center
waleedlatif1 b65768b
fix(tool-input): increase top padding of expanded tool body
waleedlatif1 f707636
fix(tool-input): apply extra top padding only to SubBlock-first path
waleedlatif1 837a13e
fix(tool-input): increase gap between SubBlock params for visual clarity
waleedlatif1 54ed579
fix spacing and optional tag
waleedlatif1 b1cde02
update styling + move predeploy checks earlier for first time deploys
waleedlatif1 6ee73fa
update change detection to account for synthetic tool ids
waleedlatif1 030c61b
fix remaining blocks who had files visibility set to hidden
waleedlatif1 a76e6e9
cleanup
waleedlatif1 e15f3dc
add catch
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
...mponents/editor/components/sub-block/components/tool-input/components/tools/parameter.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| 'use client' | ||
|
|
||
| import type React from 'react' | ||
| import { useRef, useState } from 'react' | ||
| import { ArrowLeftRight, ArrowUp } from 'lucide-react' | ||
| import { Button, Input, Label, Tooltip } from '@/components/emcn' | ||
| import { cn } from '@/lib/core/utils/cn' | ||
| import type { WandControlHandlers } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' | ||
|
|
||
| /** | ||
| * Props for a generic parameter with label component | ||
| */ | ||
| export interface ParameterWithLabelProps { | ||
| paramId: string | ||
| title: string | ||
| isRequired: boolean | ||
| visibility: string | ||
| wandConfig?: { | ||
| enabled: boolean | ||
| prompt?: string | ||
| placeholder?: string | ||
| } | ||
| canonicalToggle?: { | ||
| mode: 'basic' | 'advanced' | ||
| disabled?: boolean | ||
| onToggle?: () => void | ||
| } | ||
| disabled: boolean | ||
| isPreview: boolean | ||
| children: (wandControlRef: React.MutableRefObject<WandControlHandlers | null>) => React.ReactNode | ||
| } | ||
|
|
||
| /** | ||
| * Generic wrapper component for parameters that manages wand state and renders label + input | ||
| */ | ||
| export function ParameterWithLabel({ | ||
| paramId, | ||
| title, | ||
| isRequired, | ||
| visibility, | ||
| wandConfig, | ||
| canonicalToggle, | ||
| disabled, | ||
| isPreview, | ||
| children, | ||
| }: ParameterWithLabelProps) { | ||
| const [isSearchActive, setIsSearchActive] = useState(false) | ||
| const [searchQuery, setSearchQuery] = useState('') | ||
| const searchInputRef = useRef<HTMLInputElement>(null) | ||
| const wandControlRef = useRef<WandControlHandlers | null>(null) | ||
|
|
||
| const isWandEnabled = wandConfig?.enabled ?? false | ||
| const showWand = isWandEnabled && !isPreview && !disabled | ||
|
|
||
| const handleSearchClick = (): void => { | ||
| setIsSearchActive(true) | ||
| setTimeout(() => { | ||
| searchInputRef.current?.focus() | ||
| }, 0) | ||
| } | ||
|
|
||
| const handleSearchBlur = (): void => { | ||
| if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) { | ||
| setIsSearchActive(false) | ||
| } | ||
| } | ||
|
|
||
| const handleSearchChange = (value: string): void => { | ||
| setSearchQuery(value) | ||
| } | ||
|
|
||
| const handleSearchSubmit = (): void => { | ||
| if (searchQuery.trim() && wandControlRef.current) { | ||
| wandControlRef.current.onWandTrigger(searchQuery) | ||
| setSearchQuery('') | ||
| setIsSearchActive(false) | ||
| } | ||
| } | ||
|
|
||
| const handleSearchCancel = (): void => { | ||
| setSearchQuery('') | ||
| setIsSearchActive(false) | ||
| } | ||
|
|
||
| const isStreaming = wandControlRef.current?.isWandStreaming ?? false | ||
|
|
||
| return ( | ||
| <div key={paramId} className='relative min-w-0 space-y-[6px]'> | ||
| <div className='flex items-center justify-between gap-[6px] pl-[2px]'> | ||
| <Label className='flex items-baseline gap-[6px] whitespace-nowrap font-medium text-[13px] text-[var(--text-primary)]'> | ||
| {title} | ||
| {isRequired && visibility === 'user-only' && <span className='ml-0.5'>*</span>} | ||
| </Label> | ||
| <div className='flex min-w-0 flex-1 items-center justify-end gap-[6px]'> | ||
| {showWand && | ||
| (!isSearchActive ? ( | ||
| <Button | ||
| variant='active' | ||
| className='-my-1 h-5 px-2 py-0 text-[11px]' | ||
| onClick={handleSearchClick} | ||
| > | ||
| Generate | ||
| </Button> | ||
| ) : ( | ||
| <div className='-my-1 flex min-w-[120px] max-w-[280px] flex-1 items-center gap-[4px]'> | ||
| <Input | ||
| ref={searchInputRef} | ||
| value={isStreaming ? 'Generating...' : searchQuery} | ||
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => | ||
| handleSearchChange(e.target.value) | ||
| } | ||
| onBlur={(e: React.FocusEvent<HTMLInputElement>) => { | ||
| const relatedTarget = e.relatedTarget as HTMLElement | null | ||
| if (relatedTarget?.closest('button')) return | ||
| handleSearchBlur() | ||
| }} | ||
| onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.key === 'Enter' && searchQuery.trim() && !isStreaming) { | ||
| handleSearchSubmit() | ||
| } else if (e.key === 'Escape') { | ||
| handleSearchCancel() | ||
| } | ||
| }} | ||
| disabled={isStreaming} | ||
| className={cn( | ||
| 'h-5 min-w-[80px] flex-1 text-[11px]', | ||
| isStreaming && 'text-muted-foreground' | ||
| )} | ||
| placeholder='Generate with AI...' | ||
| /> | ||
| <Button | ||
| variant='tertiary' | ||
| disabled={!searchQuery.trim() || isStreaming} | ||
| onMouseDown={(e: React.MouseEvent) => { | ||
| e.preventDefault() | ||
| e.stopPropagation() | ||
| }} | ||
| onClick={(e: React.MouseEvent) => { | ||
| e.stopPropagation() | ||
| handleSearchSubmit() | ||
| }} | ||
| className='h-[20px] w-[20px] flex-shrink-0 p-0' | ||
| > | ||
| <ArrowUp className='h-[12px] w-[12px]' /> | ||
| </Button> | ||
| </div> | ||
| ))} | ||
| {canonicalToggle && !isPreview && ( | ||
| <Tooltip.Root> | ||
| <Tooltip.Trigger asChild> | ||
| <button | ||
| type='button' | ||
| className='flex h-[12px] w-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50' | ||
| onClick={canonicalToggle.onToggle} | ||
| disabled={canonicalToggle.disabled || disabled} | ||
| aria-label={ | ||
| canonicalToggle.mode === 'advanced' | ||
| ? 'Switch to selector' | ||
| : 'Switch to manual ID' | ||
| } | ||
| > | ||
| <ArrowLeftRight | ||
| className={cn( | ||
| '!h-[12px] !w-[12px]', | ||
| canonicalToggle.mode === 'advanced' | ||
| ? 'text-[var(--text-primary)]' | ||
| : 'text-[var(--text-secondary)]' | ||
| )} | ||
| /> | ||
| </button> | ||
| </Tooltip.Trigger> | ||
| <Tooltip.Content side='top'> | ||
| <p> | ||
| {canonicalToggle.mode === 'advanced' | ||
| ? 'Switch to selector' | ||
| : 'Switch to manual ID'} | ||
| </p> | ||
| </Tooltip.Content> | ||
| </Tooltip.Root> | ||
| )} | ||
| </div> | ||
| </div> | ||
| <div className='relative w-full min-w-0'>{children(wandControlRef)}</div> | ||
| </div> | ||
| ) | ||
| } | ||
109 changes: 109 additions & 0 deletions
109
...editor/components/sub-block/components/tool-input/components/tools/sub-block-renderer.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| 'use client' | ||
|
|
||
| import { useEffect, useRef } from 'react' | ||
| import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' | ||
| import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block' | ||
| import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types' | ||
|
|
||
| interface ToolSubBlockRendererProps { | ||
| blockId: string | ||
| subBlockId: string | ||
| toolIndex: number | ||
| subBlock: BlockSubBlockConfig | ||
| effectiveParamId: string | ||
| toolParams: Record<string, string> | undefined | ||
| onParamChange: (toolIndex: number, paramId: string, value: string) => void | ||
| disabled: boolean | ||
| canonicalToggle?: { | ||
| mode: 'basic' | 'advanced' | ||
| disabled?: boolean | ||
| onToggle?: () => void | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * SubBlock types whose store values are objects/arrays/non-strings. | ||
| * tool.params stores strings (via JSON.stringify), so when syncing | ||
| * back to the store we parse them to restore the native shape. | ||
| */ | ||
| const OBJECT_SUBBLOCK_TYPES = new Set(['file-upload', 'table', 'grouped-checkbox-list']) | ||
|
|
||
| /** | ||
| * Bridges the subblock store with StoredTool.params via a synthetic store key, | ||
| * then delegates all rendering to SubBlock for full parity. | ||
| */ | ||
| export function ToolSubBlockRenderer({ | ||
| blockId, | ||
| subBlockId, | ||
| toolIndex, | ||
| subBlock, | ||
| effectiveParamId, | ||
| toolParams, | ||
| onParamChange, | ||
| disabled, | ||
| canonicalToggle, | ||
| }: ToolSubBlockRendererProps) { | ||
| const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}` | ||
| const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId) | ||
|
|
||
| const toolParamValue = toolParams?.[effectiveParamId] ?? '' | ||
| const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type) | ||
|
|
||
| const lastPushedToStoreRef = useRef<string | null>(null) | ||
| const lastPushedToParamsRef = useRef<string | null>(null) | ||
|
|
||
| useEffect(() => { | ||
| if (!toolParamValue && lastPushedToStoreRef.current === null) { | ||
| lastPushedToStoreRef.current = toolParamValue | ||
| lastPushedToParamsRef.current = toolParamValue | ||
| return | ||
| } | ||
| if (toolParamValue !== lastPushedToStoreRef.current) { | ||
| lastPushedToStoreRef.current = toolParamValue | ||
| lastPushedToParamsRef.current = toolParamValue | ||
|
|
||
| if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) { | ||
| try { | ||
| const parsed = JSON.parse(toolParamValue) | ||
| if (typeof parsed === 'object' && parsed !== null) { | ||
| setStoreValue(parsed) | ||
| return | ||
| } | ||
| } catch { | ||
| // Not valid JSON — fall through to set as string | ||
| } | ||
| } | ||
| setStoreValue(toolParamValue) | ||
| } | ||
| }, [toolParamValue, setStoreValue, isObjectType]) | ||
|
|
||
| useEffect(() => { | ||
| if (storeValue == null) return | ||
| const stringValue = typeof storeValue === 'string' ? storeValue : JSON.stringify(storeValue) | ||
| if (stringValue !== lastPushedToParamsRef.current) { | ||
| lastPushedToParamsRef.current = stringValue | ||
| lastPushedToStoreRef.current = stringValue | ||
| onParamChange(toolIndex, effectiveParamId, stringValue) | ||
| } | ||
| }, [storeValue, toolIndex, effectiveParamId, onParamChange]) | ||
|
|
||
| const visibility = subBlock.paramVisibility ?? 'user-or-llm' | ||
| const isOptionalForUser = visibility !== 'user-only' | ||
|
|
||
| const config = { | ||
| ...subBlock, | ||
| id: syntheticId, | ||
| ...(isOptionalForUser && { required: false }), | ||
| } | ||
|
|
||
| return ( | ||
| <SubBlock | ||
| blockId={blockId} | ||
| config={config} | ||
| isPreview={false} | ||
| disabled={disabled} | ||
| canonicalToggle={canonicalToggle} | ||
| dependencyContext={toolParams} | ||
| /> | ||
waleedlatif1 marked this conversation as resolved.
Show resolved
Hide resolved
waleedlatif1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
waleedlatif1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.