Skip to content

Commit 6187561

Browse files
authored
improvement(chat): ui (#2089)
1 parent 022b4f6 commit 6187561

File tree

3 files changed

+92
-18
lines changed

3 files changed

+92
-18
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,34 @@ const formatFileSize = (bytes: number): string => {
5151
return `${Math.round((bytes / 1024 ** i) * 10) / 10} ${units[i]}`
5252
}
5353

54+
/**
55+
* Represents a chat file attachment before processing
56+
*/
57+
interface ChatFile {
58+
id: string
59+
name: string
60+
type: string
61+
size: number
62+
file: File
63+
}
64+
65+
/**
66+
* Represents a processed file attachment with data URL for display
67+
*/
68+
interface ProcessedAttachment {
69+
id: string
70+
name: string
71+
type: string
72+
size: number
73+
dataUrl: string
74+
}
75+
5476
/**
5577
* Reads files and converts them to data URLs for image display
5678
* @param chatFiles - Array of chat files to process
5779
* @returns Promise resolving to array of files with data URLs for images
5880
*/
59-
const processFileAttachments = async (chatFiles: any[]) => {
81+
const processFileAttachments = async (chatFiles: ChatFile[]): Promise<ProcessedAttachment[]> => {
6082
return Promise.all(
6183
chatFiles.map(async (file) => {
6284
let dataUrl = ''
@@ -89,7 +111,7 @@ const processFileAttachments = async (chatFiles: any[]) => {
89111
* @param outputId - Output identifier in format blockId or blockId.path
90112
* @returns Extracted output value or undefined if not found
91113
*/
92-
const extractOutputFromLogs = (logs: BlockLog[] | undefined, outputId: string): any | undefined => {
114+
const extractOutputFromLogs = (logs: BlockLog[] | undefined, outputId: string): unknown => {
93115
const blockId = extractBlockIdFromOutputId(outputId)
94116
const path = extractPathFromOutputId(outputId, blockId)
95117
const log = logs?.find((l) => l.blockId === blockId)
@@ -120,7 +142,7 @@ const extractOutputFromLogs = (logs: BlockLog[] | undefined, outputId: string):
120142
* @param output - Output value to format (string, object, or other)
121143
* @returns Formatted string, markdown code block for objects, or empty string
122144
*/
123-
const formatOutputContent = (output: any): string => {
145+
const formatOutputContent = (output: unknown): string => {
124146
if (typeof output === 'string') {
125147
return output
126148
}
@@ -130,6 +152,9 @@ const formatOutputContent = (output: any): string => {
130152
return ''
131153
}
132154

155+
/**
156+
* Represents a field in the start block's input format configuration
157+
*/
133158
interface StartInputFormatField {
134159
id?: string
135160
name?: string
@@ -379,6 +404,7 @@ export function Chat() {
379404

380405
/**
381406
* Focuses the input field with optional delay
407+
* @param delay - Delay in milliseconds before focusing (default: 0)
382408
*/
383409
const focusInput = useCallback((delay = 0) => {
384410
timeoutRef.current && clearTimeout(timeoutRef.current)
@@ -400,6 +426,9 @@ export function Chat() {
400426

401427
/**
402428
* Processes streaming response from workflow execution
429+
* Reads the stream chunk by chunk and updates the message content in real-time
430+
* @param stream - ReadableStream containing the workflow execution response
431+
* @param responseMessageId - ID of the message to update with streamed content
403432
*/
404433
const processStreamingResponse = useCallback(
405434
async (stream: ReadableStream, responseMessageId: string) => {
@@ -462,10 +491,12 @@ export function Chat() {
462491

463492
/**
464493
* Handles workflow execution response
494+
* @param result - The workflow execution result containing stream or logs
465495
*/
466496
const handleWorkflowResponse = useCallback(
467-
(result: any) => {
497+
(result: unknown) => {
468498
if (!result || !activeWorkflowId) return
499+
if (typeof result !== 'object') return
469500

470501
// Handle streaming response
471502
if ('stream' in result && result.stream instanceof ReadableStream) {
@@ -482,9 +513,9 @@ export function Chat() {
482513
}
483514

484515
// Handle success with logs
485-
if ('success' in result && result.success && 'logs' in result) {
516+
if ('success' in result && result.success && 'logs' in result && Array.isArray(result.logs)) {
486517
selectedOutputs
487-
.map((outputId) => extractOutputFromLogs(result.logs, outputId))
518+
.map((outputId) => extractOutputFromLogs(result.logs as BlockLog[], outputId))
488519
.filter((output) => output !== undefined)
489520
.forEach((output) => {
490521
const content = formatOutputContent(output)
@@ -501,7 +532,10 @@ export function Chat() {
501532

502533
// Handle error response
503534
if ('success' in result && !result.success) {
504-
const errorMessage = 'error' in result ? result.error : 'Workflow execution failed.'
535+
const errorMessage =
536+
'error' in result && typeof result.error === 'string'
537+
? result.error
538+
: 'Workflow execution failed.'
505539
addMessage({
506540
content: `Error: ${errorMessage}`,
507541
workflowId: activeWorkflowId,
@@ -514,6 +548,8 @@ export function Chat() {
514548

515549
/**
516550
* Sends a chat message and executes the workflow
551+
* Processes file attachments, adds the user message to the chat,
552+
* and triggers workflow execution with the message as input
517553
*/
518554
const handleSendMessage = useCallback(async () => {
519555
if ((!chatMessage.trim() && chatFiles.length === 0) || !activeWorkflowId || isExecuting) return
@@ -547,7 +583,12 @@ export function Chat() {
547583
})
548584

549585
// Prepare workflow input
550-
const workflowInput: any = {
586+
const workflowInput: {
587+
input: string
588+
conversationId: string
589+
files?: Array<{ name: string; size: number; type: string; file: File }>
590+
onUploadError?: (message: string) => void
591+
} = {
551592
input: sentMessage,
552593
conversationId,
553594
}
@@ -595,6 +636,8 @@ export function Chat() {
595636

596637
/**
597638
* Handles keyboard input for chat
639+
* Supports Enter to send, ArrowUp/Down to navigate prompt history
640+
* @param e - Keyboard event from the input field
598641
*/
599642
const handleKeyPress = useCallback(
600643
(e: KeyboardEvent<HTMLInputElement>) => {
@@ -628,6 +671,8 @@ export function Chat() {
628671

629672
/**
630673
* Handles output selection changes
674+
* Deduplicates and stores selected workflow outputs for the current workflow
675+
* @param values - Array of selected output IDs or labels
631676
*/
632677
const handleOutputSelection = useCallback(
633678
(values: string[]) => {
@@ -819,7 +864,7 @@ export function Chat() {
819864
<div className='flex-1 overflow-hidden'>
820865
{workflowMessages.length === 0 ? (
821866
<div className='flex h-full items-center justify-center text-[#8D8D8D] text-[13px]'>
822-
Workflow input: {'<start.input>'}
867+
No messages yet
823868
</div>
824869
) : (
825870
<div ref={scrollAreaRef} className='h-full overflow-y-auto overflow-x-hidden'>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,43 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
1616
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1717
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
1818

19+
/**
20+
* Props for the OutputSelect component
21+
*/
1922
interface OutputSelectProps {
23+
/** The workflow ID to fetch outputs from */
2024
workflowId: string | null
25+
/** Array of currently selected output IDs or labels */
2126
selectedOutputs: string[]
27+
/** Callback fired when output selection changes */
2228
onOutputSelect: (outputIds: string[]) => void
29+
/** Whether the select is disabled */
2330
disabled?: boolean
31+
/** Placeholder text when no outputs are selected */
2432
placeholder?: string
33+
/** Whether to emit output IDs or labels in onOutputSelect callback */
2534
valueMode?: 'id' | 'label'
2635
/**
2736
* When true, renders the underlying popover content inline instead of in a portal.
2837
* Useful when used inside dialogs or other portalled components that manage scroll locking.
2938
*/
3039
disablePopoverPortal?: boolean
40+
/** Alignment of the popover relative to the trigger */
3141
align?: 'start' | 'end' | 'center'
42+
/** Maximum height of the popover content in pixels */
3243
maxHeight?: number
3344
}
3445

46+
/**
47+
* OutputSelect component for selecting workflow block outputs
48+
*
49+
* Displays a dropdown menu of all available workflow outputs grouped by block.
50+
* Supports multi-selection, keyboard navigation, and shows visual indicators
51+
* for selected outputs.
52+
*
53+
* @param props - Component props
54+
* @returns The OutputSelect component
55+
*/
3556
export function OutputSelect({
3657
workflowId,
3758
selectedOutputs = [],
@@ -94,7 +115,7 @@ export function OutputSelect({
94115
: subBlockValues?.[block.id]?.responseFormat
95116
const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id)
96117

97-
let outputsToProcess: Record<string, any> = {}
118+
let outputsToProcess: Record<string, unknown> = {}
98119

99120
if (responseFormat) {
100121
const schemaFields = extractFieldsFromSchema(responseFormat)
@@ -111,7 +132,7 @@ export function OutputSelect({
111132

112133
if (Object.keys(outputsToProcess).length === 0) return
113134

114-
const addOutput = (path: string, outputObj: any, prefix = '') => {
135+
const addOutput = (path: string, outputObj: unknown, prefix = '') => {
115136
const fullPath = prefix ? `${prefix}.${path}` : path
116137
const createOutput = () => ({
117138
id: `${block.id}_${fullPath}`,
@@ -146,7 +167,9 @@ export function OutputSelect({
146167
}, [workflowBlocks, workflowId, isShowingDiff, isDiffReady, diffWorkflow, blocks, subBlockValues])
147168

148169
/**
149-
* Checks if output is selected by id or label
170+
* Checks if an output is currently selected by comparing both ID and label
171+
* @param o - The output object to check
172+
* @returns True if the output is selected, false otherwise
150173
*/
151174
const isSelectedValue = (o: { id: string; label: string }) =>
152175
selectedOutputs.includes(o.id) || selectedOutputs.includes(o.label)
@@ -234,7 +257,10 @@ export function OutputSelect({
234257
}, [workflowOutputs, blocks])
235258

236259
/**
237-
* Gets block color for an output
260+
* Gets the background color for a block output based on its type
261+
* @param blockId - The block ID (unused but kept for future extensibility)
262+
* @param blockType - The type of the block
263+
* @returns The hex color code for the block
238264
*/
239265
const getOutputColor = (blockId: string, blockType: string) => {
240266
const blockConfig = getBlock(blockType)
@@ -249,7 +275,8 @@ export function OutputSelect({
249275
}, [groupedOutputs])
250276

251277
/**
252-
* Handles output selection - toggle selection
278+
* Handles output selection by toggling the selected state
279+
* @param value - The output label to toggle
253280
*/
254281
const handleOutputSelection = (value: string) => {
255282
const emittedValue =
@@ -265,7 +292,9 @@ export function OutputSelect({
265292
}
266293

267294
/**
268-
* Keyboard navigation handler
295+
* Handles keyboard navigation within the output list
296+
* Supports ArrowUp, ArrowDown, Enter, and Escape keys
297+
* @param e - Keyboard event
269298
*/
270299
const handleKeyDown = (e: React.KeyboardEvent) => {
271300
if (flattenedOutputs.length === 0) return
@@ -359,7 +388,7 @@ export function OutputSelect({
359388
<div ref={triggerRef} className='min-w-0 max-w-full'>
360389
<Badge
361390
variant='outline'
362-
className='min-w-0 max-w-full cursor-pointer rounded-[4px] border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]'
391+
className='flex-none cursor-pointer whitespace-nowrap rounded-[6px]'
363392
title='Select outputs'
364393
aria-expanded={open}
365394
onMouseDown={(e) => {
@@ -368,7 +397,7 @@ export function OutputSelect({
368397
setOpen((prev) => !prev)
369398
}}
370399
>
371-
<span className='min-w-0 flex-1 truncate'>{selectedOutputsDisplayText}</span>
400+
<span className='whitespace-nowrap text-[12px]'>{selectedOutputsDisplayText}</span>
372401
</Badge>
373402
</div>
374403
</PopoverTrigger>

apps/sim/stores/chat/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const MAX_MESSAGES = 50
1313
/**
1414
* Floating chat dimensions
1515
*/
16-
const DEFAULT_WIDTH = 330
16+
const DEFAULT_WIDTH = 305
1717
const DEFAULT_HEIGHT = 286
1818

1919
/**

0 commit comments

Comments
 (0)