From d44c75f486da9412eeeb898a09ce6b28817f7b1f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 29 Jan 2026 13:17:27 -0800 Subject: [PATCH] Add toggle, haven't tested --- .../messages-input/messages-input.tsx | 478 ++++++++++-------- .../executor/handlers/agent/agent-handler.ts | 247 ++++++++- apps/sim/executor/handlers/agent/types.ts | 18 +- apps/sim/providers/types.ts | 18 +- 4 files changed, 545 insertions(+), 216 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx index 30b3fa2e9..e69f04b0b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx @@ -8,11 +8,19 @@ import { useState, } from 'react' import { isEqual } from 'lodash' -import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react' -import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn' +import { ArrowLeftRight, ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react' +import { + Button, + Popover, + PopoverContent, + PopoverItem, + PopoverTrigger, + Tooltip, +} from '@/components/emcn' import { Trash } from '@/components/emcn/icons/trash' import { cn } from '@/lib/core/utils/cn' import { EnvVarDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' +import { FileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload' import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown' import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input' @@ -27,13 +35,13 @@ const MAX_TEXTAREA_HEIGHT_PX = 320 /** Pattern to match complete message objects in JSON */ const COMPLETE_MESSAGE_PATTERN = - /"role"\s*:\s*"(system|user|assistant)"[^}]*"content"\s*:\s*"((?:[^"\\]|\\.)*)"/g + /"role"\s*:\s*"(system|user|assistant|media)"[^}]*"content"\s*:\s*"((?:[^"\\]|\\.)*)"/g /** Pattern to match incomplete content at end of buffer */ const INCOMPLETE_CONTENT_PATTERN = /"content"\s*:\s*"((?:[^"\\]|\\.)*)$/ /** Pattern to match role before content */ -const ROLE_BEFORE_CONTENT_PATTERN = /"role"\s*:\s*"(system|user|assistant)"[^{]*$/ +const ROLE_BEFORE_CONTENT_PATTERN = /"role"\s*:\s*"(system|user|assistant|media)"[^{]*$/ /** * Unescapes JSON string content @@ -41,41 +49,40 @@ const ROLE_BEFORE_CONTENT_PATTERN = /"role"\s*:\s*"(system|user|assistant)"[^{]* const unescapeContent = (str: string): string => str.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\') + +/** + * Media content for multimodal messages + */ +interface MediaContent { + data: string + mimeType?: string + fileName?: string +} + /** * Interface for individual message in the messages array */ interface Message { - role: 'system' | 'user' | 'assistant' + role: 'system' | 'user' | 'assistant' | 'media' content: string + media?: MediaContent } /** * Props for the MessagesInput component */ interface MessagesInputProps { - /** Unique identifier for the block */ blockId: string - /** Unique identifier for the sub-block */ subBlockId: string - /** Configuration object for the sub-block */ config: SubBlockConfig - /** Whether component is in preview mode */ isPreview?: boolean - /** Value to display in preview mode */ previewValue?: Message[] | null - /** Whether the input is disabled */ disabled?: boolean - /** Ref to expose wand control handlers to parent */ wandControlRef?: React.MutableRefObject } /** * MessagesInput component for managing LLM message history - * - * @remarks - * - Manages an array of messages with role and content - * - Each message can be edited, removed, or reordered - * - Stores data in LLM-compatible format: [{ role, content }] */ export function MessagesInput({ blockId, @@ -90,6 +97,10 @@ export function MessagesInput({ const [localMessages, setLocalMessages] = useState([{ role: 'user', content: '' }]) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) const [openPopoverIndex, setOpenPopoverIndex] = useState(null) + + // Local media mode state - basic = FileUpload, advanced = URL/base64 textarea + const [mediaMode, setMediaMode] = useState<'basic' | 'advanced'>('basic') + const subBlockInput = useSubBlockInput({ blockId, subBlockId, @@ -98,43 +109,38 @@ export function MessagesInput({ disabled, }) - /** - * Gets the current messages as JSON string for wand context - */ const getMessagesJson = useCallback((): string => { if (localMessages.length === 0) return '' - // Filter out empty messages for cleaner context const nonEmptyMessages = localMessages.filter((m) => m.content.trim() !== '') if (nonEmptyMessages.length === 0) return '' return JSON.stringify(nonEmptyMessages, null, 2) }, [localMessages]) - /** - * Streaming buffer for accumulating JSON content - */ const streamBufferRef = useRef('') - /** - * Parses and validates messages from JSON content - */ const parseMessages = useCallback((content: string): Message[] | null => { try { const parsed = JSON.parse(content) if (Array.isArray(parsed)) { const validMessages: Message[] = parsed .filter( - (m): m is { role: string; content: string } => + (m): m is { role: string; content: string; media?: MediaContent } => typeof m === 'object' && m !== null && typeof m.role === 'string' && typeof m.content === 'string' ) - .map((m) => ({ - role: (['system', 'user', 'assistant'].includes(m.role) - ? m.role - : 'user') as Message['role'], - content: m.content, - })) + .map((m) => { + const role = ['system', 'user', 'assistant', 'media'].includes(m.role) ? m.role : 'user' + const message: Message = { + role: role as Message['role'], + content: m.content, + } + if (m.media) { + message.media = m.media + } + return message + }) return validMessages.length > 0 ? validMessages : null } } catch { @@ -143,26 +149,19 @@ export function MessagesInput({ return null }, []) - /** - * Extracts messages from streaming JSON buffer - * Uses simple pattern matching for efficiency - */ const extractStreamingMessages = useCallback( (buffer: string): Message[] => { - // Try complete JSON parse first const complete = parseMessages(buffer) if (complete) return complete const result: Message[] = [] - // Reset regex lastIndex for global pattern COMPLETE_MESSAGE_PATTERN.lastIndex = 0 let match while ((match = COMPLETE_MESSAGE_PATTERN.exec(buffer)) !== null) { result.push({ role: match[1] as Message['role'], content: unescapeContent(match[2]) }) } - // Check for incomplete message at end (content still streaming) const lastContentIdx = buffer.lastIndexOf('"content"') if (lastContentIdx !== -1) { const tail = buffer.slice(lastContentIdx) @@ -172,7 +171,6 @@ export function MessagesInput({ const roleMatch = head.match(ROLE_BEFORE_CONTENT_PATTERN) if (roleMatch) { const content = unescapeContent(incomplete[1]) - // Only add if not duplicate of last complete message if (result.length === 0 || result[result.length - 1].content !== content) { result.push({ role: roleMatch[1] as Message['role'], content }) } @@ -185,9 +183,6 @@ export function MessagesInput({ [parseMessages] ) - /** - * Wand hook for AI-assisted content generation - */ const wandHook = useWand({ wandConfig: config.wandConfig, currentValue: getMessagesJson(), @@ -208,7 +203,6 @@ export function MessagesInput({ setLocalMessages(validMessages) setMessages(validMessages) } else { - // Fallback: treat as raw system prompt const trimmed = content.trim() if (trimmed) { const fallback: Message[] = [{ role: 'system', content: trimmed }] @@ -219,9 +213,6 @@ export function MessagesInput({ }, }) - /** - * Expose wand control handlers to parent via ref - */ useImperativeHandle( wandControlRef, () => ({ @@ -249,9 +240,6 @@ export function MessagesInput({ } }, [isPreview, previewValue, messages]) - /** - * Gets the current messages array - */ const currentMessages = useMemo(() => { if (isPreview && previewValue && Array.isArray(previewValue)) { return previewValue @@ -269,9 +257,6 @@ export function MessagesInput({ startHeight: number } | null>(null) - /** - * Updates a specific message's content - */ const updateMessageContent = useCallback( (index: number, content: string) => { if (isPreview || disabled) return @@ -287,17 +272,26 @@ export function MessagesInput({ [localMessages, setMessages, isPreview, disabled] ) - /** - * Updates a specific message's role - */ const updateMessageRole = useCallback( - (index: number, role: 'system' | 'user' | 'assistant') => { + (index: number, role: 'system' | 'user' | 'assistant' | 'media') => { if (isPreview || disabled) return const updatedMessages = [...localMessages] - updatedMessages[index] = { - ...updatedMessages[index], - role, + if (role === 'media') { + updatedMessages[index] = { + ...updatedMessages[index], + role, + content: updatedMessages[index].content || '', + media: updatedMessages[index].media || { + data: '', + }, + } + } else { + const { media: _, ...rest } = updatedMessages[index] + updatedMessages[index] = { + ...rest, + role, + } } setLocalMessages(updatedMessages) setMessages(updatedMessages) @@ -305,9 +299,6 @@ export function MessagesInput({ [localMessages, setMessages, isPreview, disabled] ) - /** - * Adds a message after the specified index - */ const addMessageAfter = useCallback( (index: number) => { if (isPreview || disabled) return @@ -320,9 +311,6 @@ export function MessagesInput({ [localMessages, setMessages, isPreview, disabled] ) - /** - * Deletes a message at the specified index - */ const deleteMessage = useCallback( (index: number) => { if (isPreview || disabled) return @@ -335,9 +323,6 @@ export function MessagesInput({ [localMessages, setMessages, isPreview, disabled] ) - /** - * Moves a message up in the list - */ const moveMessageUp = useCallback( (index: number) => { if (isPreview || disabled || index === 0) return @@ -352,9 +337,6 @@ export function MessagesInput({ [localMessages, setMessages, isPreview, disabled] ) - /** - * Moves a message down in the list - */ const moveMessageDown = useCallback( (index: number) => { if (isPreview || disabled || index === localMessages.length - 1) return @@ -369,18 +351,11 @@ export function MessagesInput({ [localMessages, setMessages, isPreview, disabled] ) - /** - * Capitalizes the first letter of the role - */ const formatRole = (role: string): string => { return role.charAt(0).toUpperCase() + role.slice(1) } - /** - * Handles header click to focus the textarea - */ const handleHeaderClick = useCallback((index: number, e: React.MouseEvent) => { - // Don't focus if clicking on interactive elements const target = e.target as HTMLElement if (target.closest('button') || target.closest('[data-radix-popper-content-wrapper]')) { return @@ -570,50 +545,52 @@ export function MessagesInput({ className='flex cursor-pointer items-center justify-between px-[8px] pt-[6px]' onClick={(e) => handleHeaderClick(index, e)} > - setOpenPopoverIndex(open ? index : null)} - > - - - - -
- {(['system', 'user', 'assistant'] as const).map((role) => ( - { - updateMessageRole(index, role) - setOpenPopoverIndex(null) - }} - > - {formatRole(role)} - - ))} -
-
-
+
+ setOpenPopoverIndex(open ? index : null)} + > + + + + +
+ {(['system', 'user', 'assistant', 'media'] as const).map((role) => ( + { + updateMessageRole(index, role) + setOpenPopoverIndex(null) + }} + > + {formatRole(role)} + + ))} +
+
+
+
{!isPreview && !disabled && (
@@ -657,6 +634,43 @@ export function MessagesInput({ )} + {/* Mode toggle for media messages */} + {message.role === 'media' && ( + + + + + +

+ {mediaMode === 'advanced' + ? 'Switch to file upload' + : 'Switch to URL/text input'} +

+
+
+ )}
- {/* Content Input with overlay for variable highlighting */} -
-