mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
improvement(chat): ui (#2089)
This commit is contained in:
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,7 @@ const MAX_MESSAGES = 50
|
||||
/**
|
||||
* Floating chat dimensions
|
||||
*/
|
||||
const DEFAULT_WIDTH = 330
|
||||
const DEFAULT_WIDTH = 305
|
||||
const DEFAULT_HEIGHT = 286
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user