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
492 changes: 272 additions & 220 deletions apps/sim/app/api/chat/utils.ts

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions apps/sim/app/chat/[subdomain]/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ import {
PasswordAuth,
VoiceInterface,
} from '@/app/chat/components'
import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants'
import { useAudioStreaming, useChatStreaming } from '@/app/chat/hooks'

const logger = createLogger('ChatClient')

// Chat timeout configuration (5 minutes)
const CHAT_REQUEST_TIMEOUT_MS = 300000

interface ChatConfig {
id: string
title: string
Expand Down Expand Up @@ -237,7 +235,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
}
} catch (error) {
logger.error('Error fetching chat config:', error)
setError('This chat is currently unavailable. Please try again later.')
setError(CHAT_ERROR_MESSAGES.CHAT_UNAVAILABLE)
}
}

Expand Down Expand Up @@ -372,7 +370,7 @@ export default function ChatClient({ subdomain }: { subdomain: string }) {
setIsLoading(false)
const errorMessage: ChatMessage = {
id: crypto.randomUUID(),
content: 'Sorry, there was an error processing your message. Please try again.',
content: CHAT_ERROR_MESSAGES.GENERIC_ERROR,
type: 'assistant',
timestamp: new Date(),
}
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/app/chat/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const CHAT_ERROR_MESSAGES = {
GENERIC_ERROR: 'Sorry, there was an error processing your message. Please try again.',
NETWORK_ERROR: 'Unable to connect to the server. Please check your connection and try again.',
TIMEOUT_ERROR: 'Request timed out. Please try again.',
AUTH_REQUIRED_PASSWORD: 'This chat requires a password to access.',
AUTH_REQUIRED_EMAIL: 'Please provide your email to access this chat.',
CHAT_UNAVAILABLE: 'This chat is currently unavailable. Please try again later.',
NO_CHAT_TRIGGER:
'No Chat trigger configured for this workflow. Add a Chat Trigger block to enable chat execution.',
USAGE_LIMIT_EXCEEDED: 'Usage limit exceeded. Please upgrade your plan to continue using chat.',
} as const

export const CHAT_REQUEST_TIMEOUT_MS = 300000 // 5 minutes (same as in chat.tsx)

export type ChatErrorType = keyof typeof CHAT_ERROR_MESSAGES
20 changes: 20 additions & 0 deletions apps/sim/app/chat/hooks/use-chat-streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useRef, useState } from 'react'
import { createLogger } from '@/lib/logs/console/logger'
import type { ChatMessage } from '@/app/chat/components/message/message'
import { CHAT_ERROR_MESSAGES } from '@/app/chat/constants'
// No longer need complex output extraction - backend handles this
import type { ExecutionResult } from '@/executor/types'

