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 122ce9cc15f..6258731f282 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 @@ -51,12 +51,34 @@ const formatFileSize = (bytes: number): string => { return `${Math.round((bytes / 1024 ** i) * 10) / 10} ${units[i]}` } +/** + * Represents a chat file attachment before processing + */ +interface ChatFile { + id: string + name: string + type: string + size: number + file: File +} + +/** + * Represents a processed file attachment with data URL for display + */ +interface ProcessedAttachment { + id: string + name: string + type: string + size: number + dataUrl: string +} + /** * Reads files and converts them to data URLs for image display * @param chatFiles - Array of chat files to process * @returns Promise resolving to array of files with data URLs for images */ -const processFileAttachments = async (chatFiles: any[]) => { +const processFileAttachments = async (chatFiles: ChatFile[]): Promise => { return Promise.all( chatFiles.map(async (file) => { let dataUrl = '' @@ -89,7 +111,7 @@ const processFileAttachments = async (chatFiles: any[]) => { * @param outputId - Output identifier in format blockId or blockId.path * @returns Extracted output value or undefined if not found */ -const extractOutputFromLogs = (logs: BlockLog[] | undefined, outputId: string): any | undefined => { +const extractOutputFromLogs = (logs: BlockLog[] | undefined, outputId: string): unknown => { const blockId = extractBlockIdFromOutputId(outputId) const path = extractPathFromOutputId(outputId, blockId) const log = logs?.find((l) => l.blockId === blockId) @@ -120,7 +142,7 @@ const extractOutputFromLogs = (logs: BlockLog[] | undefined, outputId: string): * @param output - Output value to format (string, object, or other) * @returns Formatted string, markdown code block for objects, or empty string */ -const formatOutputContent = (output: any): string => { +const formatOutputContent = (output: unknown): string => { if (typeof output === 'string') { return output } @@ -130,6 +152,9 @@ const formatOutputContent = (output: any): string => { return '' } +/** + * Represents a field in the start block's input format configuration + */ interface StartInputFormatField { id?: string name?: string @@ -379,6 +404,7 @@ export function Chat() { /** * Focuses the input field with optional delay + * @param delay - Delay in milliseconds before focusing (default: 0) */ const focusInput = useCallback((delay = 0) => { timeoutRef.current && clearTimeout(timeoutRef.current) @@ -400,6 +426,9 @@ export function Chat() { /** * Processes streaming response from workflow execution + * Reads the stream chunk by chunk and updates the message content in real-time + * @param stream - ReadableStream containing the workflow execution response + * @param responseMessageId - ID of the message to update with streamed content */ const processStreamingResponse = useCallback( async (stream: ReadableStream, responseMessageId: string) => { @@ -462,10 +491,12 @@ export function Chat() { /** * Handles workflow execution response + * @param result - The workflow execution result containing stream or logs */ const handleWorkflowResponse = useCallback( - (result: any) => { + (result: unknown) => { if (!result || !activeWorkflowId) return + if (typeof result !== 'object') return // Handle streaming response if ('stream' in result && result.stream instanceof ReadableStream) { @@ -482,9 +513,9 @@ export function Chat() { } // Handle success with logs - if ('success' in result && result.success && 'logs' in result) { + if ('success' in result && result.success && 'logs' in result && Array.isArray(result.logs)) { selectedOutputs - .map((outputId) => extractOutputFromLogs(result.logs, outputId)) + .map((outputId) => extractOutputFromLogs(result.logs as BlockLog[], outputId)) .filter((output) => output !== undefined) .forEach((output) => { const content = formatOutputContent(output) @@ -501,7 +532,10 @@ export function Chat() { // Handle error response if ('success' in result && !result.success) { - const errorMessage = 'error' in result ? result.error : 'Workflow execution failed.' + const errorMessage = + 'error' in result && typeof result.error === 'string' + ? result.error + : 'Workflow execution failed.' addMessage({ content: `Error: ${errorMessage}`, workflowId: activeWorkflowId, @@ -514,6 +548,8 @@ export function Chat() { /** * Sends a chat message and executes the workflow + * Processes file attachments, adds the user message to the chat, + * and triggers workflow execution with the message as input */ const handleSendMessage = useCallback(async () => { if ((!chatMessage.trim() && chatFiles.length === 0) || !activeWorkflowId || isExecuting) return @@ -547,7 +583,12 @@ export function Chat() { }) // Prepare workflow input - const workflowInput: any = { + const workflowInput: { + input: string + conversationId: string + files?: Array<{ name: string; size: number; type: string; file: File }> + onUploadError?: (message: string) => void + } = { input: sentMessage, conversationId, } @@ -595,6 +636,8 @@ export function Chat() { /** * Handles keyboard input for chat + * Supports Enter to send, ArrowUp/Down to navigate prompt history + * @param e - Keyboard event from the input field */ const handleKeyPress = useCallback( (e: KeyboardEvent) => { @@ -628,6 +671,8 @@ export function Chat() { /** * Handles output selection changes + * Deduplicates and stores selected workflow outputs for the current workflow + * @param values - Array of selected output IDs or labels */ const handleOutputSelection = useCallback( (values: string[]) => { @@ -819,7 +864,7 @@ export function Chat() {
{workflowMessages.length === 0 ? (
- Workflow input: {''} + No messages yet
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index 6e610427a43..bf960b3a3ac 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -16,22 +16,43 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +/** + * Props for the OutputSelect component + */ interface OutputSelectProps { + /** The workflow ID to fetch outputs from */ workflowId: string | null + /** Array of currently selected output IDs or labels */ selectedOutputs: string[] + /** Callback fired when output selection changes */ onOutputSelect: (outputIds: string[]) => void + /** Whether the select is disabled */ disabled?: boolean + /** Placeholder text when no outputs are selected */ placeholder?: string + /** Whether to emit output IDs or labels in onOutputSelect callback */ valueMode?: 'id' | 'label' /** * When true, renders the underlying popover content inline instead of in a portal. * Useful when used inside dialogs or other portalled components that manage scroll locking. */ disablePopoverPortal?: boolean + /** Alignment of the popover relative to the trigger */ align?: 'start' | 'end' | 'center' + /** Maximum height of the popover content in pixels */ maxHeight?: number } +/** + * OutputSelect component for selecting workflow block outputs + * + * Displays a dropdown menu of all available workflow outputs grouped by block. + * Supports multi-selection, keyboard navigation, and shows visual indicators + * for selected outputs. + * + * @param props - Component props + * @returns The OutputSelect component + */ export function OutputSelect({ workflowId, selectedOutputs = [], @@ -94,7 +115,7 @@ export function OutputSelect({ : subBlockValues?.[block.id]?.responseFormat const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) - let outputsToProcess: Record = {} + let outputsToProcess: Record = {} if (responseFormat) { const schemaFields = extractFieldsFromSchema(responseFormat) @@ -111,7 +132,7 @@ export function OutputSelect({ if (Object.keys(outputsToProcess).length === 0) return - const addOutput = (path: string, outputObj: any, prefix = '') => { + const addOutput = (path: string, outputObj: unknown, prefix = '') => { const fullPath = prefix ? `${prefix}.${path}` : path const createOutput = () => ({ id: `${block.id}_${fullPath}`, @@ -146,7 +167,9 @@ export function OutputSelect({ }, [workflowBlocks, workflowId, isShowingDiff, isDiffReady, diffWorkflow, blocks, subBlockValues]) /** - * Checks if output is selected by id or label + * Checks if an output is currently selected by comparing both ID and label + * @param o - The output object to check + * @returns True if the output is selected, false otherwise */ const isSelectedValue = (o: { id: string; label: string }) => selectedOutputs.includes(o.id) || selectedOutputs.includes(o.label) @@ -234,7 +257,10 @@ export function OutputSelect({ }, [workflowOutputs, blocks]) /** - * Gets block color for an output + * Gets the background color for a block output based on its type + * @param blockId - The block ID (unused but kept for future extensibility) + * @param blockType - The type of the block + * @returns The hex color code for the block */ const getOutputColor = (blockId: string, blockType: string) => { const blockConfig = getBlock(blockType) @@ -249,7 +275,8 @@ export function OutputSelect({ }, [groupedOutputs]) /** - * Handles output selection - toggle selection + * Handles output selection by toggling the selected state + * @param value - The output label to toggle */ const handleOutputSelection = (value: string) => { const emittedValue = @@ -265,7 +292,9 @@ export function OutputSelect({ } /** - * Keyboard navigation handler + * Handles keyboard navigation within the output list + * Supports ArrowUp, ArrowDown, Enter, and Escape keys + * @param e - Keyboard event */ const handleKeyDown = (e: React.KeyboardEvent) => { if (flattenedOutputs.length === 0) return @@ -359,7 +388,7 @@ export function OutputSelect({
{ @@ -368,7 +397,7 @@ export function OutputSelect({ setOpen((prev) => !prev) }} > - {selectedOutputsDisplayText} + {selectedOutputsDisplayText}
diff --git a/apps/sim/stores/chat/store.ts b/apps/sim/stores/chat/store.ts index b1fcff4a6d8..755dd44913c 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 = 330 +const DEFAULT_WIDTH = 305 const DEFAULT_HEIGHT = 286 /**