improvement(chat): ui (#2089)

This commit is contained in:
Emir Karabeg
2025-11-20 19:16:51 -08:00
committed by GitHub
parent 022b4f64a7
commit 6187561219
3 changed files with 92 additions and 18 deletions

View File

@@ -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 = ''
@@ -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<HTMLInputElement>) => {
@@ -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() {
<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'>

View File

@@ -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<string, any> = {}
let outputsToProcess: Record<string, unknown> = {}
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({
<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) => {
@@ -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>

View File

@@ -13,7 +13,7 @@ const MAX_MESSAGES = 50
/**
* Floating chat dimensions
*/
const DEFAULT_WIDTH = 330
const DEFAULT_WIDTH = 305
const DEFAULT_HEIGHT = 286
/**