Expand Down Expand Up @@ -151,6 +152,25 @@ export function useChatStreaming() {
const json = JSON.parse(line.substring(6))
const { blockId, chunk: contentChunk, event: eventType } = json

// Handle error events from the server
if (eventType === 'error' || json.event === 'error') {
const errorMessage = json.error || CHAT_ERROR_MESSAGES.GENERIC_ERROR
setMessages((prev) =>
prev.map((msg) =>
msg.id === messageId
? {
...msg,
content: errorMessage,
isStreaming: false,
type: 'assistant' as const,
}
: msg
)
)
setIsLoading(false)
return
}

if (eventType === 'final' && json.data) {
// The backend has already processed and combined all outputs
// We just need to extract the combined content and use it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export const WorkflowEdge = ({
style={{
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
zIndex: 22,
zIndex: 100,
}}
onClick={(e) => {
e.preventDefault()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,19 @@ export function useWorkflowExecution() {
selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId)
}

// Helper to extract test values from inputFormat subblock
const extractTestValuesFromInputFormat = (inputFormatValue: any): Record<string, any> => {
const testInput: Record<string, any> = {}
if (Array.isArray(inputFormatValue)) {
inputFormatValue.forEach((field: any) => {
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
testInput[field.name] = field.value
}
})
}
return testInput
}

// Determine start block and workflow input based on execution type
let startBlockId: string | undefined
let finalWorkflowInput = workflowInput
Expand Down Expand Up @@ -720,19 +733,12 @@ export function useWorkflowExecution() {
// Extract test values from the API trigger's inputFormat
if (selectedTrigger.type === 'api_trigger' || selectedTrigger.type === 'starter') {
const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
if (Array.isArray(inputFormatValue)) {
const testInput: Record<string, any> = {}
inputFormatValue.forEach((field: any) => {
if (field && typeof field === 'object' && field.name && field.value !== undefined) {
testInput[field.name] = field.value
}
})
const testInput = extractTestValuesFromInputFormat(inputFormatValue)

// Use the test input as workflow input
if (Object.keys(testInput).length > 0) {
finalWorkflowInput = testInput
logger.info('Using API trigger test values for manual run:', testInput)
}
// Use the test input as workflow input
if (Object.keys(testInput).length > 0) {
finalWorkflowInput = testInput
logger.info('Using API trigger test values for manual run:', testInput)
}
}
}
Expand All @@ -741,18 +747,29 @@ export function useWorkflowExecution() {
logger.error('Multiple API triggers found')
setIsExecuting(false)
throw error
} else if (manualTriggers.length === 1) {
// No API trigger, check for manual trigger
selectedTrigger = manualTriggers[0]
} else if (manualTriggers.length >= 1) {
// No API trigger, check for manual triggers
// Prefer manual_trigger over input_trigger for simple runs
const manualTrigger = manualTriggers.find((t) => t.type === 'manual_trigger')
const inputTrigger = manualTriggers.find((t) => t.type === 'input_trigger')

selectedTrigger = manualTrigger || inputTrigger || manualTriggers[0]
const blockEntry = entries.find(([, block]) => block === selectedTrigger)
if (blockEntry) {
selectedBlockId = blockEntry[0]

// Extract test values from input trigger's inputFormat if it's an input_trigger
if (selectedTrigger.type === 'input_trigger') {
const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value
const testInput = extractTestValuesFromInputFormat(inputFormatValue)

// Use the test input as workflow input
if (Object.keys(testInput).length > 0) {
finalWorkflowInput = testInput
logger.info('Using Input trigger test values for manual run:', testInput)
}
}
}
} else if (manualTriggers.length > 1) {
const error = new Error('Multiple Input Trigger blocks found. Keep only one.')
logger.error('Multiple input triggers found')
setIsExecuting(false)
throw error
} else {
// Fallback: Check for legacy starter block
const starterBlock = Object.values(filteredStates).find((block) => block.type === 'starter')
Expand All @@ -769,8 +786,8 @@ export function useWorkflowExecution() {
}

if (!selectedBlockId || !selectedTrigger) {
const error = new Error('Manual run requires an Input Trigger or API Trigger block')
logger.error('No input or API triggers found for manual run')
const error = new Error('Manual run requires a Manual, Input Form, or API Trigger block')
logger.error('No manual/input or API triggers found for manual run')
setIsExecuting(false)
throw error
}
Expand Down
14 changes: 8 additions & 6 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1751,9 +1751,7 @@ const WorkflowContent = React.memo(() => {

// An edge is inside a loop if either source or target has a parent
// If source and target have different parents, prioritize source's parent
const parentLoopId =
(sourceNode?.id && blocks[sourceNode.id]?.data?.parentId) ||
(targetNode?.id && blocks[targetNode.id]?.data?.parentId)
const parentLoopId = sourceNode?.parentId || targetNode?.parentId

// Create a unique identifier that combines edge ID and parent context
const contextId = `${edge.id}${parentLoopId ? `-${parentLoopId}` : ''}`
Expand All @@ -1772,9 +1770,7 @@ const WorkflowContent = React.memo(() => {
// Check if this edge connects nodes inside a loop
const sourceNode = getNodes().find((n) => n.id === edge.source)
const targetNode = getNodes().find((n) => n.id === edge.target)
const parentLoopId =
(sourceNode?.id && blocks[sourceNode.id]?.data?.parentId) ||
(targetNode?.id && blocks[targetNode.id]?.data?.parentId)
const parentLoopId = sourceNode?.parentId || targetNode?.parentId
const isInsideLoop = Boolean(parentLoopId)

// Create a unique context ID for this edge
Expand Down Expand Up @@ -1867,6 +1863,12 @@ const WorkflowContent = React.memo(() => {
return (
<div className='flex h-screen w-full flex-col overflow-hidden'>
<div className='relative h-full w-full flex-1 transition-all duration-200'>
<style jsx global>{`
/* Ensure edge labels (e.g., delete X) render above group/subflow nodes */
.react-flow__edge-labels {
z-index: 60 !important;
}
`}</style>
<div className='fixed top-0 right-0 z-10'>
<Panel />
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/blocks/blocks/input_trigger.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { SVGProps } from 'react'
import { createElement } from 'react'
import { Play } from 'lucide-react'
import { FormInput } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types'

const InputTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(Play, props)
const InputTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(FormInput, props)

export const InputTriggerBlock: BlockConfig = {
type: 'input_trigger',
Expand Down
31 changes: 31 additions & 0 deletions apps/sim/blocks/blocks/manual_trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { SVGProps } from 'react'
import { createElement } from 'react'
import { Play } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types'

const ManualTriggerIcon = (props: SVGProps<SVGSVGElement>) => createElement(Play, props)

export const ManualTriggerBlock: BlockConfig = {
type: 'manual_trigger',
name: 'Manual',
description: 'Start workflow manually from the editor',
longDescription:
'Trigger the workflow manually without defining an input schema. Useful for simple runs where no structured input is needed.',
bestPractices: `
- Use when you want a simple manual start without defining an input format.
- If you need structured inputs or child workflows to map variables from, prefer the Input Form Trigger.
`,
category: 'triggers',
bgColor: '#2563EB',
icon: ManualTriggerIcon,
subBlocks: [],
tools: {
access: [],
},
inputs: {},
outputs: {},
triggers: {
enabled: true,
available: ['manual'],
},
}
4 changes: 2 additions & 2 deletions apps/sim/blocks/blocks/workflow_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const getAvailableWorkflows = (): Array<{ label: string; id: string }> => {
export const WorkflowInputBlock: BlockConfig = {
type: 'workflow_input',
name: 'Workflow',
description: 'Execute another workflow and map variables to its Input Trigger schema.',
longDescription: `Execute another child workflow and map variables to its Input Trigger schema. Helps with modularizing workflows.`,
description: 'Execute another workflow and map variables to its Input Form Trigger schema.',
longDescription: `Execute another child workflow and map variables to its Input Form Trigger schema. Helps with modularizing workflows.`,
bestPractices: `
- Usually clarify/check if the user has tagged a workflow to use as the child workflow. Understand the child workflow to determine the logical position of this block in the workflow.
- Remember, that the start point of the child workflow is the Input Form Trigger block.
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/blocks/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { JiraBlock } from '@/blocks/blocks/jira'
import { KnowledgeBlock } from '@/blocks/blocks/knowledge'
import { LinearBlock } from '@/blocks/blocks/linear'
import { LinkupBlock } from '@/blocks/blocks/linkup'
import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
import { McpBlock } from '@/blocks/blocks/mcp'
import { Mem0Block } from '@/blocks/blocks/mem0'
import { MemoryBlock } from '@/blocks/blocks/memory'
Expand Down Expand Up @@ -153,6 +154,7 @@ export const registry: Record<string, BlockConfig> = {
starter: StarterBlock,
input_trigger: InputTriggerBlock,
chat_trigger: ChatTriggerBlock,
manual_trigger: ManualTriggerBlock,
api_trigger: ApiTriggerBlock,
supabase: SupabaseBlock,
tavily: TavilyBlock,
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/executor/__test-utils__/executor-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,8 @@ export const createParallelManagerMock = (options?: {
getIterationItem: vi.fn(),
areAllVirtualBlocksExecuted: vi
.fn()
.mockImplementation((parallelId, parallel, executedBlocks, state) => {
.mockImplementation((parallelId, parallel, executedBlocks, state, context) => {
// Simple mock implementation - check all blocks (ignoring conditional routing for tests)
for (const nodeId of parallel.nodes) {
for (let i = 0; i < state.parallelCount; i++) {
const virtualBlockId = `${nodeId}_parallel_${parallelId}_iteration_${i}`
Expand Down
2 changes: 0 additions & 2 deletions apps/sim/executor/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export enum BlockType {
RESPONSE = 'response',
WORKFLOW = 'workflow',
STARTER = 'starter',
SCHEDULE = 'schedule',
WEBHOOK_TRIGGER = 'webhook_trigger',
}

/**
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/executor/handlers/condition/condition-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ export class ConditionBlockHandler implements BlockHandler {
`Condition block ${block.id} selected path: ${selectedCondition.title} (${selectedCondition.id}) -> ${targetBlock.metadata?.name || targetBlock.id}`
)

// Update context decisions
context.decisions.condition.set(block.id, selectedCondition.id)
// Update context decisions - use virtual block ID if available (for parallel execution)
const decisionKey = context.currentVirtualBlockId || block.id
context.decisions.condition.set(decisionKey, selectedCondition.id)

// Return output, preserving source output structure if possible
return {
Expand Down
19 changes: 8 additions & 11 deletions apps/sim/executor/handlers/parallel/parallel-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/consts'
import { ParallelRoutingUtils } from '@/executor/parallels/utils'
import type { PathTracker } from '@/executor/path/path'
import type { InputResolver } from '@/executor/resolver/resolver'
import { Routing } from '@/executor/routing/routing'
Expand Down Expand Up @@ -338,17 +339,13 @@ export class ParallelBlockHandler implements BlockHandler {

if (!parallel || !parallelState) return false

// Check each node in the parallel for all iterations
for (const nodeId of parallel.nodes) {
for (let i = 0; i < parallelState.parallelCount; i++) {
const virtualBlockId = `${nodeId}_parallel_${parallelId}_iteration_${i}`
if (!context.executedBlocks.has(virtualBlockId)) {
return false
}
}
}

return true
// Use the shared utility that respects conditional routing
return ParallelRoutingUtils.areAllRequiredVirtualBlocksExecuted(
parallel,
parallelState.parallelCount,
context.executedBlocks,
context
)
}

/**
Expand Down
Loading