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 @@ -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
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
34 changes: 28 additions & 6 deletions apps/sim/executor/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BlockPathCalculator } from '@/lib/block-path-calculator'
import { createLogger } from '@/lib/logs/console/logger'
import type { TraceSpan } from '@/lib/logs/types'
import { getBlock } from '@/blocks'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/consts'
import {
Expand Down Expand Up @@ -1779,9 +1780,16 @@ export class Executor {

context.blockLogs.push(blockLog)

// Skip console logging for infrastructure blocks like loops and parallels
// Skip console logging for infrastructure blocks and trigger blocks
// For streaming blocks, we'll add the console entry after stream processing
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
const blockConfig = getBlock(block.metadata?.id || '')
const isTriggerBlock =
blockConfig?.category === 'triggers' || block.metadata?.id === BlockType.STARTER
if (
block.metadata?.id !== BlockType.LOOP &&
block.metadata?.id !== BlockType.PARALLEL &&
!isTriggerBlock
) {
// Determine iteration context for this block
let iterationCurrent: number | undefined
let iterationTotal: number | undefined
Expand Down Expand Up @@ -1889,8 +1897,15 @@ export class Executor {

context.blockLogs.push(blockLog)

// Skip console logging for infrastructure blocks like loops and parallels
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
// Skip console logging for infrastructure blocks and trigger blocks
const nonStreamBlockConfig = getBlock(block.metadata?.id || '')
const isNonStreamTriggerBlock =
nonStreamBlockConfig?.category === 'triggers' || block.metadata?.id === BlockType.STARTER
if (
block.metadata?.id !== BlockType.LOOP &&
block.metadata?.id !== BlockType.PARALLEL &&
!isNonStreamTriggerBlock
) {
// Determine iteration context for this block
let iterationCurrent: number | undefined
let iterationTotal: number | undefined
Expand Down Expand Up @@ -2001,8 +2016,15 @@ export class Executor {
// Log the error even if we'll continue execution through error path
context.blockLogs.push(blockLog)

// Skip console logging for infrastructure blocks like loops and parallels
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
// Skip console logging for infrastructure blocks and trigger blocks
const errorBlockConfig = getBlock(block.metadata?.id || '')
const isErrorTriggerBlock =
errorBlockConfig?.category === 'triggers' || block.metadata?.id === BlockType.STARTER
if (
block.metadata?.id !== BlockType.LOOP &&
block.metadata?.id !== BlockType.PARALLEL &&
!isErrorTriggerBlock
) {
// Determine iteration context for this block
let iterationCurrent: number | undefined
let iterationTotal: number | undefined
Expand Down
22 changes: 17 additions & 5 deletions apps/sim/lib/workflows/triggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getBlock } from '@/blocks'
*/
export const TRIGGER_TYPES = {
INPUT: 'input_trigger',
MANUAL: 'manual_trigger',
CHAT: 'chat_trigger',
API: 'api_trigger',
WEBHOOK: 'webhook',
Expand Down Expand Up @@ -81,7 +82,7 @@ export class TriggerUtils {
* Check if a block is a manual-compatible trigger
*/
static isManualTrigger(block: { type: string; subBlocks?: any }): boolean {
if (block.type === TRIGGER_TYPES.INPUT) {
if (block.type === TRIGGER_TYPES.INPUT || block.type === TRIGGER_TYPES.MANUAL) {
return true
}

Expand Down Expand Up @@ -139,6 +140,8 @@ export class TriggerUtils {
return 'Chat'
case TRIGGER_TYPES.INPUT:
return 'Input Trigger'
case TRIGGER_TYPES.MANUAL:
return 'Manual'
case TRIGGER_TYPES.API:
return 'API'
case TRIGGER_TYPES.WEBHOOK:
Expand Down Expand Up @@ -216,12 +219,14 @@ export class TriggerUtils {
* Check if a trigger type requires single instance constraint
*/
static requiresSingleInstance(triggerType: string): boolean {
// API and Input triggers cannot coexist with each other
// Chat trigger must be unique
// Schedules and webhooks can coexist with API/Input triggers
// Each trigger type can only have one instance of itself
// Manual and Input Form can coexist
// API, Chat triggers must be unique
// Schedules and webhooks can have multiple instances
return (
triggerType === TRIGGER_TYPES.API ||
triggerType === TRIGGER_TYPES.INPUT ||
triggerType === TRIGGER_TYPES.MANUAL ||
triggerType === TRIGGER_TYPES.CHAT
)
}
Expand All @@ -244,11 +249,12 @@ export class TriggerUtils {
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
const hasLegacyStarter = TriggerUtils.hasLegacyStarter(blocks)

// Legacy starter block can't coexist with Chat, Input, or API triggers
// Legacy starter block can't coexist with Chat, Input, Manual, or API triggers
if (hasLegacyStarter) {
if (
triggerType === TRIGGER_TYPES.CHAT ||
triggerType === TRIGGER_TYPES.INPUT ||
triggerType === TRIGGER_TYPES.MANUAL ||
triggerType === TRIGGER_TYPES.API
) {
return true
Expand All @@ -260,6 +266,7 @@ export class TriggerUtils {
(block) =>
block.type === TRIGGER_TYPES.CHAT ||
block.type === TRIGGER_TYPES.INPUT ||
block.type === TRIGGER_TYPES.MANUAL ||
block.type === TRIGGER_TYPES.API
)
if (hasModernTriggers) {
Expand All @@ -272,6 +279,11 @@ export class TriggerUtils {
return blockArray.some((block) => block.type === TRIGGER_TYPES.INPUT)
}

// Only one Manual trigger allowed
if (triggerType === TRIGGER_TYPES.MANUAL) {
return blockArray.some((block) => block.type === TRIGGER_TYPES.MANUAL)
}

// Only one API trigger allowed
if (triggerType === TRIGGER_TYPES.API) {
return blockArray.some((block) => block.type === TRIGGER_TYPES.API)
Expand Down