Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProcessedAttachment[]> => {
return Promise.all(
chatFiles.map(async (file) => {
let dataUrl = ''
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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<HTMLInputElement>) => {
Expand Down Expand Up @@ -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[]) => {
Expand Down Expand Up @@ -819,7 +864,7 @@ export function Chat() {
<div className='flex-1 overflow-hidden'>
{workflowMessages.length === 0 ? (
<div className='flex h-full items-center justify-center text-[#8D8D8D] text-[13px]'>
Workflow input: {'<start.input>'}
No messages yet
</div>
) : (
<div ref={scrollAreaRef} className='h-full overflow-y-auto overflow-x-hidden'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [],
Expand Down Expand Up @@ -94,7 +115,7 @@ export function OutputSelect({
: subBlockValues?.[block.id]?.responseFormat
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)

let outputsToProcess: Record<string, any> = {}
let outputsToProcess: Record<string, unknown> = {}

if (responseFormat) {
const schemaFields = extractFieldsFromSchema(responseFormat)
Expand All @@ -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}`,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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 =
Expand All @@ -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
Expand Down Expand Up @@ -359,7 +388,7 @@ export function OutputSelect({
<div ref={triggerRef} className='min-w-0 max-w-full'>
<Badge
variant='outline'
className='min-w-0 max-w-full cursor-pointer rounded-[4px] border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]'
className='flex-none cursor-pointer whitespace-nowrap rounded-[6px]'
title='Select outputs'
aria-expanded={open}
onMouseDown={(e) => {
Expand All @@ -368,7 +397,7 @@ export function OutputSelect({
setOpen((prev) => !prev)
}}
>
<span className='min-w-0 flex-1 truncate'>{selectedOutputsDisplayText}</span>
<span className='whitespace-nowrap text-[12px]'>{selectedOutputsDisplayText}</span>
</Badge>
</div>
</PopoverTrigger>
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/stores/chat/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const MAX_MESSAGES = 50
/**
* Floating chat dimensions
*/
const DEFAULT_WIDTH = 330
const DEFAULT_WIDTH = 305
const DEFAULT_HEIGHT = 286

/**
Expand Down