From 58889f96d0b140d1fd5af2607962f334e456b3b8 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Wed, 19 Nov 2025 13:06:05 -0800 Subject: [PATCH 1/3] improvement(ui): workflow-block border --- .../w/[workflowId]/components/workflow-block/workflow-block.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index cdced8dd364..7938954b5f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -794,7 +794,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ ref={contentRef} onClick={handleClick} className={cn( - 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] bg-[var(--surface-2)]' + 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]' )} > {isPending && ( From d135f3a7b57f1b60e72d2f6597c25e910cc70017 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Wed, 19 Nov 2025 15:48:50 -0800 Subject: [PATCH 2/3] feat(chat): add inputs button --- apps/sim/app/globals.css | 2 + .../w/[workflowId]/components/chat/chat.tsx | 208 ++++++++++++++++-- .../components/chat/hooks/use-chat-resize.ts | 59 ++++- .../components/panel-new/panel-new.tsx | 27 +-- .../components/terminal/terminal.tsx | 3 +- apps/sim/stores/chat/store.ts | 2 +- 6 files changed, 248 insertions(+), 53 deletions(-) diff --git a/apps/sim/app/globals.css b/apps/sim/app/globals.css index 5da83a32b22..8e63a273ff3 100644 --- a/apps/sim/app/globals.css +++ b/apps/sim/app/globals.css @@ -281,6 +281,8 @@ --c-F4F4F4: #f4f4f4; --c-F5F5F5: #f5f5f5; + --c-CFCFCF: #cfcfcf; + /* Blues and cyans */ --c-00B0B0: #00b0b0; --c-264F78: #264f78; diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index c6e2168b037..943fd0545f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -20,6 +20,8 @@ import { extractPathFromOutputId, parseOutputContentSafely, } from '@/lib/response-format' +// import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types' +// import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers' import { cn } from '@/lib/utils' import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' @@ -28,6 +30,8 @@ import { getChatPosition, useChatStore } from '@/stores/chat/store' import { useExecutionStore } from '@/stores/execution/store' import { useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +// import { useSubBlockStore } from '@/stores/workflows/subblock/store' +// import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { ChatMessage, OutputSelect } from './components' import { useChatBoundarySync, useChatDrag, useChatFileUpload, useChatResize } from './hooks' @@ -124,6 +128,39 @@ const formatOutputContent = (output: any): string => { return '' } +// interface StartInputFormatField { +// id?: string +// name?: string +// type?: string +// value?: unknown +// collapsed?: boolean +// } + +// /** +// * Normalizes an input format value into a list of valid fields. +// * +// * @param value - Raw input format value from subblock state. +// * @returns Array of fields with non-empty names. +// */ +// const normalizeStartInputFormat = (value: unknown): StartInputFormatField[] => { +// if (!Array.isArray(value)) { +// return [] +// } + +// return value.filter((field): field is StartInputFormatField => { +// if (!field || typeof field !== 'object') { +// return false +// } + +// const name = (field as StartInputFormatField).name +// return typeof name === 'string' && name.trim() !== '' +// }) +// } + +const CHAT_START_EXAMPLE_JSON = `"input": string, +"conversationId": string, +"files": array` + /** * Floating chat modal component * @@ -140,6 +177,9 @@ export function Chat() { const params = useParams() const workspaceId = params.workspaceId as string const { activeWorkflowId } = useWorkflowRegistry() + // const blocks = useWorkflowStore((state) => state.blocks) + // const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate) + // const setSubBlockValue = useSubBlockStore((state) => state.setValue) // Chat state (UI and messages from unified store) const { @@ -190,6 +230,69 @@ export function Chat() { handleDrop, } = useChatFileUpload() + /** + * Resolves the unified start block for chat execution, if available. + */ + // const startBlockCandidate = useMemo(() => { + // if (!activeWorkflowId) { + // return null + // } + + // if (!blocks || Object.keys(blocks).length === 0) { + // return null + // } + + // const candidate = TriggerUtils.findStartBlock(blocks, 'chat') + // if (!candidate || candidate.path !== StartBlockPath.UNIFIED) { + // return null + // } + + // return candidate + // }, [activeWorkflowId, blocks]) + + // const startBlockId = startBlockCandidate?.blockId ?? null + + // /** + // * Reads the current input format for the unified start block from the subblock store, + // * falling back to the workflow store if no explicit value is stored yet. + // */ + // const startBlockInputFormat = useSubBlockStore((state) => { + // if (!activeWorkflowId || !startBlockId) { + // return null + // } + + // const workflowValues = state.workflowValues[activeWorkflowId] + // const fromStore = workflowValues?.[startBlockId]?.inputFormat + // if (fromStore !== undefined && fromStore !== null) { + // return fromStore + // } + + // const startBlock = blocks[startBlockId] + // return startBlock?.subBlocks?.inputFormat?.value ?? null + // }) + + // /** + // * Determines which reserved start inputs are missing from the input format. + // */ + // const missingStartReservedFields = useMemo(() => { + // if (!startBlockId) { + // return START_BLOCK_RESERVED_FIELDS + // } + + // const normalizedFields = normalizeStartInputFormat(startBlockInputFormat) + // const existingNames = new Set( + // normalizedFields + // .map((field) => field.name) + // .filter((name): name is string => typeof name === 'string' && name.trim() !== '') + // .map((name) => name.trim()) + // ) + + // return START_BLOCK_RESERVED_FIELDS.filter((fieldName) => !existingNames.has(fieldName)) + // }, [startBlockId, startBlockInputFormat]) + + // const shouldShowConfigureStartInputsButton = + // Boolean(startBlockId) && missingStartReservedFields.length > 0 + // Get actual position (default if not set) const actualPosition = useMemo( () => getChatPosition(chatPosition, chatWidth, chatHeight), @@ -564,6 +667,60 @@ export function Chat() { setIsChatOpen(false) }, [setIsChatOpen]) + // /** + // * Adds any missing reserved inputs (input, conversationId, files) to the unified start block. + // */ + // const handleConfigureStartInputs = useCallback(() => { + // if (!activeWorkflowId || !startBlockId) { + // logger.warn('Cannot configure start inputs: missing active workflow ID or start block ID') + // return + // } + + // try { + // const existingFields = Array.isArray(startBlockInputFormat) + // ? [...startBlockInputFormat] + // : [] + + // const normalizedExisting = normalizeStartInputFormat(existingFields) + // const existingNames = new Set( + // normalizedExisting + // .map((field) => field.name) + // .filter((name): name is string => typeof name === 'string' && name.trim() !== '') + // .map((name) => name.trim()) + // ) + + // const updatedFields: StartInputFormatField[] = [...existingFields] + + // missingStartReservedFields.forEach((fieldName) => { + // if (existingNames.has(fieldName)) { + // return + // } + + // const defaultType = fieldName === 'files' ? 'files' : 'string' + + // updatedFields.push({ + // id: crypto.randomUUID(), + // name: fieldName, + // type: defaultType, + // value: '', + // collapsed: false, + // }) + // }) + + // setSubBlockValue(startBlockId, 'inputFormat', updatedFields) + // triggerWorkflowUpdate() + // } catch (error) { + // logger.error('Failed to configure start block reserved inputs', error) + // } + // }, [ + // activeWorkflowId, + // missingStartReservedFields, + // setSubBlockValue, + // startBlockId, + // startBlockInputFormat, + // triggerWorkflowUpdate, + // ]) + // Don't render if not open if (!isChatOpen) return null @@ -583,17 +740,32 @@ export function Chat() { > {/* Header with drag handle */}
-
- - Chat - -
+ + Chat + + + {/* Start inputs button and output selector - with max-width to prevent overflow */} +
e.stopPropagation()} + > + {/* {shouldShowConfigureStartInputsButton && ( + { + e.stopPropagation() + handleConfigureStartInputs() + }} + > + Add inputs + + )} */} - {/* Output selector - centered with mx-auto */} -
e.stopPropagation()}>
-
+
{/* More menu with actions */} @@ -628,22 +800,22 @@ export function Chat() { { e.stopPropagation() - if (activeWorkflowId) clearChat(activeWorkflowId) + if (activeWorkflowId) exportChatCSV(activeWorkflowId) }} - disabled={messages.length === 0} + disabled={workflowMessages.length === 0} > - - Clear + + Download { e.stopPropagation() - if (activeWorkflowId) exportChatCSV(activeWorkflowId) + if (activeWorkflowId) clearChat(activeWorkflowId) }} - disabled={messages.length === 0} + disabled={workflowMessages.length === 0} > - - Download + + Clear @@ -662,7 +834,7 @@ export function Chat() {
{workflowMessages.length === 0 ? (
- No messages yet + Workflow input: {''}
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts index e9050264d81..08a3e17d27d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts @@ -178,16 +178,11 @@ export function useChatResize({ (e: MouseEvent) => { if (!isResizingRef.current || !activeDirectionRef.current) return - const deltaX = e.clientX - resizeStartRef.current.x - const deltaY = e.clientY - resizeStartRef.current.y + let deltaX = e.clientX - resizeStartRef.current.x + let deltaY = e.clientY - resizeStartRef.current.y const initial = initialStateRef.current const direction = activeDirectionRef.current - let newX = initial.x - let newY = initial.y - let newWidth = initial.width - let newHeight = initial.height - // Get layout bounds const sidebarWidth = Number.parseInt( getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' @@ -199,6 +194,56 @@ export function useChatResize({ getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0' ) + // Clamp vertical drag when resizing from the top so the chat does not grow downward + // after its top edge hits the top of the viewport. + if (direction === 'top' || direction === 'top-left' || direction === 'top-right') { + // newY = initial.y + deltaY should never be less than 0 + const maxUpwardDelta = initial.y + if (deltaY < -maxUpwardDelta) { + deltaY = -maxUpwardDelta + } + } + + // Clamp vertical drag when resizing from the bottom so the chat does not grow upward + // after its bottom edge hits the top of the terminal. + if (direction === 'bottom' || direction === 'bottom-left' || direction === 'bottom-right') { + const maxBottom = window.innerHeight - terminalHeight + const initialBottom = initial.y + initial.height + const maxDeltaY = maxBottom - initialBottom + + if (deltaY > maxDeltaY) { + deltaY = maxDeltaY + } + } + + // Clamp horizontal drag when resizing from the left so the chat does not grow to the right + // after its left edge hits the sidebar. + if (direction === 'left' || direction === 'top-left' || direction === 'bottom-left') { + const minLeft = sidebarWidth + const minDeltaX = minLeft - initial.x + + if (deltaX < minDeltaX) { + deltaX = minDeltaX + } + } + + // Clamp horizontal drag when resizing from the right so the chat does not grow to the left + // after its right edge hits the panel. + if (direction === 'right' || direction === 'top-right' || direction === 'bottom-right') { + const maxRight = window.innerWidth - panelWidth + const initialRight = initial.x + initial.width + const maxDeltaX = maxRight - initialRight + + if (deltaX > maxDeltaX) { + deltaX = maxDeltaX + } + } + + let newX = initial.x + let newY = initial.y + let newWidth = initial.width + let newHeight = initial.height + // Calculate new dimensions based on resize direction switch (direction) { // Corners diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx index 7ce096f7111..20f93a9ca1e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' -import { ArrowDown, Braces, Square } from 'lucide-react' +import { Braces, Square } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { BubbleChatPreview, @@ -270,14 +270,6 @@ export function Panel() { workspaceId, ]) - /** - * Handles triggering file input for workflow import - */ - const handleImportWorkflow = useCallback(() => { - setIsMenuOpen(false) - fileInputRef.current?.click() - }, []) - // Compute run button state const canRun = userPermissions.canRead // Running only requires read permissions const isLoadingPermissions = userPermissions.isLoading @@ -349,13 +341,6 @@ export function Panel() { Export workflow - - - Import workflow - - - {/* Hidden file input for workflow import */} - ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 2699a823fe6..6f8cbdff4ea 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -721,7 +721,8 @@ export function Terminal() { align='start' sideOffset={4} onClick={(e) => e.stopPropagation()} - style={{ minWidth: '120px', maxWidth: '120px' }} + minWidth={120} + maxWidth={200} > {uniqueBlocks.map((block, index) => { diff --git a/apps/sim/stores/chat/store.ts b/apps/sim/stores/chat/store.ts index f2ef913af7c..b1fcff4a6d8 100644 --- a/apps/sim/stores/chat/store.ts +++ b/apps/sim/stores/chat/store.ts @@ -13,7 +13,7 @@ const MAX_MESSAGES = 50 /** * Floating chat dimensions */ -const DEFAULT_WIDTH = 250 +const DEFAULT_WIDTH = 330 const DEFAULT_HEIGHT = 286 /** From bbd9111c1f2c95c09d89d4fb6d06edddcbc682e6 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 19 Nov 2025 18:09:26 -0800 Subject: [PATCH 3/3] added socket event, reused existing utils to persist chat inputs --- .../w/[workflowId]/components/chat/chat.tsx | 303 +++++++++--------- apps/sim/lib/workflows/block-outputs.ts | 19 +- apps/sim/lib/workflows/input-format-utils.ts | 39 +++ 3 files changed, 184 insertions(+), 177 deletions(-) create mode 100644 apps/sim/lib/workflows/input-format-utils.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index 943fd0545f0..122ce9cc15f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -2,7 +2,6 @@ import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AlertCircle, ArrowDownToLine, ArrowUp, MoreVertical, Paperclip, X } from 'lucide-react' -import { useParams } from 'next/navigation' import { Badge, Button, @@ -14,24 +13,27 @@ import { PopoverTrigger, Trash, } from '@/components/emcn' +import { useSession } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' import { extractBlockIdFromOutputId, extractPathFromOutputId, parseOutputContentSafely, } from '@/lib/response-format' -// import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types' -// import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers' import { cn } from '@/lib/utils' +import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils' +import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers' +import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types' import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import type { BlockLog, ExecutionResult } from '@/executor/types' import { getChatPosition, useChatStore } from '@/stores/chat/store' import { useExecutionStore } from '@/stores/execution/store' +import { useOperationQueue } from '@/stores/operation-queue/store' import { useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -// import { useSubBlockStore } from '@/stores/workflows/subblock/store' -// import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { ChatMessage, OutputSelect } from './components' import { useChatBoundarySync, useChatDrag, useChatFileUpload, useChatResize } from './hooks' @@ -128,38 +130,13 @@ const formatOutputContent = (output: any): string => { return '' } -// interface StartInputFormatField { -// id?: string -// name?: string -// type?: string -// value?: unknown -// collapsed?: boolean -// } - -// /** -// * Normalizes an input format value into a list of valid fields. -// * -// * @param value - Raw input format value from subblock state. -// * @returns Array of fields with non-empty names. -// */ -// const normalizeStartInputFormat = (value: unknown): StartInputFormatField[] => { -// if (!Array.isArray(value)) { -// return [] -// } - -// return value.filter((field): field is StartInputFormatField => { -// if (!field || typeof field !== 'object') { -// return false -// } - -// const name = (field as StartInputFormatField).name -// return typeof name === 'string' && name.trim() !== '' -// }) -// } - -const CHAT_START_EXAMPLE_JSON = `"input": string, -"conversationId": string, -"files": array` +interface StartInputFormatField { + id?: string + name?: string + type?: string + value?: unknown + collapsed?: boolean +} /** * Floating chat modal component @@ -174,12 +151,10 @@ const CHAT_START_EXAMPLE_JSON = `"input": string, * position across sessions using the floating chat store. */ export function Chat() { - const params = useParams() - const workspaceId = params.workspaceId as string const { activeWorkflowId } = useWorkflowRegistry() - // const blocks = useWorkflowStore((state) => state.blocks) - // const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate) - // const setSubBlockValue = useSubBlockStore((state) => state.setValue) + const blocks = useWorkflowStore((state) => state.blocks) + const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate) + const setSubBlockValue = useSubBlockStore((state) => state.setValue) // Chat state (UI and messages from unified store) const { @@ -204,6 +179,8 @@ export function Chat() { const { entries } = useTerminalConsoleStore() const { isExecuting } = useExecutionStore() const { handleRunWorkflow } = useWorkflowExecution() + const { data: session } = useSession() + const { addToQueue } = useOperationQueue() // Local state const [chatMessage, setChatMessage] = useState('') @@ -233,65 +210,67 @@ export function Chat() { /** * Resolves the unified start block for chat execution, if available. */ - // const startBlockCandidate = useMemo(() => { - // if (!activeWorkflowId) { - // return null - // } - - // if (!blocks || Object.keys(blocks).length === 0) { - // return null - // } - - // const candidate = TriggerUtils.findStartBlock(blocks, 'chat') - // if (!candidate || candidate.path !== StartBlockPath.UNIFIED) { - // return null - // } - - // return candidate - // }, [activeWorkflowId, blocks]) - - // const startBlockId = startBlockCandidate?.blockId ?? null - - // /** - // * Reads the current input format for the unified start block from the subblock store, - // * falling back to the workflow store if no explicit value is stored yet. - // */ - // const startBlockInputFormat = useSubBlockStore((state) => { - // if (!activeWorkflowId || !startBlockId) { - // return null - // } - - // const workflowValues = state.workflowValues[activeWorkflowId] - // const fromStore = workflowValues?.[startBlockId]?.inputFormat - // if (fromStore !== undefined && fromStore !== null) { - // return fromStore - // } - - // const startBlock = blocks[startBlockId] - // return startBlock?.subBlocks?.inputFormat?.value ?? null - // }) - - // /** - // * Determines which reserved start inputs are missing from the input format. - // */ - // const missingStartReservedFields = useMemo(() => { - // if (!startBlockId) { - // return START_BLOCK_RESERVED_FIELDS - // } - - // const normalizedFields = normalizeStartInputFormat(startBlockInputFormat) - // const existingNames = new Set( - // normalizedFields - // .map((field) => field.name) - // .filter((name): name is string => typeof name === 'string' && name.trim() !== '') - // .map((name) => name.trim()) - // ) - - // return START_BLOCK_RESERVED_FIELDS.filter((fieldName) => !existingNames.has(fieldName)) - // }, [startBlockId, startBlockInputFormat]) - - // const shouldShowConfigureStartInputsButton = - // Boolean(startBlockId) && missingStartReservedFields.length > 0 + const startBlockCandidate = useMemo(() => { + if (!activeWorkflowId) { + return null + } + + if (!blocks || Object.keys(blocks).length === 0) { + return null + } + + const candidate = TriggerUtils.findStartBlock(blocks, 'chat') + if (!candidate || candidate.path !== StartBlockPath.UNIFIED) { + return null + } + + return candidate + }, [activeWorkflowId, blocks]) + + const startBlockId = startBlockCandidate?.blockId ?? null + + /** + * Reads the current input format for the unified start block from the subblock store, + * falling back to the workflow store if no explicit value is stored yet. + */ + const startBlockInputFormat = useSubBlockStore((state) => { + if (!activeWorkflowId || !startBlockId) { + return null + } + + const workflowValues = state.workflowValues[activeWorkflowId] + const fromStore = workflowValues?.[startBlockId]?.inputFormat + if (fromStore !== undefined && fromStore !== null) { + return fromStore + } + + const startBlock = blocks[startBlockId] + return startBlock?.subBlocks?.inputFormat?.value ?? null + }) + + /** + * Determines which reserved start inputs are missing from the input format. + */ + const missingStartReservedFields = useMemo(() => { + if (!startBlockId) { + return START_BLOCK_RESERVED_FIELDS + } + + const normalizedFields = normalizeInputFormatValue(startBlockInputFormat) + const existingNames = new Set( + normalizedFields + .map((field) => field.name) + .filter((name): name is string => typeof name === 'string' && name.trim() !== '') + .map((name) => name.trim().toLowerCase()) + ) + + return START_BLOCK_RESERVED_FIELDS.filter( + (fieldName) => !existingNames.has(fieldName.toLowerCase()) + ) + }, [startBlockId, startBlockInputFormat]) + + const shouldShowConfigureStartInputsButton = + Boolean(startBlockId) && missingStartReservedFields.length > 0 // Get actual position (default if not set) const actualPosition = useMemo( @@ -667,61 +646,67 @@ export function Chat() { setIsChatOpen(false) }, [setIsChatOpen]) - // /** - // * Adds any missing reserved inputs (input, conversationId, files) to the unified start block. - // */ - // const handleConfigureStartInputs = useCallback(() => { - // if (!activeWorkflowId || !startBlockId) { - // logger.warn('Cannot configure start inputs: missing active workflow ID or start block ID') - // return - // } - - // try { - // const existingFields = Array.isArray(startBlockInputFormat) - // ? [...startBlockInputFormat] - // : [] - - // const normalizedExisting = normalizeStartInputFormat(existingFields) - // const existingNames = new Set( - // normalizedExisting - // .map((field) => field.name) - // .filter((name): name is string => typeof name === 'string' && name.trim() !== '') - // .map((name) => name.trim()) - // ) - - // const updatedFields: StartInputFormatField[] = [...existingFields] - - // missingStartReservedFields.forEach((fieldName) => { - // if (existingNames.has(fieldName)) { - // return - // } - - // const defaultType = fieldName === 'files' ? 'files' : 'string' - - // updatedFields.push({ - // id: crypto.randomUUID(), - // name: fieldName, - // type: defaultType, - // value: '', - // collapsed: false, - // }) - // }) - - // setSubBlockValue(startBlockId, 'inputFormat', updatedFields) - // triggerWorkflowUpdate() - // } catch (error) { - // logger.error('Failed to configure start block reserved inputs', error) - // } - // }, [ - // activeWorkflowId, - // missingStartReservedFields, - // setSubBlockValue, - // startBlockId, - // startBlockInputFormat, - // triggerWorkflowUpdate, - // ]) - - // Don't render if not open + /** + * Adds any missing reserved inputs (input, conversationId, files) to the unified start block. + */ + const handleConfigureStartInputs = useCallback(() => { + if (!activeWorkflowId || !startBlockId) { + logger.warn('Cannot configure start inputs: missing active workflow ID or start block ID') + return + } + + try { + const normalizedExisting = normalizeInputFormatValue(startBlockInputFormat) + + const newReservedFields: StartInputFormatField[] = missingStartReservedFields.map( + (fieldName) => { + const defaultType = fieldName === 'files' ? 'files' : 'string' + + return { + id: crypto.randomUUID(), + name: fieldName, + type: defaultType, + value: '', + collapsed: false, + } + } + ) + + const updatedFields: StartInputFormatField[] = [...newReservedFields, ...normalizedExisting] + + setSubBlockValue(startBlockId, 'inputFormat', updatedFields) + + const userId = session?.user?.id || 'unknown' + addToQueue({ + id: crypto.randomUUID(), + operation: { + operation: 'subblock-update', + target: 'subblock', + payload: { + blockId: startBlockId, + subblockId: 'inputFormat', + value: updatedFields, + }, + }, + workflowId: activeWorkflowId, + userId, + }) + + triggerWorkflowUpdate() + } catch (error) { + logger.error('Failed to configure start block reserved inputs', error) + } + }, [ + activeWorkflowId, + missingStartReservedFields, + setSubBlockValue, + startBlockId, + startBlockInputFormat, + triggerWorkflowUpdate, + session, + addToQueue, + ]) + if (!isChatOpen) return null return ( @@ -752,10 +737,10 @@ export function Chat() { className='ml-auto flex min-w-0 flex-shrink items-center gap-[6px]' onMouseDown={(e) => e.stopPropagation()} > - {/* {shouldShowConfigureStartInputsButton && ( + {shouldShowConfigureStartInputsButton && ( { e.stopPropagation() @@ -764,7 +749,7 @@ export function Chat() { > Add inputs - )} */} + )} field && typeof field === 'object' && field.name && field.name.trim() !== '' - ) -} - function applyInputFormatFields( inputFormat: InputFormatField[], outputs: OutputDefinition diff --git a/apps/sim/lib/workflows/input-format-utils.ts b/apps/sim/lib/workflows/input-format-utils.ts new file mode 100644 index 00000000000..fec752d89ff --- /dev/null +++ b/apps/sim/lib/workflows/input-format-utils.ts @@ -0,0 +1,39 @@ +import type { InputFormatField } from '@/lib/workflows/types' + +/** + * Normalizes an input format value into a list of valid fields. + * + * Filters out: + * - null or undefined values + * - Empty arrays + * - Non-array values + * - Fields without names + * - Fields with empty or whitespace-only names + * + * @param inputFormatValue - Raw input format value from subblock state + * @returns Array of validated input format fields + */ +export function normalizeInputFormatValue(inputFormatValue: unknown): InputFormatField[] { + // Handle null, undefined, and empty arrays + if ( + inputFormatValue === null || + inputFormatValue === undefined || + (Array.isArray(inputFormatValue) && inputFormatValue.length === 0) + ) { + return [] + } + + // Handle non-array values + if (!Array.isArray(inputFormatValue)) { + return [] + } + + // Filter valid fields + return inputFormatValue.filter( + (field): field is InputFormatField => + field && + typeof field === 'object' && + typeof field.name === 'string' && + field.name.trim() !== '' + ) +}