-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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 2 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
File renamed without changes.
189 changes: 189 additions & 0 deletions
189
...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,189 @@ | ||
| '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-center gap-[6px] whitespace-nowrap font-medium text-[13px] text-[var(--text-primary)]'> | ||
| {title} | ||
| {isRequired && visibility === 'user-only' && <span className='ml-0.5'>*</span>} | ||
| {visibility !== 'user-only' && ( | ||
| <span className='ml-[6px] text-[12px] text-[var(--text-tertiary)]'>(optional)</span> | ||
| )} | ||
| </Label> | ||
|
waleedlatif1 marked this conversation as resolved.
|
||
| <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, useMemo, 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 | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Bridges the subblock store with StoredTool.params via a synthetic store key, | ||
| * then delegates all rendering to SubBlock for full parity. | ||
| * | ||
| * Two effects handle bidirectional sync: | ||
| * - tool.params → store (external changes) | ||
| * - store → tool.params (user interaction) | ||
| */ | ||
| 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] ?? '' | ||
|
|
||
| /** Tracks the last value we pushed to the store from tool.params to avoid echo loops */ | ||
| const lastPushedToStoreRef = useRef<string | null>(null) | ||
| /** Tracks the last value we synced back to tool.params from the store */ | ||
| const lastPushedToParamsRef = useRef<string | null>(null) | ||
|
|
||
| // Sync tool.params → store: push when the prop value changes (including first mount) | ||
| useEffect(() => { | ||
| if (!toolParamValue && lastPushedToStoreRef.current === null) { | ||
| // Skip initializing the store with an empty value on first mount — | ||
| // let the SubBlock component use its own default. | ||
| lastPushedToStoreRef.current = toolParamValue | ||
| lastPushedToParamsRef.current = toolParamValue | ||
| return | ||
| } | ||
| if (toolParamValue !== lastPushedToStoreRef.current) { | ||
| lastPushedToStoreRef.current = toolParamValue | ||
| lastPushedToParamsRef.current = toolParamValue | ||
| setStoreValue(toolParamValue) | ||
| } | ||
| }, [toolParamValue, setStoreValue]) | ||
|
|
||
| // Sync store → tool.params: push when the user changes the value via SubBlock | ||
| 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]) | ||
|
|
||
| // Determine if the parameter is optional for the user (LLM can fill it) | ||
| const visibility = subBlock.paramVisibility ?? 'user-or-llm' | ||
| const isOptionalForUser = visibility !== 'user-only' | ||
|
|
||
| const labelSuffix = useMemo( | ||
| () => | ||
| isOptionalForUser ? ( | ||
| <span className='ml-[6px] text-[12px] text-[var(--text-tertiary)]'>(optional)</span> | ||
| ) : null, | ||
| [isOptionalForUser] | ||
| ) | ||
|
|
||
| // Suppress SubBlock's "*" required indicator for optional-for-user params | ||
| const config = { | ||
| ...subBlock, | ||
| id: syntheticId, | ||
| ...(isOptionalForUser && { required: false }), | ||
| } | ||
|
|
||
| return ( | ||
| <SubBlock | ||
| blockId={blockId} | ||
| config={config} | ||
| isPreview={false} | ||
| disabled={disabled} | ||
| canonicalToggle={canonicalToggle} | ||
| labelSuffix={labelSuffix} | ||
| /> | ||
|
waleedlatif1 marked this conversation as resolved.
waleedlatif1 marked this conversation as resolved.
|
||
| ) | ||
|
waleedlatif1 marked this conversation as resolved.
|
||
| } | ||
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
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.